乐于分享
好东西不私藏

AI 编辑器 CC for Unity

AI 编辑器 CC for Unity

About OpenClaw

最近的龙虾神乎其神呀,OpenClaw 被众多人吹捧,一个 7 * 24 小时持续运行的 AI Agent 系统。

但是你了解它背后的秘密吗?其实很简单。

OpenClaw 的架构由三个模块组成:Agent Loop、Tools、Gateway。

  • • Agent Loop:龙虾的大脑,它负责决策和思考,它会根据当前任务判断下一步要做什么,并在需要的时候调用各种工具来完成行动。
  • • Tools:龙虾的手脚,为Agent Loop提供各种能力,比如浏览网页、执行命令、调用 API、处理文件等,让龙虾真正具备干活的能力。
  • • Gateway:龙虾的身体,它让整个系统能够持续在线,接收来自不同渠道的消息(比如 Telegram、飞书等),并把任务交给Agent Loop处理,再把结果返回给用户。

其中 Agent Loop、Tools 并不是 OpenClaw 独有的,Claude Code 和 Codex 都有,Gateway 是 OpenClaw 独有的模块。

Gateway 只是持续在线通信的手段,并不在我们关心的范围。Agent Loop、Tools 的实现才是我们着重关注的。

而在 Claude Code 中也有,因此这里我打算在 Unity 中做一个极简版的 Claude Code。

带大家了解其背后的秘密,其实 AI 应用层它极其简单,一步一步实现出来,也许会震碎你三观…

和引擎一样,人工智能的底层逻辑才是 AI 的命脉,后续面向 AI-Infra 才是我个人开发实践的重点。

这篇文章也是我从游戏引擎过度人工智能的开始,后续会把学习的重心放在 AI 上,待 AI 小有所成后,回过头来继续游戏引擎、继续图形学。到时候用超魔法打败魔法~

回归正题,即《AI 编辑器 MCP for Unity》的后续之作,这次要开发的是 《AI 编辑器 Claude Code for Unity》。

Anthropic SDK CSharp

最初的打算是使用 C++ 来实现一个 Claude Code CLI,但考虑到时间成本,之前也已经实现了 MCP for Unity,而且 Anthropic SDK 没有 C++ 版(可以自己实现),有 C# 版本,如果能配合 CC for Unity,便可以轻松打造一个超强的智能体。

通过将 MCP for Unity 与 CC for Unity 结合使用,两者相辅相成,能够帮助开发者深入理解如何开发 AI 工具,并形成一个完整的闭环。

我只是抛砖引玉,如果感兴趣的话,可以沿着这个思路开发下去,做出自己心仪的 AI 产品。

Anthropic SDK 是 Anthropic 官方提供的开发工具包,旨在帮助开发者将 Claude 大语言模型无缝集成到自己的应用程序中。

这里提供 C# 版本的下载地址:https://github.com/anthropics/anthropic-sdk-csharp

当前最新的版本是 v12.8.0,但我下载的是 v10.4.0。

我使用的是 Unity 6.3 版本,因为当前 Unity 只支持 C# 9 版本,而 Anthropic SDK 已经支持到了 C# 12。本以为下载低版本的做降级会轻松些,事后发现我想多了…

不得不说为 Anthropic SDK 做降级还是花了一些心思的,但不是课程的重点,因此这里只做一下总结!

Unity 中使用 Anthropic SDK 的相关修改:

  • • 代码降级到 C# 9
  • • 删除 Microsoft.Extensions.AI 依赖
  • • 清理不需要的目录(examples、tests等)
  • • 下载必需的 DLL 文件(System.Text.Json.dll、System.Collections.Immutable.dll、System.Memory.dll、System.Buffers.dll 等)如何下载 DLL?使用 NuGet 命令行:

    nuget install System.Text.Json -Version 4.7.2
    nuget install System.Collections.Immutable -Version 8.0.0
  • • System.Text.Json 兼容性修复

改动还是蛮多的…

Claude Code for Unity

在后续的开发过程中,你会发现,这里只用到了 Anthropic SDK 的 token 验证和模型交互功能。

其余的都是自己实现的,这样跟着看下来,你会对 AI Agent 开发有个整体的认知。

而且这部分代码可以完全剥离 Unity 依赖,你也可以单独提炼出来在 dotnet 以命令行去实现。

Agent Loop

Agent Loop,是 AI 接到一个任务之后,自己一步一步把事情做完的机制。

实践上它就是一个循环体,是一个和 AI 不停交互的过程,这就是 Agent Loop。

因为我想一次性在文章讲完,虽然代码不复杂但还是有些体量的,就直接贴全码了,不讲究开发阶段过程了

AgentLoop.cs

using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;

namespaceClaudeCodeForUnity
{
publicclassAgentLoop
    {
privatereadonly AnthropicClient _client;
privatereadonlystring _model;
privatereadonlystring _systemPrompt;
privatereadonly ToolManager _skillAgent;
privatereadonly BackgroundManager _backgroundTaskManager;
private ToolManager.SkillType _skillType;
private List<MessageParam> _conversationHistory;
privatelong _totalInputTokens;
privatelong _totalOutputTokens;

publicevent System.Action<longlonglong> OnTokenUsageUpdated;

publicAgentLoop(AnthropicClient client, string model, string systemPrompt,
                         ToolManager skillAgent, ToolManager.SkillType skillType = ToolManager.SkillType.Code,
                         BackgroundManager backgroundTaskManager = null
)

        {
            _client = client;
            _model = model;
            _systemPrompt = systemPrompt;
            _skillAgent = skillAgent;
            _skillType = skillType;
            _backgroundTaskManager = backgroundTaskManager;
            _conversationHistory = new List<MessageParam>();
        }

public IReadOnlyList<MessageParam> ConversationHistory => _conversationHistory;

publicvoidClearHistory()
        {
            _conversationHistory.Clear();
        }

publicasync Task<stringProcessMessageAsync(string userMessage, CancellationToken cancellationToken = default)
        {
// 添加用户消息到历史
            _conversationHistory.Add(new MessageParam
            {
                Role = Role.User,
                Content = userMessage
            });

// 运行代理循环
var result = await RunAgentLoopAsync(cancellationToken);

// 提取最终的文本响应
return ExtractTextResponse(result);
        }

privateasync Task<List<ContentBlockParam>> RunAgentLoopAsync(CancellationToken cancellationToken = default)
        {
var tools = _skillAgent.GetToolsForSkill(_skillType);

while (true)
            {
// 检查取消请求
                cancellationToken.ThrowIfCancellationRequested();
// 0. 排空后台任务通知队列,将完成的任务结果注入对话
if (_backgroundTaskManager != null)
                {
var notifications = _backgroundTaskManager.DrainNotifications();
if (notifications.Count > 0 && _conversationHistory.Count > 0)
                    {
var notifText = string.Join("\n",
                            notifications.Select(n => $"[bg:{n.TaskId}{n.Status}{n.Result}"));

// 注入后台结果作为用户消息
                        _conversationHistory.Add(new MessageParam
                        {
                            Role = Role.User,
                            Content = $"<background-results>\n{notifText}\n</background-results>"
                        });

// 助手确认
                        _conversationHistory.Add(new MessageParam
                        {
                            Role = Role.Assistant,
                            Content = "Noted background results."
                        });

                        UnityEngine.Debug.Log($"[Background Notifications] Injected {notifications.Count} completed task(s)");
                    }
                }

// 1. 调用模型
var response = await _client.Messages.Create(new MessageCreateParams
                {
                    Model = _model,
                    Messages = _conversationHistory,
                    System = _systemPrompt,
                    Tools = tools.Select(t => (ToolUnion)t).ToList(),
                    MaxTokens = 4096
                });

// 更新 token 使用统计
if (response.Usage != null)
                {
                    _totalInputTokens += response.Usage.InputTokens;
                    _totalOutputTokens += response.Usage.OutputTokens;
var totalTokens = _totalInputTokens + _totalOutputTokens;

// 触发事件通知UI更新
                    OnTokenUsageUpdated?.Invoke(_totalInputTokens, _totalOutputTokens, totalTokens);

                    UnityEngine.Debug.Log($"[Token Usage] Input: {response.Usage.InputTokens}, Output: {response.Usage.OutputTokens}, Total Session: {totalTokens}");
                }

// 2. 收集工具调用和文本输出
var toolCalls = new List<ToolUseBlock>();
var textOutput = new List<string>();

foreach (var block in response.Content)
                {
if (block.TryPickText(outvar text))
                        textOutput.Add(text.Text);

if (block.TryPickThinking(outvar thinking))
                    {
// 记录思考内容(可选显示)
                        UnityEngine.Debug.Log($"[AI Thinking] {thinking.Thinking}");
                    }

if (block.TryPickToolUse(outvar toolUse))
                        toolCalls.Add(toolUse);
                }

// 3. 如果没有工具调用,任务完成
if (response.StopReason != StopReason.ToolUse)
                {
// 将助手响应添加到历史
var assistantContent = ConvertContentBlocks(response.Content);
                    _conversationHistory.Add(new MessageParam
                    {
                        Role = Role.Assistant,
                        Content = assistantContent
                    });

return assistantContent;
                }

// 4. 执行每个工具并收集结果
var toolResults = new List<ToolResultBlockParam>();
foreach (var toolCall in toolCalls)
                {
var output = await _skillAgent.ExecuteToolAsync(toolCall.Name, toolCall.Input);
                    toolResults.Add(new ToolResultBlockParam
                    {
                        ToolUseID = toolCall.ID,
                        Content = output
                    });
                }

// 5. 将助手消息和工具结果添加到历史,继续循环
var assistantBlocks = ConvertContentBlocks(response.Content);
                _conversationHistory.Add(new MessageParam
                {
                    Role = Role.Assistant,
                    Content = assistantBlocks
                });

                _conversationHistory.Add(new MessageParam
                {
                    Role = Role.User,
                    Content = toolResults.Select(r => (ContentBlockParam)r).ToList()
                });
            }
        }

privatestringExtractTextResponse(List<ContentBlockParam> contentBlocks)
        {
var textResponse = new System.Text.StringBuilder();

foreach (var block in contentBlocks)
            {
if (block.TryPickText(outvar textBlock))
                {
                    textResponse.AppendLine(textBlock.Text);
                }
// 可选:显示思考内容(DeepSeek Reasoner)
// else if (block.TryPickThinking(out var thinkingBlock))
// {
//     textResponse.AppendLine($"[Thinking: {thinkingBlock.Thinking}]");
// }
            }

return textResponse.ToString().Trim();
        }

private List<ContentBlockParam> ConvertContentBlocks(IEnumerable<ContentBlock> contentBlocks)
        {
var result = new List<ContentBlockParam>();

foreach (var block in contentBlocks)
            {
if (block.TryPickText(outvar textBlock))
                {
                    result.Add(new TextBlockParam { Text = textBlock.Text });
                }
elseif (block.TryPickThinking(outvar thinkingBlock))
                {
// 处理 thinking 块(DeepSeek Reasoner 需要)
                    result.Add(new ThinkingBlockParam
                    {
                        Signature = thinkingBlock.Signature,
                        Thinking = thinkingBlock.Thinking
                    });
                }
elseif (block.TryPickToolUse(outvar toolUseBlock))
                {
                    result.Add(new ToolUseBlockParam
                    {
                        ID = toolUseBlock.ID,
                        Name = toolUseBlock.Name,
                        Input = toolUseBlock.Input
                    });
                }
            }

return result;
        }

publicvoidSetSystemPrompt(string systemPrompt)
        {
// 注意:系统提示在每次API调用时传递,这里只是存储
// 实际使用时需要在RunAgentLoopAsync中传递
        }

publicvoidSetSkillType(ToolManager.SkillType skillType)
        {
// 技能类型影响可用工具,需要在下次循环中生效
            _skillType = skillType;
        }
    }
}

Bash Agent

Bash 就是一切 – 最简 Agent 的哲学。

构建了各种复杂的 Agent 系统后,我们追问一个根本问题:Agent 的本质是什么?

答案令人惊讶地简单:一个工具 + 一个循环 = 完整的 Agent 能力

Unix 哲学说”一切皆文件,一切皆可管道”。Bash 是通往这个世界的大门,让我们来实现一个 Bash Agent 吧!

BashAgent.cs

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Linq;
using System.Threading;

namespaceClaudeCodeForUnity
{
publicclassBashAgent
    {
privatereadonlystring _workingDirectory;
privatereadonlyint _timeoutSeconds;

publicBashAgent(string workingDirectory, int timeoutSeconds = 60)
        {
            _workingDirectory = workingDirectory;
            _timeoutSeconds = timeoutSeconds;
        }

privateasync Task WaitForProcessExitAsync(Process process, CancellationToken cancellationToken)
        {
while (!process.HasExited && !cancellationToken.IsCancellationRequested)
            {
await Task.Delay(100, cancellationToken);
            }

if (!process.HasExited)
            {
                process.Kill();
            }
        }

publicasync Task<stringExecuteAsync(string command)
        {
// 安全检查 - 阻止危险命令
string[] dangerousCommands = { "rm -rf /""sudo""shutdown""reboot""> /dev/" };
if (dangerousCommands.Any(d => command.Contains(d)))
return"Error: 危险命令已阻止";

try
            {
usingvar process = new Process
                {
                    StartInfo = new ProcessStartInfo
                    {
                        FileName = "bash",
                        Arguments = $"-c \"{command.Replace("\"""\\\"")}\"",
                        WorkingDirectory = _workingDirectory,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                        UseShellExecute = false,
                        CreateNoWindow = true
                    }
                };

                process.Start();
var stdout = await process.StandardOutput.ReadToEndAsync();
var stderr = await process.StandardError.ReadToEndAsync();

usingvar cts = new CancellationTokenSource(TimeSpan.FromSeconds(_timeoutSeconds));
await WaitForProcessExitAsync(process, cts.Token);

var output = (stdout + stderr).Trim();
returnstring.IsNullOrEmpty(output) ? "(no output)" : SafeSubstring(output, 50000);
            }
catch (OperationCanceledException)
            {
return$"Error: 命令超时 ({_timeoutSeconds}s)";
            }
catch (Exception ex)
            {
return$"Error: {ex.Message}";
            }
        }

publicstringExecute(string command)
        {
// 安全检查
string[] dangerousCommands = { "rm -rf /""sudo""shutdown""reboot""> /dev/" };
if (dangerousCommands.Any(d => command.Contains(d)))
return"Error: 危险命令已阻止";

try
            {
usingvar process = new Process
                {
                    StartInfo = new ProcessStartInfo
                    {
                        FileName = "bash",
                        Arguments = $"-c \"{command.Replace("\"""\\\"")}\"",
                        WorkingDirectory = _workingDirectory,
                        RedirectStandardOutput = true,
                        RedirectStandardError = true,
                        UseShellExecute = false,
                        CreateNoWindow = true
                    }
                };

                process.Start();
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();

// 设置超时
if (!process.WaitForExit(_timeoutSeconds * 1000))
                {
                    process.Kill();
return"Error: 命令超时";
                }

var output = (stdout + stderr).Trim();
returnstring.IsNullOrEmpty(output) ? "(no output)" : SafeSubstring(output, 50000);
            }
catch (Exception ex)
            {
return$"Error: {ex.Message}";
            }
        }

privatestringSafeSubstring(string text, int maxLength)
        {
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
return text;

return text.Substring(0, maxLength);
        }
    }
}

File Agent

模型即代理 – 4 个工具覆盖 90% 场景。

工具
用途
覆盖场景
bash
运行任何命令
git, npm, dotnet, curl…
read_file
读取文件内容
理解代码
write_file
创建/覆盖文件
生成新文件
edit_file
精确文本替换
修改现有代码

因此产生了 File Agent,实现一些文件读写操作。

FileAgent.cs

using System;
using System.IO;
using System.Linq;

namespaceClaudeCodeForUnity
{
publicclassFileAgent
    {
privatereadonlystring _workingDirectory;

publicFileAgent(string workingDirectory)
        {
            _workingDirectory = workingDirectory;
        }

publicstringReadFile(string relativePath, int? limit = null)
        {
try
            {
var fullPath = GetSafePath(relativePath);
var lines = File.ReadAllLines(fullPath);

if (limit.HasValue && limit.Value < lines.Length)
                {
                    lines = lines.Take(limit.Value)
                        .Concat(new[] { $"... ({lines.Length - limit.Value} more lines)" })
                        .ToArray();
                }

var result = string.Join("\n", lines);
return SafeSubstring(result, 50000);
            }
catch (Exception ex)
            {
return$"Error: {ex.Message}";
            }
        }

publicstringWriteFile(string relativePath, string content)
        {
try
            {
var fullPath = GetSafePath(relativePath);
var dir = Path.GetDirectoryName(fullPath);

if (!string.IsNullOrEmpty(dir))
                    Directory.CreateDirectory(dir);

                File.WriteAllText(fullPath, content);
return$"Wrote {content.Length} bytes to {relativePath}";
            }
catch (Exception ex)
            {
return$"Error: {ex.Message}";
            }
        }

publicstringEditFile(string relativePath, string oldText, string newText)
        {
try
            {
var fullPath = GetSafePath(relativePath);
var fileContent = File.ReadAllText(fullPath);

if (!fileContent.Contains(oldText))
return$"Error: 在 {relativePath} 中未找到文本";

// 只替换第一次出现,保证安全
var index = fileContent.IndexOf(oldText);
if (index == -1)
return$"Error: 在 {relativePath} 中未找到文本";

var newContent = fileContent.Substring(0, index) +
                                newText +
                                fileContent.Substring(index + oldText.Length);

                File.WriteAllText(fullPath, newContent);
return$"Edited {relativePath}";
            }
catch (Exception ex)
            {
return$"Error: {ex.Message}";
            }
        }

publicboolFileExists(string relativePath)
        {
try
            {
var fullPath = GetSafePath(relativePath);
return File.Exists(fullPath);
            }
catch
            {
returnfalse;
            }
        }

///<exception cref="InvalidOperationException">路径逃逸工作目录</exception>
privatestringGetSafePath(string relativePath)
        {
var fullPath = Path.GetFullPath(Path.Combine(_workingDirectory, relativePath));
if (!fullPath.StartsWith(_workingDirectory))
thrownew InvalidOperationException($"路径逃逸工作区: {relativePath}");
return fullPath;
        }

privatestringSafeSubstring(string text, int maxLength)
        {
if (string.IsNullOrEmpty(text) || text.Length <= maxLength)
return text;

return text.Substring(0, maxLength);
        }
    }
}

Todo Manager

当下的 AI 编辑器,都提供了规划计划的能力,OK~ 接下来我们也要实现此功能了。

TodoManager.cs

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;

namespaceClaudeCodeForUnity
{
publicclassTodoManager
    {
private List<TodoItem> _items = new List<TodoItem>();

publicevent System.Action OnTodoListUpdated;

publicstringUpdate(JsonElement itemsJson)
        {
var validated = new List<TodoItem>();
var inProgressCount = 0;

foreach (var item in itemsJson.EnumerateArray())
            {
var content = item.TryGetProperty("content"outvar c) ? c.GetString()?.Trim() ?? "" : "";
var status = item.TryGetProperty("status"outvar s) ? s.GetString()?.ToLower() ?? "pending" : "pending";
var activeForm = item.TryGetProperty("activeForm"outvar a) ? a.GetString()?.Trim() ?? "" : "";

if (string.IsNullOrEmpty(content))
thrownew System.InvalidOperationException($"Item: content required");
if (status isnot ("pending"or"in_progress"or"completed"))
thrownew System.InvalidOperationException($"Item: invalid status '{status}'")
;

// 如果 activeForm 为空,使用 content 作为默认值
if (string.IsNullOrEmpty(activeForm))
                    activeForm = content;

if (status == "in_progress")
                    inProgressCount++;

                validated.Add(new TodoItem(content, status, activeForm));
            }

if (validated.Count > 20)
thrownew System.InvalidOperationException("Max 20 todos allowed");
if (inProgressCount > 1)
thrownew System.InvalidOperationException("Only one task can be in_progress at a time");

            _items = validated;

// 触发更新事件
            OnTodoListUpdated?.Invoke();

return Render();
        }

publicstringRender()
        {
if (_items.Count == 0)
return"No todos.";

var sb = new StringBuilder();
foreach (var item in _items)
            {
var mark = item.Status switch
                {
"completed" => "[x]",
"in_progress" => "[>]",
                    _ => "[ ]"
                };

if (item.Status == "in_progress")
                    sb.AppendLine($"{mark}{item.Content} <- {item.ActiveForm}");
else
                    sb.AppendLine($"{mark}{item.Content}");
            }

var completed = _items.Count(t => t.Status == "completed");
            sb.AppendLine($"\n({completed}/{_items.Count} completed)");
return sb.ToString();
        }

publicvoidClear()
        {
            _items.Clear();
            OnTodoListUpdated?.Invoke();
        }

public IReadOnlyList<TodoItem> Items => _items;
    }

publicclassTodoItem
    {
publicstring Content { get; }

publicstring Status { get; }

publicstring ActiveForm { get; }

publicTodoItem(string content, string status, string activeForm)
        {
            Content = content;
            Status = status;
            ActiveForm = activeForm;
        }
    }
}

Sub Agent

有了计划功能的加入,我们要设计对应的三个子代理模式了,它们分其责,

类型
名称
工具
用途
explore
只读代理
bash, read_file
安全探索,不会意外修改
code
完整代理
全部
实际实现
plan
规划代理
bash, read_file
设计方案,不执行

SubAgentManager.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;

namespaceClaudeCodeForUnity
{
publicclassSubAgentManager
    {
privatereadonlystring _workingDirectory;
privatereadonly AnthropicClient _client;
privatereadonlystring _model;

publicclassSubAgentConfig
        {
publicstring Name { getset; }

publicstring Description { getset; }

publicstring[] AllowedTools { getset; }

publicstring SystemPrompt { getset; }

publicSubAgentConfig(string name, string description, string[] allowedTools, string systemPrompt)
            {
                Name = name;
                Description = description;
                AllowedTools = allowedTools;
                SystemPrompt = systemPrompt;
            }
        }

privatereadonly Dictionary<string, SubAgentConfig> _agentConfigs = new()
        {
            ["explore"] = new SubAgentConfig(
"explore",
"只读代理,用于探索代码、查找文件、搜索",
new[] { "bash""read_file" },
"你是一个探索代理。搜索和分析,但不要修改文件。返回简洁的摘要。"
            ),
            ["code"] = new SubAgentConfig(
"code",
"完整代理,用于实现功能和修复bug",
new[] { "*" }, // 所有工具
"你是一个编程代理。高效地实现请求的更改。"
            ),
            ["plan"] = new SubAgentConfig(
"plan",
"规划代理,用于设计实现策略",
new[] { "bash""read_file" },
"你是一个规划代理。分析代码库并输出编号的实现计划。不要做更改。"
            )
        };

publicSubAgentManager(string workingDirectory, AnthropicClient client, string model)
        {
            _workingDirectory = workingDirectory;
            _client = client;
            _model = model;
        }

publicasync Task<stringExecuteSubAgentAsync(string description, string prompt, string agentType,
            List<Tool> availableTools
)

        {
if (!_agentConfigs.TryGetValue(agentType, outvar config))
return$"Error: 未知agent类型 '{agentType}'";

// 过滤工具列表
var filteredTools = FilterTools(availableTools, config.AllowedTools);

// 创建子代理系统提示
var subSystemPrompt = $@"你是一个位于 {_workingDirectory} 的 {agentType} 子代理。

{config.SystemPrompt}

完成任务并返回清晰简洁的摘要。"
;

// 创建独立的对话历史
var subHistory = new List<MessageParam>
            {
new MessageParam { Role = Role.User, Content = prompt }
            };

var startTime = DateTime.Now;
var toolCount = 0;

// 运行子代理循环
while (true)
            {
var response = await _client.Messages.Create(new MessageCreateParams
                {
                    Model = _model,
                    Messages = subHistory,
                    System = subSystemPrompt,
                    Tools = filteredTools.Select(t => (ToolUnion)t).ToList(),
                    MaxTokens = 4096
                });

// 如果没有工具调用,返回最终文本
if (response.StopReason != StopReason.ToolUse)
                {
var elapsed = (DateTime.Now - startTime).TotalSeconds;
return FormatResult(response, toolCount, elapsed);
                }

// 执行工具调用
var toolCalls = response.Content
                    .Where(c => c.TryPickToolUse(out _))
                    .Select(c => { c.TryPickToolUse(outvar tu); return tu!; })
                    .ToList();

var toolResults = new List<ToolResultBlockParam>();

foreach (var toolCall in toolCalls)
                {
                    toolCount++;
// 注意:这里需要实际的工具执行逻辑
// 由于工具执行依赖于主代理的ToolManager,这里返回模拟结果
var output = $"[子代理工具执行: {toolCall.Name}]";
                    toolResults.Add(new ToolResultBlockParam
                    {
                        ToolUseID = toolCall.ID,
                        Content = output
                    });
                }

// 更新对话历史
var assistantBlocks = response.Content.Select<ContentBlock, ContentBlockParam>(c =>
                {
if (c.TryPickText(outvar t)) returnnew TextBlockParam { Text = t.Text };
if (c.TryPickThinking(outvar thinking)) returnnew ThinkingBlockParam
                    {
                        Signature = thinking.Signature,
                        Thinking = thinking.Thinking
                    };
if (c.TryPickToolUse(outvar tu)) returnnew ToolUseBlockParam
                    {
                        ID = tu.ID,
                        Name = tu.Name,
                        Input = tu.Input
                    };
thrownew InvalidOperationException("Unknown content block type");
                }).ToList();

                subHistory.Add(new MessageParam { Role = Role.Assistant, Content = assistantBlocks });
                subHistory.Add(new MessageParam
                {
                    Role = Role.User,
                    Content = toolResults.Select(r => (ContentBlockParam)r).ToList()
                });
            }
        }

private List<Tool> FilterTools(List<Tool> allTools, string[] allowedTools)
        {
if (allowedTools.Contains("*"))
return allTools;

return allTools.Where(t => allowedTools.Contains(t.Name)).ToList();
        }

privatestringFormatResult(Message response, int toolCount, double elapsedSeconds)
        {
var textBlocks = response.Content
                .Where(c => c.TryPickText(out _))
                .Select(c => { c.TryPickText(outvar t); return t!.Text; })
                .ToList();

var result = string.Join("", textBlocks);
return$"[子代理完成] 工具调用: {toolCount}次, 耗时: {elapsedSeconds:F1}秒\n\n{result}";
        }

public Dictionary<stringstringGetAgentDescriptions()
        {
return _agentConfigs.ToDictionary(
                kv => kv.Key,
                kv => kv.Value.Description
            );
        }

publicboolAgentTypeExists(string agentType)
        {
return _agentConfigs.ContainsKey(agentType);
        }

publicvoidAddAgentConfig(string name, string description, string[] allowedTools, string systemPrompt)
        {
            _agentConfigs[name] = new SubAgentConfig(name, description, allowedTools, systemPrompt);
        }
    }
}

Skill Agent

Agent Skills 确实是最近AI领域最火爆的概念之一。也是 OpenClaw 的“灵魂”与“工具箱”。

工具是能力,技能是知识。Skills 机制便是知识外部化。

SKILL.md 标准

skills/
├── pdf/
│   └── SKILL.md          # 必需:YAML frontmatter + Markdown
├── mcp-builder/
│   ├── SKILL.md
│   └── references/       # 可选:参考文档
└── code-review/
    ├── SKILL.md
    └── scripts/          # 可选:辅助脚本

SKILL.md 格式

---
name: my-skill
description: 简短描述,用于触发判断。
---

# My Skill

## 什么时候使用

当用户需要...

## 如何使用

### 模式 1: ...

```bash
example command

具体细节这里就不做讲解,网上很多现成的 Skill ,直接拿来主义即可。

SkillLoader.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespaceClaudeCodeForUnity
{
publicclassSkillLoader
    {
publicclassSkill
        {
publicstring Name { get; }
publicstring Description { get; }
publicstring Body { get; }
publicstring Path { get; }
publicstring Directory { get; }

publicSkill(string name, string description, string body, string path, string directory)
            {
                Name = name;
                Description = description;
                Body = body;
                Path = path;
                Directory = directory;
            }
        }

privatereadonlystring _skillsDirectory;
privatereadonly Dictionary<string, Skill> _skills = new Dictionary<string, Skill>();

publicSkillLoader(string skillsDirectory)
        {
            _skillsDirectory = skillsDirectory;
            LoadSkills();
        }

private Skill ParseSkillMd(string path)
        {
var content = File.ReadAllText(path);

// 匹配 YAML frontmatter: ---\n...\n---\n
var match = Regex.Match(content, @"^---\s*\n(.*?)\n---\s*\n(.*)$",
                RegexOptions.Singleline);

if (!match.Success)
            {
                UnityEngine.Debug.LogWarning($"[SkillLoader] Invalid SKILL.md format: {path}");
returnnull;
            }

var frontmatter = match.Groups[1].Value;
var body = match.Groups[2].Value.Trim();

// 解析 YAML frontmatter
var metadata = new Dictionary<stringstring>();
foreach (var line in frontmatter.Split('\n'))
            {
var colonIndex = line.IndexOf(':');
if (colonIndex > 0)
                {
var key = line.Substring(0, colonIndex).Trim();
varvalue = line.Substring(colonIndex + 1).Trim().Trim('"''\'');
                    metadata[key] = value;
                }
            }

if (!metadata.TryGetValue("name"outvar name) ||
                !metadata.TryGetValue("description"outvar description))
            {
                UnityEngine.Debug.LogWarning($"[SkillLoader] Missing name or description in: {path}");
returnnull;
            }

var directory = System.IO.Path.GetDirectoryName(path);
returnnew Skill(name, description, body, path, directory);
        }

privatevoidLoadSkills()
        {
if (!System.IO.Directory.Exists(_skillsDirectory))
            {
                UnityEngine.Debug.LogWarning($"[SkillLoader] Skills directory not found: {_skillsDirectory}");
return;
            }

var skillDirs = System.IO.Directory.GetDirectories(_skillsDirectory);
foreach (var skillDir in skillDirs)
            {
var skillMdPath = System.IO.Path.Combine(skillDir, "SKILL.md");
if (!File.Exists(skillMdPath))
continue;

var skill = ParseSkillMd(skillMdPath);
if (skill != null)
                {
                    _skills[skill.Name] = skill;
                    UnityEngine.Debug.Log($"[SkillLoader] Loaded skill: {skill.Name} - {skill.Description}");
                }
            }

            UnityEngine.Debug.Log($"[SkillLoader] Total skills loaded: {_skills.Count}");
        }

publicstringGetDescriptions()
        {
if (_skills.Count == 0)
return"(no skills available)";

var descriptions = _skills.Select(kv => $"- {kv.Key}{kv.Value.Description}");
returnstring.Join("\n", descriptions);
        }

publicstringGetSkillContent(string name)
        {
if (!_skills.TryGetValue(name, outvar skill))
returnnull;

var content = $"# Skill: {skill.Name}\n\n{skill.Body}";

// 检查可用资源
var resources = new List<string>();
var resourceFolders = new[]
            {
                ("scripts""Scripts"),
                ("references""References"),
                ("assets""Assets")
            };

foreach (var (folder, label) in resourceFolders)
            {
var folderPath = System.IO.Path.Combine(skill.Directory, folder);
if (System.IO.Directory.Exists(folderPath))
                {
var files = System.IO.Directory.GetFiles(folderPath)
                        .Select(System.IO.Path.GetFileName);

if (files.Any())
                        resources.Add($"{label}{string.Join(", ", files)}");
                }
            }

// 添加资源信息
if (resources.Count > 0)
            {
                content += $"\n\n**Available resources in {skill.Directory}:**\n";
                content += string.Join("\n", resources.Select(r => $"- {r}"));
            }

return content;
        }

public IEnumerable<stringListSkills()
        {
return _skills.Keys;
        }

publicint SkillCount => _skills.Count;

publicboolHasSkill(string name)
        {
return _skills.ContainsKey(name);
        }
    }
}

SkillAgent.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespaceClaudeCodeForUnity
{
publicclassSkillAgent
    {
privatereadonly SkillLoader _skillLoader;

publicSkillAgent(SkillLoader skillLoader)
        {
            _skillLoader = skillLoader ?? thrownew ArgumentNullException(nameof(skillLoader));
        }

publicstringGetSkillDescriptions()
        {
return _skillLoader.GetDescriptions();
        }

public IEnumerable<stringListSkills()
        {
return _skillLoader.ListSkills();
        }

publicboolHasSkill(string skillName)
        {
return _skillLoader.HasSkill(skillName);
        }

publicstringLoadSkill(string skillName)
        {
if (string.IsNullOrEmpty(skillName))
            {
return"Error: skill name cannot be empty";
            }

var content = _skillLoader.GetSkillContent(skillName);

if (content == null)
            {
var available = string.Join(", ", ListSkills());
return$"Error: Unknown skill '{skillName}'. Available: {(string.IsNullOrEmpty(available) ? "none" : available)}";
            }

// 关键机制:以 tool_result 形式注入,不修改系统提示(保持 prompt cache)
return$@"<skill-loaded name=""{skillName}"">
{content}
</skill-loaded>

按照上面技能中的指令完成用户的任务。"
;
        }

publicint SkillCount => _skillLoader.SkillCount;

public SkillLoader SkillLoader => _skillLoader;
    }
}

Task Manager

任务管理实际要解决的是上下文压缩和记忆功能。

上下文窗口有限,但智能体会话可以无限。

三层压缩在不同粒度上解决这个问题:

  • • MicroCompact(替换旧工具输出)- 静默替换
  • • AutoCompact(接近限制时 LLM 摘要)- 阈值触发
  • • Manual Compact(用户触发)- 手动触发

分层方法让每一层在各自的粒度上独立运作,从静默的逐轮清理到完整的对话重置。

有了压缩功能,要面对另一个问题,当对话被压缩时,所有的任务状态都丢失了。

这里使用文件系统是最简单的持久化方案:无需数据库,无需序列化框架,JSON + 文件目录就是一个功能完整的任务看板。

TaskManager.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;

namespaceClaudeCodeForUnity
{
publicclassTaskManager
    {
privatereadonly AnthropicClient _client;
privatereadonlystring _model;
privatereadonly TodoManager _todoManager;
privatereadonlystring _workDir;
privatereadonlystring _transcriptDir;
privatereadonlystring _tasksDir;

// 压缩参数
privateconstint TOKEN_THRESHOLD = 50000;  // Token 阈值
privateconstint KEEP_RECENT = 3;          // 保留最近 N 个工具结果

// 工具结果追踪器: 记录哪些消息包含 tool_result 及其 ID 和工具名
privatereadonly List<(int msgIdx, List<(string toolUseId, string toolName)> tools)> _toolResultTracker
            = new List<(int, List<(stringstring)>)>();

// 任务 ID 计数器
privateint _nextTaskId;

publicevent Action<stringstring> OnCompactCompleted;

publicevent Action<int> OnTokenEstimateUpdated;

publicevent Action<intstring> OnTaskCreated;

publicevent Action<intstring> OnTaskUpdated;

publicTaskManager(AnthropicClient client, string model, TodoManager todoManager, string workDir)
        {
            _client = client ?? thrownew ArgumentNullException(nameof(client));
            _model = model ?? thrownew ArgumentNullException(nameof(model));
            _todoManager = todoManager ?? thrownew ArgumentNullException(nameof(todoManager));
            _workDir = workDir ?? Directory.GetCurrentDirectory();
            _transcriptDir = Path.Combine(_workDir, ".transcripts");
            _tasksDir = Path.Combine(_workDir, ".tasks");

// 初始化任务系统
            Directory.CreateDirectory(_tasksDir);
            _nextTaskId = GetMaxTaskId() + 1;

            UnityEngine.Debug.Log($"[TaskManager] Initialized with tasks directory: {_tasksDir}");
            UnityEngine.Debug.Log($"[TaskManager] Next task ID: {_nextTaskId}");
        }

privatestatic JsonElement SerializeToElement<T>(value)
        {
var json = JsonSerializer.Serialize(value);
usingvar doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
        }

publicintEstimateTokens(List<MessageParam> messages)
        {
var json = JsonSerializer.Serialize(messages);
var estimate = json.Length / 4;
            OnTokenEstimateUpdated?.Invoke(estimate);
return estimate;
        }

publicvoidMicroCompact(List<MessageParam> messages)
        {
if (_toolResultTracker.Count <= KEEP_RECENT)
return;

var toClear = _toolResultTracker.Take(_toolResultTracker.Count - KEEP_RECENT).ToList();
foreach (var (msgIdx, tools) in toClear)
            {
if (msgIdx >= messages.Count)
continue;

// 用包含占位符内容的新 ToolResultBlockParam 替换整条消息
var newBlocks = tools.Select(t => (ContentBlockParam)new ToolResultBlockParam
                {
                    ToolUseID = t.toolUseId,
                    Content = $"[Previous: used {t.toolName}]"
                }).ToList();

                messages[msgIdx] = new MessageParam
                {
                    Role = Role.User,
                    Content = newBlocks
                };
            }

            UnityEngine.Debug.Log($"[MicroCompact] Compressed {toClear.Count} old tool results");
        }

publicasync Task<stringAutoCompactAsync(List<MessageParam> messages)
        {
// 1. 保存完整记录到磁盘
            Directory.CreateDirectory(_transcriptDir);
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var transcriptPath = Path.Combine(_transcriptDir, $"transcript_{timestamp}.jsonl");

var lines = messages.Select(m => JsonSerializer.Serialize(m));
await File.WriteAllLinesAsync(transcriptPath, lines);
            UnityEngine.Debug.Log($"[AutoCompact] Transcript saved: {transcriptPath}");

// 2. 让 LLM 总结对话
var conversationText = JsonSerializer.Serialize(messages);
if (conversationText.Length > 80000)
                conversationText = conversationText.Substring(080000);

var summaryResponse = await _client.Messages.Create(new MessageCreateParams
            {
                Model = _model,
                Messages = new List<MessageParam>
                {
new MessageParam
                    {
                        Role = Role.User,
                        Content = "Summarize this conversation for continuity. Include: " +
"1) What was accomplished, 2) Current state, 3) Key decisions made. " +
"Be concise but preserve critical details.\n\n" + conversationText
                    }
                },
                MaxTokens = 2000
            });

var summary = string.Join("", summaryResponse.Content
                .Where(c => c.TryPickText(out _))
                .Select(c => { c.TryPickText(outvar t); return t!.Text; }));

// 3. 替换所有消息为压缩摘要
            messages.Clear();
            _toolResultTracker.Clear();

            messages.Add(new MessageParam
            {
                Role = Role.User,
                Content = $"[Conversation compressed. Transcript: {transcriptPath}]\n\n{summary}"
            });
            messages.Add(new MessageParam
            {
                Role = Role.Assistant,
                Content = "Understood. I have the context from the summary. Continuing."
            });

// 4. 清除 TodoManager 的任务列表 - 这是关键的记忆清除
            _todoManager.Clear();
            UnityEngine.Debug.Log("[AutoCompact] TodoManager cleared - memory reset");

// 5. 触发压缩完成事件
            OnCompactCompleted?.Invoke(transcriptPath, summary);

return summary;
        }

publicasync Task<stringManualCompactAsync(List<MessageParam> messages, string focus = "")
        {
            UnityEngine.Debug.Log($"[ManualCompact] Triggered{(string.IsNullOrEmpty(focus) ? "" : $" with focus: {focus}")}");
returnawait AutoCompactAsync(messages);
        }

publicboolShouldAutoCompact(List<MessageParam> messages)
        {
var tokens = EstimateTokens(messages);
return tokens > TOKEN_THRESHOLD;
        }

publicvoidTrackToolResult(int msgIdx, List<(string toolUseId, string toolName)> toolInfos)
        {
            _toolResultTracker.Add((msgIdx, toolInfos));
        }

publicint TrackedResultCount => _toolResultTracker.Count;

publicvoidClearTrackers()
        {
            _toolResultTracker.Clear();
        }

publicstring[] GetTranscripts()
        {
if (!Directory.Exists(_transcriptDir))
return Array.Empty<string>();

return Directory.GetFiles(_transcriptDir, "transcript_*.jsonl")
                .OrderByDescending(f => f)
                .ToArray();
        }

publicintCleanupOldTranscripts(int keepCount = 10)
        {
var transcripts = GetTranscripts();
if (transcripts.Length <= keepCount)
return0;

var toDelete = transcripts.Skip(keepCount).ToArray();
foreach (varfilein toDelete)
            {
try
                {
                    File.Delete(file);
                }
catch (Exception ex)
                {
                    UnityEngine.Debug.LogWarning($"Failed to delete transcript {file}{ex.Message}");
                }
            }

            UnityEngine.Debug.Log($"[CleanupTranscripts] Deleted {toDelete.Length} old transcripts");
return toDelete.Length;
        }

#region v6 持久化任务系统

privateintGetMaxTaskId()
        {
var files = Directory.GetFiles(_tasksDir, "task_*.json");
if (files.Length == 0)
return0;

return files
                .Select(f =>
                {
var fileName = Path.GetFileNameWithoutExtension(f);
var parts = fileName.Split('_');
return parts.Length >= 2 && int.TryParse(parts[1], outvar id) ? id : 0;
                })
                .DefaultIfEmpty(0)
                .Max();
        }

private Dictionary<string, JsonElement> LoadTask(int taskId)
        {
var path = Path.Combine(_tasksDir, $"task_{taskId}.json");
if (!File.Exists(path))
thrownew InvalidOperationException($"Task {taskId} not found");

var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
        }

privatevoidSaveTask(Dictionary<string, JsonElement> task)
        {
var taskId = task["id"].GetInt32();
var path = Path.Combine(_tasksDir, $"task_{taskId}.json");
var json = JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
            File.WriteAllText(path, json);
        }

publicstringCreateTask(string subject, string description = ""int? sessionId = null)
        {
var task = new Dictionary<string, JsonElement>
            {
                ["id"] = SerializeToElement(_nextTaskId),
                ["subject"] = SerializeToElement(subject),
                ["description"] = SerializeToElement(description),
                ["status"] = SerializeToElement("pending"),
                ["blockedBy"] = SerializeToElement(new List<int>()),
                ["blocks"] = SerializeToElement(new List<int>()),
                ["owner"] = SerializeToElement(""),
                ["sessionId"] = SerializeToElement(sessionId ?? 0),
                ["createdAt"] = SerializeToElement(DateTimeOffset.UtcNow.ToUnixTimeSeconds())
            };

            SaveTask(task);
            UnityEngine.Debug.Log($"[TaskCreate] #{_nextTaskId}{subject} (Session: {sessionId ?? 0})");
            OnTaskCreated?.Invoke(_nextTaskId, subject);

            _nextTaskId++;

return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
        }

publicstringUpdateTask(int taskId, string status = null, List<int> addBlockedBy = null, List<int> addBlocks = null)
        {
var task = LoadTask(taskId);

// 更新状态
if (!string.IsNullOrEmpty(status))
            {
if (status != "pending" && status != "in_progress" && status != "completed")
thrownew InvalidOperationException($"Invalid status: {status}");

                task["status"] = SerializeToElement(status);
                UnityEngine.Debug.Log($"[TaskUpdate] #{taskId} -> {status}");

// 如果任务完成,清除依赖关系
if (status == "completed")
                    ClearDependency(taskId);

                OnTaskUpdated?.Invoke(taskId, status);
            }

// 添加 blockedBy 依赖
if (addBlockedBy != null && addBlockedBy.Count > 0)
            {
var current = GetIntList(task, "blockedBy");
                current.AddRange(addBlockedBy);
                task["blockedBy"] = SerializeToElement(current.Distinct().ToList());
            }

// 添加 blocks 依赖
if (addBlocks != null && addBlocks.Count > 0)
            {
var current = GetIntList(task, "blocks");
                current.AddRange(addBlocks);
                task["blocks"] = SerializeToElement(current.Distinct().ToList());

// 反向更新:将当前任务添加到被阻塞任务的 blockedBy 列表
foreach (var blockedId in addBlocks)
                {
try
                    {
var blocked = LoadTask(blockedId);
var blockedBy = GetIntList(blocked, "blockedBy");
if (!blockedBy.Contains(taskId))
                        {
                            blockedBy.Add(taskId);
                            blocked["blockedBy"] = SerializeToElement(blockedBy);
                            SaveTask(blocked);
                        }
                    }
catch (Exception ex)
                    {
                        UnityEngine.Debug.LogWarning($"Failed to update blocked task {blockedId}{ex.Message}");
                    }
                }
            }

            task["updatedAt"] = SerializeToElement(DateTimeOffset.UtcNow.ToUnixTimeSeconds());
            SaveTask(task);

return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
        }

privatevoidClearDependency(int completedId)
        {
var files = Directory.GetFiles(_tasksDir, "task_*.json");
foreach (varfilein files)
            {
try
                {
var json = File.ReadAllText(file);
var task = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
var blockedBy = GetIntList(task, "blockedBy");

if (blockedBy.Remove(completedId))
                    {
                        task["blockedBy"] = SerializeToElement(blockedBy);
                        SaveTask(task);
                        UnityEngine.Debug.Log($"[ClearDependency] Removed #{completedId} from task #{task["id"].GetInt32()}");
                    }
                }
catch (Exception ex)
                {
                    UnityEngine.Debug.LogWarning($"Failed to process task file {file}{ex.Message}");
                }
            }
        }

publicstringGetTask(int taskId)
        {
var task = LoadTask(taskId);
return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
        }

publicstringListAllTasks(int? sessionId = null)
        {
var files = Directory.GetFiles(_tasksDir, "task_*.json").OrderBy(f => f).ToArray();
if (files.Length == 0)
return"No tasks.";

var lines = new List<string>();
foreach (varfilein files)
            {
try
                {
var json = File.ReadAllText(file);
var task = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);

// 如果指定了会话 ID,则过滤任务
if (sessionId.HasValue)
                    {
var taskSessionId = task.TryGetValue("sessionId"outvar sid) ? sid.GetInt32() : 0;
if (taskSessionId != sessionId.Value)
continue;
                    }

var taskId = task["id"].GetInt32();
var subject = task["subject"].GetString();
var status = task["status"].GetString();
var blockedBy = GetIntList(task, "blockedBy");

var marker = status switch
                    {
"pending" => "[ ]",
"in_progress" => "[>]",
"completed" => "[x]",
                        _ => "[?]"
                    };

var blockedInfo = blockedBy.Count > 0
                        ? $" (blocked by: [{string.Join(", ", blockedBy)}])"
                        : "";

                    lines.Add($"{marker} #{taskId}{subject}{blockedInfo}");
                }
catch (Exception ex)
                {
                    UnityEngine.Debug.LogWarning($"Failed to read task file {file}{ex.Message}");
                }
            }

return lines.Count > 0 ? string.Join("\n", lines) : "No tasks.";
        }

privatestaticList<intGetIntList(Dictionary<string, JsonElement> dict, string key)
        {
if (!dict.TryGetValue(key, outvar element))
returnnew List<int>();

if (element.ValueKind == JsonValueKind.Array)
            {
return element.EnumerateArray()
                    .Where(e => e.ValueKind == JsonValueKind.Number)
                    .Select(e => e.GetInt32())
                    .ToList();
            }

returnnew List<int>();
        }

publicvoidDeleteTask(int taskId)
        {
var path = Path.Combine(_tasksDir, $"task_{taskId}.json");
if (File.Exists(path))
            {
                File.Delete(path);
                UnityEngine.Debug.Log($"[TaskDelete] Deleted task #{taskId}");
            }
        }

publicintCleanupCompletedTasks()
        {
var files = Directory.GetFiles(_tasksDir, "task_*.json");
var deletedCount = 0;

foreach (varfilein files)
            {
try
                {
var json = File.ReadAllText(file);
var task = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
if (task["status"].GetString() == "completed")
                    {
                        File.Delete(file);
                        deletedCount++;
                    }
                }
catch (Exception ex)
                {
                    UnityEngine.Debug.LogWarning($"Failed to process task file {file}{ex.Message}");
                }
            }

if (deletedCount > 0)
                UnityEngine.Debug.Log($"[CleanupTasks] Deleted {deletedCount} completed tasks");

return deletedCount;
        }

publicstringGetTaskStats()
        {
var files = Directory.GetFiles(_tasksDir, "task_*.json");
if (files.Length == 0)
return"No tasks.";

var pending = 0;
var inProgress = 0;
var completed = 0;

foreach (varfilein files)
            {
try
                {
var json = File.ReadAllText(file);
var task = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
var status = task["status"].GetString();

switch (status)
                    {
case"pending": pending++; break;
case"in_progress": inProgress++; break;
case"completed": completed++; break;
                    }
                }
catch { }
            }

return$"Tasks: {files.Length} total | {pending} pending | {inProgress} in progress | {completed} completed";
        }

#endregion
    }
}

Session Manager

有了任务后,这里我们添加会话管理,这样可以处理多个会话功能:

SessionManager.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Anthropic.Models.Messages;

namespaceClaudeCodeForUnity
{
publicclassSessionManager
    {
privatereadonlystring _sessionsDir;
privateint _nextSessionId;
private Session _currentSession;

publicevent Action<intstring> OnSessionCreated;

publicevent Action<intstring> OnSessionSwitched;

publicevent Action<int> OnSessionUpdated;

publicevent Action<intintstring> OnTaskCreated;

publicevent Action<intintstring> OnTaskUpdated;

publicSessionManager(string workDir)
        {
            _sessionsDir = Path.Combine(workDir, ".sessions");

            Directory.CreateDirectory(_sessionsDir);

            _nextSessionId = GetMaxSessionId() + 1;

// 加载或创建默认会话
var existingSessions = GetAllSessions();
if (existingSessions.Count > 0)
            {
// 加载最近使用的会话
                _currentSession = existingSessions.OrderByDescending(s => s.UpdatedAt).First();
                UnityEngine.Debug.Log($"[SessionManager] Loaded session #{_currentSession.Id}{_currentSession.Name}");
            }
else
            {
// 创建默认会话
                _currentSession = CreateSessionInternal("Default Session");
                UnityEngine.Debug.Log($"[SessionManager] Created default session #{_currentSession.Id}");
            }
        }

public Session CurrentSession => _currentSession;

public Session CreateSession(string name = null)
        {
if (string.IsNullOrWhiteSpace(name))
                name = $"Session {DateTime.Now:yyyy-MM-dd HH:mm}";

var session = CreateSessionInternal(name);
            OnSessionCreated?.Invoke(session.Id, session.Name);
return session;
        }

private Session CreateSessionInternal(string name)
        {
var session = new Session
            {
                Id = _nextSessionId++,
                Name = name,
                CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
            };

// 创建会话目录
var sessionDir = GetSessionDirectory(session.Id);
            Directory.CreateDirectory(sessionDir);

// 保存会话元数据
            SaveSession(session);

// 初始化空任务列表
var tasksPath = GetTasksPath(session.Id);
            File.WriteAllText(tasksPath, "[]");

// 初始化空历史文件
var historyPath = GetHistoryPath(session.Id);
            File.WriteAllText(historyPath, "");

            UnityEngine.Debug.Log($"[SessionManager] Session directory created: {sessionDir}");

return session;
        }

publicboolSwitchToSession(int sessionId)
        {
var session = LoadSession(sessionId);
if (session == null)
returnfalse;

            _currentSession = session;
            UnityEngine.Debug.Log($"[SessionManager] Switched to session #{sessionId}{session.Name}");
            OnSessionSwitched?.Invoke(sessionId, session.Name);
returntrue;
        }

publicvoidRenameCurrentSession(string newName)
        {
if (_currentSession == nullreturn;

            _currentSession.Name = newName;
            _currentSession.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            SaveSession(_currentSession);
            OnSessionUpdated?.Invoke(_currentSession.Id);
        }

publicboolDeleteSession(int sessionId)
        {
if (_currentSession != null && _currentSession.Id == sessionId)
            {
                UnityEngine.Debug.LogWarning($"[SessionManager] Cannot delete current session #{sessionId}");
returnfalse;
            }

var sessionDir = GetSessionDirectory(sessionId);

try
            {
// 删除整个会话目录(包含所有文件)
if (Directory.Exists(sessionDir))
                {
                    Directory.Delete(sessionDir, true); // recursive = true
                    UnityEngine.Debug.Log($"[SessionManager] Deleted session directory: {sessionDir}");
                }

returntrue;
            }
catch (Exception ex)
            {
                UnityEngine.Debug.LogError($"[SessionManager] Failed to delete session #{sessionId}{ex.Message}");
returnfalse;
            }
        }

publicvoidSaveConversationHistory(List<MessageParam> messages)
        {
if (_currentSession == nullreturn;

var historyPath = GetHistoryPath(_currentSession.Id);
var lines = messages.Select(m => JsonSerializer.Serialize(m));
            File.WriteAllLines(historyPath, lines);

            _currentSession.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            SaveSession(_currentSession);
        }

public List<MessageParam> LoadConversationHistory()
        {
if (_currentSession == null)
returnnew List<MessageParam>();

var historyPath = GetHistoryPath(_currentSession.Id);
if (!File.Exists(historyPath))
returnnew List<MessageParam>();

try
            {
var lines = File.ReadAllLines(historyPath);
return lines
                    .Where(line => !string.IsNullOrWhiteSpace(line))
                    .Select(line => JsonSerializer.Deserialize<MessageParam>(line))
                    .ToList();
            }
catch (Exception ex)
            {
                UnityEngine.Debug.LogError($"[SessionManager] Failed to load conversation history: {ex.Message}");
returnnew List<MessageParam>();
            }
        }

public List<Session> GetAllSessions()
        {
var sessions = new List<Session>();
var sessionDirs = Directory.GetDirectories(_sessionsDir, "session_*");

foreach (var sessionDir in sessionDirs)
            {
try
                {
var sessionFile = Path.Combine(sessionDir, "session.json");
if (File.Exists(sessionFile))
                    {
var json = File.ReadAllText(sessionFile);
var session = JsonSerializer.Deserialize<Session>(json);
if (session != null)
                            sessions.Add(session);
                    }
                }
catch (Exception ex)
                {
                    UnityEngine.Debug.LogWarning($"[SessionManager] Failed to load session from {sessionDir}{ex.Message}");
                }
            }

return sessions.OrderByDescending(s => s.UpdatedAt).ToList();
        }

publicstringGetCurrentSessionTaskSummary()
        {
if (_currentSession == null)
return"No active session";

var tasks = LoadCurrentSessionTasks();
return$"Session: {_currentSession.Name} | Tasks: {tasks.Count}";
        }

#region 私有方法

privateintGetMaxSessionId()
        {
var sessionDirs = Directory.GetDirectories(_sessionsDir, "session_*");
if (sessionDirs.Length == 0)
return0;

return sessionDirs
                .Select(dir =>
                {
var dirName = Path.GetFileName(dir);
var parts = dirName.Split('_');
return parts.Length >= 2 && int.TryParse(parts[1], outvar id) ? id : 0;
                })
                .DefaultIfEmpty(0)
                .Max();
        }

privatestringGetSessionDirectory(int sessionId)
        {
return Path.Combine(_sessionsDir, $"session_{sessionId}");
        }

privatestringGetSessionPath(int sessionId)
        {
return Path.Combine(GetSessionDirectory(sessionId), "session.json");
        }

privatestringGetHistoryPath(int sessionId)
        {
return Path.Combine(GetSessionDirectory(sessionId), "history.jsonl");
        }

privatestringGetTasksPath(int sessionId)
        {
return Path.Combine(GetSessionDirectory(sessionId), "tasks.json");
        }

privatevoidSaveSession(Session session)
        {
// 确保会话目录存在
var sessionDir = GetSessionDirectory(session.Id);
            Directory.CreateDirectory(sessionDir);

// 保存会话元数据
var path = GetSessionPath(session.Id);
var json = JsonSerializer.Serialize(session, new JsonSerializerOptions { WriteIndented = true });
            File.WriteAllText(path, json);
        }

private Session LoadSession(int sessionId)
        {
var path = GetSessionPath(sessionId);
if (!File.Exists(path))
returnnull;

try
            {
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<Session>(json);
            }
catch (Exception ex)
            {
                UnityEngine.Debug.LogError($"[SessionManager] Failed to load session #{sessionId}{ex.Message}");
returnnull;
            }
        }

#region 任务管理

private List<SessionTask> LoadCurrentSessionTasks()
        {
if (_currentSession == null)
returnnew List<SessionTask>();

var tasksPath = GetTasksPath(_currentSession.Id);
if (!File.Exists(tasksPath))
returnnew List<SessionTask>();

try
            {
var json = File.ReadAllText(tasksPath);
return JsonSerializer.Deserialize<List<SessionTask>>(json) ?? new List<SessionTask>();
            }
catch (Exception ex)
            {
                UnityEngine.Debug.LogError($"[SessionManager] Failed to load tasks: {ex.Message}");
returnnew List<SessionTask>();
            }
        }

privatevoidSaveCurrentSessionTasks(List<SessionTask> tasks)
        {
if (_currentSession == nullreturn;

var tasksPath = GetTasksPath(_currentSession.Id);
var json = JsonSerializer.Serialize(tasks, new JsonSerializerOptions { WriteIndented = true });
            File.WriteAllText(tasksPath, json);

            _currentSession.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            SaveSession(_currentSession);
        }

public SessionTask CreateTask(string subject, string description = "")
        {
if (_currentSession == null)
thrownew InvalidOperationException("No active session");

var tasks = LoadCurrentSessionTasks();
var nextId = tasks.Count > 0 ? tasks.Max(t => t.Id) + 1 : 1;

var task = new SessionTask
            {
                Id = nextId,
                Subject = subject,
                Description = description,
                Status = "pending",
                BlockedBy = new List<int>(),
                Blocks = new List<int>(),
                CreatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
            };

            tasks.Add(task);
            SaveCurrentSessionTasks(tasks);

            UnityEngine.Debug.Log($"[SessionManager] Task #{nextId} created in session #{_currentSession.Id}{subject}");
            OnTaskCreated?.Invoke(_currentSession.Id, nextId, subject);

return task;
        }

public SessionTask UpdateTask(int taskId, string status = null, List<int> addBlockedBy = null, List<int> addBlocks = null)
        {
if (_currentSession == null)
thrownew InvalidOperationException("No active session");

var tasks = LoadCurrentSessionTasks();
var task = tasks.FirstOrDefault(t => t.Id == taskId);
if (task == null)
thrownew InvalidOperationException($"Task #{taskId} not found in current session");

// 更新状态
if (!string.IsNullOrEmpty(status))
            {
if (status != "pending" && status != "in_progress" && status != "completed")
thrownew InvalidOperationException($"Invalid status: {status}");

                task.Status = status;
                UnityEngine.Debug.Log($"[SessionManager] Task #{taskId} -> {status}");

// 如果任务完成,清除依赖
if (status == "completed")
                {
foreach (var t in tasks)
                    {
                        t.BlockedBy.Remove(taskId);
                    }
                }

                OnTaskUpdated?.Invoke(_currentSession.Id, taskId, status);
            }

// 添加 blockedBy 依赖
if (addBlockedBy != null && addBlockedBy.Count > 0)
            {
foreach (var id in addBlockedBy)
                {
if (!task.BlockedBy.Contains(id))
                        task.BlockedBy.Add(id);
                }
            }

// 添加 blocks 依赖
if (addBlocks != null && addBlocks.Count > 0)
            {
foreach (var id in addBlocks)
                {
if (!task.Blocks.Contains(id))
                        task.Blocks.Add(id);

// 反向更新
var blockedTask = tasks.FirstOrDefault(t => t.Id == id);
if (blockedTask != null && !blockedTask.BlockedBy.Contains(taskId))
                    {
                        blockedTask.BlockedBy.Add(taskId);
                    }
                }
            }

            task.UpdatedAt = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
            SaveCurrentSessionTasks(tasks);

return task;
        }

public SessionTask GetTask(int taskId)
        {
if (_currentSession == null)
thrownew InvalidOperationException("No active session");

var tasks = LoadCurrentSessionTasks();
return tasks.FirstOrDefault(t => t.Id == taskId);
        }

public List<SessionTask> ListTasks()
        {
return LoadCurrentSessionTasks();
        }

publicboolDeleteTask(int taskId)
        {
if (_currentSession == null)
returnfalse;

var tasks = LoadCurrentSessionTasks();
var task = tasks.FirstOrDefault(t => t.Id == taskId);
if (task == null)
returnfalse;

            tasks.Remove(task);
            SaveCurrentSessionTasks(tasks);

            UnityEngine.Debug.Log($"[SessionManager] Task #{taskId} deleted");
returntrue;
        }

publicstringGetTaskStats()
        {
var tasks = LoadCurrentSessionTasks();
if (tasks.Count == 0)
return"No tasks";

var pending = tasks.Count(t => t.Status == "pending");
var inProgress = tasks.Count(t => t.Status == "in_progress");
var completed = tasks.Count(t => t.Status == "completed");

return$"Tasks: {tasks.Count} total | {pending} pending | {inProgress} in progress | {completed} completed";
        }

#endregion

#endregion
    }

publicclassSession
    {
publicint Id { getset; }

publicstring Name { getset; }

publiclong CreatedAt { getset; }

publiclong UpdatedAt { getset; }

publicstringGetFormattedCreatedAt()
        {
return DateTimeOffset.FromUnixTimeSeconds(CreatedAt).ToLocalTime().ToString("yyyy-MM-dd HH:mm");
        }

publicstringGetFormattedUpdatedAt()
        {
return DateTimeOffset.FromUnixTimeSeconds(UpdatedAt).ToLocalTime().ToString("yyyy-MM-dd HH:mm");
        }
    }

publicclassSessionTask
    {
publicint Id { getset; }

publicstring Subject { getset; }

publicstring Description { getset; }

publicstring Status { getset; }

public List<int> BlockedBy { getset; }

public List<int> Blocks { getset; }

publiclong CreatedAt { getset; }

publiclong UpdatedAt { getset; }
    }
}

Background Manager

后台任务 – 永不阻塞的智能体:在真实项目中,构建、测试、部署动辄数分钟。让智能体干等着是极大的浪费。

在独立线程中运行命令,在每次 LLM 调用前排空通知队列,使智能体永远不会因长时间运行的操作而阻塞。

BackgroundManager.cs

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespaceClaudeCodeForUnity
{
publicclassBackgroundManager
    {
publicclassBackgroundNotification
        {
publicstring TaskId { get; }
publicstring Status { get; }
publicstring Command { get; }
publicstring Result { get; }

publicBackgroundNotification(string taskId, string status, string command, string result)
            {
                TaskId = taskId;
                Status = status;
                Command = command;
                Result = result;
            }
        }

privateclassTaskInfo
        {
publicstring Status { get; }
publicstring Result { get; }
publicstring Command { get; }

publicTaskInfo(string status, string result, string command)
            {
                Status = status;
                Result = result;
                Command = command;
            }
        }

privatereadonly ConcurrentDictionary<string, TaskInfo> _tasks = new();
privatereadonly ConcurrentQueue<BackgroundNotification> _notificationQueue = new();
privatereadonlystring _workingDirectory;

publicBackgroundManager(string workingDirectory)
        {
            _workingDirectory = workingDirectory;
        }

publicstringRun(string command)
        {
var taskId = Guid.NewGuid().ToString().Substring(08);
            _tasks[taskId] = new TaskInfo("running"null, command);

var thread = new Thread(() => ExecuteTask(taskId, command))
            {
                IsBackground = true
            };
            thread.Start();

var preview = command.Length > 80 ? command.Substring(080) + "..." : command;
return$"Background task {taskId} started: {preview}";
        }

privatevoidExecuteTask(string taskId, string command)
        {
string output;
string status;

try
            {
usingvar proc = new Process();
                proc.StartInfo = new ProcessStartInfo
                {
                    FileName = "bash",
                    Arguments = $"-c \"{command.Replace("\"""\\\"")}\"",
                    WorkingDirectory = _workingDirectory,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true,
                    UseShellExecute = false,
                    CreateNoWindow = true
                };
                proc.Start();

// 5 分钟超时
if (!proc.WaitForExit(300_000))
                {
                    proc.Kill();
                    output = "Error: Timeout (300s)";
                    status = "timeout";
                }
else
                {
var stdout = proc.StandardOutput.ReadToEnd();
var stderr = proc.StandardError.ReadToEnd();
                    output = (stdout + stderr).Trim();
                    output = string.IsNullOrEmpty(output) ? "(no output)" : output;

// 限制输出大小
if (output.Length > 50000)
                        output = output.Substring(050000) + "\n... (output truncated)";

                    status = "completed";
                }
            }
catch (Exception ex)
            {
                output = $"Error: {ex.Message}";
                status = "error";
            }

// 更新任务状态
            _tasks[taskId] = new TaskInfo(status, output, command);

// 添加通知到队列
var commandPreview = command.Length > 80 ? command.Substring(080) + "..." : command;
var resultPreview = output.Length > 500 ? output.Substring(0500) + "..." : output;
            _notificationQueue.Enqueue(new BackgroundNotification(
                taskId,
                status,
                commandPreview,
                resultPreview
            ));

            UnityEngine.Debug.Log($"[BackgroundTask {taskId}{status}{commandPreview}");
        }

publicstringCheck(string taskId = null)
        {
if (taskId != null)
            {
if (!_tasks.TryGetValue(taskId, outvar task))
return$"Error: Unknown task {taskId}";

var commandPreview = task.Command.Length > 60
                    ? task.Command.Substring(060) + "..."
                    : task.Command;
var result = task.Result ?? "(running)";
return$"[{task.Status}{commandPreview}\n{result}";
            }

// 返回所有任务
if (_tasks.IsEmpty)
return"No background tasks.";

var taskList = _tasks.Select(kv =>
            {
var commandPreview = kv.Value.Command.Length > 60
                    ? kv.Value.Command.Substring(060) + "..."
                    : kv.Value.Command;
return$"{kv.Key}: [{kv.Value.Status}{commandPreview}";
            });

returnstring.Join("\n", taskList);
        }

public List<BackgroundNotification> DrainNotifications()
        {
var result = new List<BackgroundNotification>();
while (_notificationQueue.TryDequeue(outvar notification))
            {
                result.Add(notification);
            }
return result;
        }

publicintGetActiveTaskCount()
        {
return _tasks.Count(kv => kv.Value.Status == "running");
        }

publicvoidClearCompletedTasks()
        {
var completedTasks = _tasks.Where(kv => kv.Value.Status != "running")
                .Select(kv => kv.Key)
                .ToList();

foreach (var taskId in completedTasks)
            {
                _tasks.TryRemove(taskId, out _);
            }

            UnityEngine.Debug.Log($"[BackgroundManager] Cleared {completedTasks.Count} completed tasks");
        }
    }
}

Tool Manager

工具管理类,是对上述功能的整合。

TextManager.cs

using System.Collections.Generic;

namespaceClaudeCodeForUnity
{
publicclassTextManager
    {
publicenum Language
        {
            Chinese,
            English
        }

private Language _currentLanguage = Language.Chinese;
privatereadonly Dictionary<string, Dictionary<Language, string>> _texts;

publicTextManager()
        {
            _texts = new Dictionary<string, Dictionary<Language, string>>();
            InitializeTexts();
        }

publicvoidSetLanguage(Language language)
        {
            _currentLanguage = language;
        }

public Language CurrentLanguage => _currentLanguage;

publicstringGet(string key)
        {
if (_texts.TryGetValue(key, outvar translations))
            {
if (translations.TryGetValue(_currentLanguage, outvar text))
return text;
            }

            UnityEngine.Debug.LogWarning($"[TextManager] Missing text for key: {key}");
return$"[{key}]";
        }

publicstringGetFormat(string key, paramsobject[] args)
        {
var template = Get(key);
try
            {
returnstring.Format(template, args);
            }
catch (System.FormatException ex)
            {
                UnityEngine.Debug.LogWarning($"[TextManager] Format error for key '{key}': {ex.Message}");
return template;
            }
        }

publicvoidSet(string key, string chineseText, string englishText)
        {
if (!_texts.ContainsKey(key))
                _texts[key] = new Dictionary<Language, string>();

            _texts[key][Language.Chinese] = chineseText;
            _texts[key][Language.English] = englishText;
        }

privatevoidInitializeTexts()
        {
// UI 文本
            Set("window.title""Claude Code""Claude Code");
            Set("window.mode""模式:""Mode:");
            Set("window.send""发送""Send");
            Set("window.stop""停止""Stop");
            Set("window.stopping""停止中...""Stopping...");
            Set("window.session""会话""Session");
            Set("window.session.default""默认会话""Default Session");

// 会话管理
            Set("session.new""新建会话""New Session");
            Set("session.rename""重命名会话""Rename Session");
            Set("session.rename.hint""💡 重命名会话:使用 /rename <新名称>""💡 To rename session, use: /rename <new name>");
            Set("session.rename.hint2""或告诉代理:\"将此会话重命名为 <新名称>\"""Or tell the agent: \"Rename this session to <new name>\"");
            Set("session.delete""删除会话""Delete Session");
            Set("session.delete.confirm""确定要删除会话吗?""Are you sure you want to delete this session?");
            Set("session.delete.warning""此操作无法撤销!""This action cannot be undone!");
            Set("session.delete.title""删除会话""Delete Session");
            Set("session.delete.message""确定要删除会话 '{0}' 吗?\n\n这将永久删除:\n- 对话历史\n- {1} 个任务\n- 会话数据\n\n此操作无法撤销!""Are you sure you want to delete session '{0}'?\n\nThis will permanently delete:\n- Conversation history\n- {1} task(s)\n- Session data\n\nThis action cannot be undone!");
            Set("session.delete.button_yes""是的,删除""Yes, Delete");
            Set("session.delete.button_cancel""取消""Cancel");
            Set("session.no_active""❌ 没有活动会话可删除""❌ No active session to delete");
            Set("session.cannot_delete_last""❌ 无法删除最后一个会话。请先创建新会话。""❌ Cannot delete the last session. Create a new session first.");
            Set("session.delete.cancelled""ℹ️ 会话删除已取消""ℹ️ Session deletion cancelled");
            Set("session.deleted""🗑️ 会话 '{0}' 删除成功 ({1} 个任务已移除)""🗑️ Session '{0}' deleted successfully ({1} task(s) removed)");
            Set("session.delete_failed""❌ 删除会话 '{0}' 失败""❌ Failed to delete session '{0}'");
            Set("session.created""📂 新会话已创建: {0}""📂 New session created: {0}");
            Set("session.switched""📂 已切换到会话: {0}""📂 Switched to session: {0}");

// 状态消息
            Set("status.initializing""初始化中...""Initializing...");
            Set("status.ready""就绪""Ready");
            Set("status.connecting""连接中...""Connecting...");
            Set("status.error""错误""Error");
            Set("status.idle""空闲""Idle");
            Set("status.task_cancelled""任务已取消""Task cancelled");
            Set("status.task_cancelling""正在取消任务...""Cancelling task...");
            Set("status.thinking""思考中...""Thinking...");

// 欢迎消息
            Set("welcome.message""欢迎使用 Claude Code!""Welcome to Claude Code!");
            Set("welcome.model""模型:""Model:");
            Set("welcome.directory""目录:""Directory:");
            Set("welcome.session""当前会话:""Current Session:");
            Set("welcome.session.hint""会话:每个会话存储独立任务。使用上方工具栏切换会话。",
"Sessions: Each session stores separate tasks. Switch sessions using the toolbar above.");
            Set("welcome.tasks.hint""持久化任务:创建的任务会保存到 .sessions/ 目录,即使对话压缩也不会丢失!",
"Persistent Tasks: Created tasks are saved to .sessions/ and survive conversation compression!");

// 任务相关
            Set("task.create""创建任务""Create Task");
            Set("task.update""更新任务""Update Task");
            Set("task.list""任务列表""Task List");
            Set("task.list.persistent""持久化任务""Persistent Tasks");
            Set("task.get""获取任务""Get Task");
            Set("task.no_tasks""暂无任务。""No tasks.");
            Set("task.pending""待办""Pending");
            Set("task.in_progress""进行中""In Progress");
            Set("task.completed""已完成""Completed");

// 后台任务
            Set("background.run""后台运行""Background Run");
            Set("background.check""检查后台任务""Check Background");
            Set("background.no_tasks""没有后台任务。""No background tasks.");
            Set("background.timeout""超时""Timeout");
            Set("background.running""运行中""Running");
            Set("background.completed""已完成""Completed");
            Set("background.error""错误""Error");

// 技能相关
            Set("skill.load""加载技能""Load Skill");
            Set("skill.no_skills""暂无可用技能。""No skills available.");
            Set("skill.loaded""技能已加载""Skill loaded");
            Set("skill.unknown""未知技能""Unknown skill");
            Set("skill.available""可用技能:""Available skills:");

// 工具相关
            Set("tool.bash""运行shell命令""Run shell command");
            Set("tool.read_file""读取文件内容""Read file content");
            Set("tool.write_file""写入内容到文件""Write content to file");
            Set("tool.edit_file""替换文件中的精确文本""Replace exact text in file");
            Set("tool.executing""工具执行中...""Tool executing...");
            Set("tool.status""工具: {0}""Tool: {0}");
            Set("tool.status_with_details""工具: {0} - {1}""Tool: {0} - {1}");
            Set("tool.status_with_progress""工具: {0} ({1}/{2})""Tool: {0} ({1}/{2})");

// 错误消息
            Set("error.init_failed""模块初始化失败""Module initialization failed");
            Set("error.connection_failed""连接失败""Connection failed");
            Set("error.unknown_tool""未知工具""Unknown tool");
            Set("error.missing_param""缺少必需参数""Missing required parameter");
            Set("error.file_not_found""文件未找到""File not found");
            Set("error.permission_denied""权限被拒绝""Permission denied");

// 压缩相关
            Set("compact.auto""自动压缩""Auto Compact");
            Set("compact.manual""手动压缩""Manual Compact");
            Set("compact.completed""🗜️ 对话已压缩。记录: {0}""🗜️ Conversation compacted. Transcript: {0}");
            Set("compact.summary""摘要: {0}""Summary: {0}");
            Set("compact.clearing_memory""清理内存...""Clearing memory...");

// Token 使用
            Set("token.input""输入""Input");
            Set("token.output""输出""Output");
            Set("token.total""总计""Total");
            Set("token.usage""Token 使用""Token Usage");

// 模式
            Set("mode.explore""探索""Explore");
            Set("mode.code""编程""Code");
            Set("mode.plan""规划""Plan");
        }

public IEnumerable<stringGetAllKeys()
        {
return _texts.Keys;
        }
    }
}

ToolManager

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Anthropic;
using Anthropic.Models.Messages;

namespaceClaudeCodeForUnity
{
publicclassToolManager
    {
privatereadonlystring _workingDirectory;
privatereadonly BashAgent _bashAgent;
privatereadonly FileAgent _fileAgent;
privatereadonly TodoManager _todoManager;
privatereadonly TaskManager _taskManager;
privatereadonly SessionManager _sessionManager;
privatereadonly SubAgentManager _subAgentManager;
privatereadonly BackgroundManager _backgroundTaskManager;
privatereadonly SkillAgent _skillAgent;

// Helper method to serialize to JsonElement for .NET Standard 2.1 compatibility
privatestatic JsonElement SerializeToElement<T>(value)
        {
var json = JsonSerializer.Serialize(value);
usingvar doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
        }

publicenum SkillType
        {
            Explore,

            Code,

            Plan
        }

publicToolManager(string workingDirectory, BashAgent bashAgent, FileAgent fileAgent,
                         TodoManager todoManager, TaskManager taskManager = null,
                         SessionManager sessionManager = null, SubAgentManager subAgentManager = null,
                         BackgroundManager backgroundTaskManager = null, SkillAgent skillAgent = null
)

        {
            _workingDirectory = workingDirectory;
            _bashAgent = bashAgent;
            _fileAgent = fileAgent;
            _todoManager = todoManager;
            _taskManager = taskManager;
            _sessionManager = sessionManager;
            _subAgentManager = subAgentManager;
            _backgroundTaskManager = backgroundTaskManager;
            _skillAgent = skillAgent;
        }

public List<Tool> GetToolsForSkill(SkillType skillType)
        {
var tools = new List<Tool>();

// 基础工具
var bashTool = CreateBashTool();
var readFileTool = CreateReadFileTool();
var writeFileTool = CreateWriteFileTool();
var editFileTool = CreateEditFileTool();
var todoTool = CreateTodoTool();

// SubAgent Task工具(如果有subAgentManager)
            Tool subAgentTaskTool = null;
if (_subAgentManager != null)
            {
                subAgentTaskTool = CreateTaskTool();
            }

// 持久化任务工具(如果有sessionManager)
            Tool taskCreateTool = null;
            Tool taskUpdateTool = null;
            Tool taskListTool = null;
            Tool taskGetTool = null;
if (_sessionManager != null)
            {
                taskCreateTool = CreateTaskCreateTool();
                taskUpdateTool = CreateTaskUpdateTool();
                taskListTool = CreateTaskListTool();
                taskGetTool = CreateTaskGetTool();
            }

// 后台任务工具(如果有backgroundTaskManager)
            Tool backgroundRunTool = null;
            Tool checkBackgroundTool = null;
if (_backgroundTaskManager != null)
            {
                backgroundRunTool = CreateBackgroundRunTool();
                checkBackgroundTool = CreateCheckBackgroundTool();
            }

// 技能工具(如果有skillAgent)
            Tool skillTool = null;
if (_skillAgent != null)
            {
                skillTool = CreateSkillTool();
            }

switch (skillType)
            {
case SkillType.Explore:
// 只读探索:bash和read_file
                    tools.Add(bashTool);
                    tools.Add(readFileTool);
break;

case SkillType.Code:
// 完整编程:所有工具
                    tools.Add(bashTool);
                    tools.Add(readFileTool);
                    tools.Add(writeFileTool);
                    tools.Add(editFileTool);
                    tools.Add(todoTool);
if (subAgentTaskTool != null)
                        tools.Add(subAgentTaskTool);
// 添加持久化任务工具
if (taskCreateTool != null)
                    {
                        tools.Add(taskCreateTool);
                        tools.Add(taskUpdateTool);
                        tools.Add(taskListTool);
                        tools.Add(taskGetTool);
                    }
// 添加后台任务工具
if (backgroundRunTool != null)
                    {
                        tools.Add(backgroundRunTool);
                        tools.Add(checkBackgroundTool);
                    }
// 添加技能工具
if (skillTool != null)
                    {
                        tools.Add(skillTool);
                    }
break;

case SkillType.Plan:
// 规划:bash和read_file(不修改)
                    tools.Add(bashTool);
                    tools.Add(readFileTool);
                    tools.Add(todoTool);
if (subAgentTaskTool != null)
                        tools.Add(subAgentTaskTool);
// 添加持久化任务工具
if (taskListTool != null)
                    {
                        tools.Add(taskListTool);
                        tools.Add(taskGetTool);
                    }
break;
            }

return tools;
        }

publicasync Task<stringExecuteToolAsync(string toolName, IReadOnlyDictionary<string, JsonElement> args)
        {
return toolName switch
            {
"bash" => await ExecuteBashAsync(args),
"read_file" => ExecuteReadFile(args),
"write_file" => ExecuteWriteFile(args),
"edit_file" => ExecuteEditFile(args),
"TodoWrite" => ExecuteTodoWrite(args),
"Task" => await ExecuteTaskAsync(args),
"task_create" => ExecuteTaskCreate(args),
"task_update" => ExecuteTaskUpdate(args),
"task_list" => ExecuteTaskList(args),
"task_get" => ExecuteTaskGet(args),
"background_run" => ExecuteBackgroundRun(args),
"check_background" => ExecuteCheckBackground(args),
"Skill" => ExecuteSkill(args),
                _ => $"Unknown tool: {toolName}"
            };
        }

#region 工具创建方法

private Tool CreateBashTool()
        {
returnnew Tool
            {
                Name = "bash",
                Description = "运行shell命令。用于: ls, find, grep, git, npm, dotnet等。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["command"] = SerializeToElement(new { type = "string", description = "要执行的命令" })
                    },
                    Required = new List<string> { "command" }
                }
            };
        }

private Tool CreateReadFileTool()
        {
returnnew Tool
            {
                Name = "read_file",
                Description = "读取文件内容。返回UTF-8文本。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["path"] = SerializeToElement(new { type = "string", description = "文件的相对路径" }),
                        ["limit"] = SerializeToElement(new { type = "integer", description = "最大读取行数(默认: 全部)" })
                    },
                    Required = new List<string> { "path" }
                }
            };
        }

private Tool CreateWriteFileTool()
        {
returnnew Tool
            {
                Name = "write_file",
                Description = "将内容写入文件。如果需要会创建父目录。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["path"] = SerializeToElement(new { type = "string", description = "文件的相对路径" }),
                        ["content"] = SerializeToElement(new { type = "string", description = "要写入的内容" })
                    },
                    Required = new List<string> { "path""content" }
                }
            };
        }

private Tool CreateEditFileTool()
        {
returnnew Tool
            {
                Name = "edit_file",
                Description = "替换文件中的精确文本。用于局部编辑。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["path"] = SerializeToElement(new { type = "string", description = "文件的相对路径" }),
                        ["old_text"] = SerializeToElement(new { type = "string", description = "要查找的精确文本(必须完全匹配)" }),
                        ["new_text"] = SerializeToElement(new { type = "string", description = "替换文本" })
                    },
                    Required = new List<string> { "path""old_text""new_text" }
                }
            };
        }

private Tool CreateTodoTool()
        {
returnnew Tool
            {
                Name = "TodoWrite",
                Description = @"更新结构化任务列表。用于规划和追踪进度。

规则:
1. 最多20项任务
2. 同时只能有1个任务状态为 in_progress
3. 每个任务需要 content(任务描述)、status(状态)、activeForm(进行时描述)

示例:
{
  ""items"": [
    {""content"": ""读取配置文件"", ""status"": ""completed"", ""activeForm"": ""Reading config file""},
    {""content"": ""解析JSON数据"", ""status"": ""in_progress"", ""activeForm"": ""Parsing JSON data""},
    {""content"": ""写入输出文件"", ""status"": ""pending"", ""activeForm"": ""Writing output file""}
  ]
}"
,
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["items"] = SerializeToElement(new
                        {
                            type = "array",
                            description = "完整的任务列表(替换现有)",
                            items = new
                            {
                                type = "object",
                                properties = new
                                {
                                    content = new { type = "string", description = "任务描述(必填)" },
                                    status = new { type = "string", @enum = new[] { "pending""in_progress""completed" }, description = "任务状态:pending(待处理)、in_progress(进行中)、completed(已完成)" },
                                    activeForm = new { type = "string", description = "现在进行时描述,如'Reading file'、'Parsing data'(必填)" }
                                },
required = new[] { "content""status""activeForm" }
                            }
                        })
                    },
                    Required = new List<string> { "items" }
                }
            };
        }

private Tool CreateTaskTool()
        {
var agentDescriptions = _subAgentManager?.GetAgentDescriptions() ?? new Dictionary<stringstring>();
var descriptionText = string.Join("\n", agentDescriptions.Select(kv => $"- {kv.Key}{kv.Value}"));

returnnew Tool
            {
                Name = "Task",
                Description = $"为专注的子任务生成子代理。\n\n可用代理类型:\n{descriptionText}",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["description"] = SerializeToElement(new { type = "string", description = "简短任务名(3-5词)用于进度显示" }),
                        ["prompt"] = SerializeToElement(new { type = "string", description = "给子代理的详细指令" }),
                        ["agent_type"] = SerializeToElement(new { type = "string", @enum = agentDescriptions.Keys.ToArray() })
                    },
                    Required = new List<string> { "description""prompt""agent_type" }
                }
            };
        }

private Tool CreateTaskCreateTool()
        {
returnnew Tool
            {
                Name = "task_create",
                Description = "创建一个新的持久化任务。任务会保存到 .tasks/ 目录,即使对话压缩也不会丢失。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["subject"] = SerializeToElement(new { type = "string", description = "任务主题(必填)" }),
                        ["description"] = SerializeToElement(new { type = "string", description = "任务详细描述(可选)" })
                    },
                    Required = new List<string> { "subject" }
                }
            };
        }

private Tool CreateTaskUpdateTool()
        {
returnnew Tool
            {
                Name = "task_update",
                Description = "更新任务状态或依赖关系。完成任务时会自动清除其他任务的阻塞关系。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["task_id"] = SerializeToElement(new { type = "integer", description = "任务 ID(必填)" }),
                        ["status"] = SerializeToElement(new { type = "string", @enum = new[] { "pending""in_progress""completed" }, description = "新状态(可选)" }),
                        ["addBlockedBy"] = SerializeToElement(new { type = "array", items = new { type = "integer" }, description = "添加阻塞依赖:此任务被哪些任务阻塞(可选)" }),
                        ["addBlocks"] = SerializeToElement(new { type = "array", items = new { type = "integer" }, description = "添加阻塞关系:此任务阻塞哪些任务(可选)" })
                    },
                    Required = new List<string> { "task_id" }
                }
            };
        }

private Tool CreateTaskListTool()
        {
returnnew Tool
            {
                Name = "task_list",
                Description = "列出所有持久化任务及其状态和依赖关系。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>(),
                    Required = new List<string>()
                }
            };
        }

private Tool CreateTaskGetTool()
        {
returnnew Tool
            {
                Name = "task_get",
                Description = "获取指定任务的完整详情(JSON格式)。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["task_id"] = SerializeToElement(new { type = "integer", description = "任务 ID" })
                    },
                    Required = new List<string> { "task_id" }
                }
            };
        }

private Tool CreateBackgroundRunTool()
        {
returnnew Tool
            {
                Name = "background_run",
                Description = "在后台线程运行命令。适用于长时间运行的操作(如 build, test, install)。立即返回 task_id。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["command"] = SerializeToElement(new { type = "string", description = "要在后台执行的命令" })
                    },
                    Required = new List<string> { "command" }
                }
            };
        }

private Tool CreateCheckBackgroundTool()
        {
returnnew Tool
            {
                Name = "check_background",
                Description = "检查后台任务状态。省略 task_id 返回所有任务列表。",
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["task_id"] = SerializeToElement(new { type = "string", description = "任务 ID(可选)" })
                    },
                    Required = new List<string>()
                }
            };
        }

private Tool CreateSkillTool()
        {
var skillDescriptions = _skillAgent != null ? _skillAgent.GetSkillDescriptions() : "(no skills available)";

returnnew Tool
            {
                Name = "Skill",
                Description = $@"加载技能以获取任务的专业知识。

可用技能:
{skillDescriptions}

何时使用:
- 用户任务匹配技能描述时立即使用
- 在尝试特定领域工作(PDF、MCP 等)之前

技能内容将注入对话,给你详细指令和资源访问。"
,
                InputSchema = new InputSchema
                {
                    Properties = new Dictionary<string, JsonElement>
                    {
                        ["skill"] = SerializeToElement(new { type = "string", description = "要加载的技能名称" })
                    },
                    Required = new List<string> { "skill" }
                }
            };
        }

#endregion

#region 工具执行方法

privateasync Task<stringExecuteBashAsync(IReadOnlyDictionary<string, JsonElement> args)
        {
var command = args["command"].GetString();
if (string.IsNullOrEmpty(command))
return"Error: command is required";

returnawait _bashAgent.ExecuteAsync(command);
        }

privatestringExecuteReadFile(IReadOnlyDictionary<string, JsonElement> args)
        {
var path = args["path"].GetString();
if (string.IsNullOrEmpty(path))
return"Error: path is required";

var limit = args.TryGetValue("limit"outvar limitElement) ? limitElement.GetInt32() : (int?)null;
return _fileAgent.ReadFile(path, limit);
        }

privatestringExecuteWriteFile(IReadOnlyDictionary<string, JsonElement> args)
        {
var path = args["path"].GetString();
var content = args["content"].GetString();

if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(content))
return"Error: path and content are required";

return _fileAgent.WriteFile(path, content);
        }

privatestringExecuteEditFile(IReadOnlyDictionary<string, JsonElement> args)
        {
var path = args["path"].GetString();
var oldText = args["old_text"].GetString();
var newText = args["new_text"].GetString();

if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(oldText) || string.IsNullOrEmpty(newText))
return"Error: path, old_text and new_text are required";

return _fileAgent.EditFile(path, oldText, newText);
        }

privatestringExecuteTodoWrite(IReadOnlyDictionary<string, JsonElement> args)
        {
var items = args["items"];
return _todoManager.Update(items);
        }

privateasync Task<stringExecuteTaskAsync(IReadOnlyDictionary<string, JsonElement> args)
        {
if (_subAgentManager == null)
return"Error: SubAgentManager not available";

var description = args["description"].GetString();
var prompt = args["prompt"].GetString();
var agentType = args["agent_type"].GetString();

if (string.IsNullOrEmpty(description) || string.IsNullOrEmpty(prompt) || string.IsNullOrEmpty(agentType))
return"Error: description, prompt and agent_type are required";

// 获取当前可用的工具列表
var currentTools = GetToolsForSkill(SkillType.Code);

try
            {
returnawait _subAgentManager.ExecuteSubAgentAsync(description, prompt, agentType, currentTools);
            }
catch (Exception ex)
            {
return$"Error executing sub-agent: {ex.Message}";
            }
        }

privatestringExecuteTaskCreate(IReadOnlyDictionary<string, JsonElement> args)
        {
if (_sessionManager == null)
return"Error: SessionManager not available";

var subject = args["subject"].GetString();
if (string.IsNullOrEmpty(subject))
return"Error: subject is required";

var description = args.TryGetValue("description"outvar desc) ? desc.GetString() : "";

try
            {
// 在当前会话中创建任务
var task = _sessionManager.CreateTask(subject, description);
return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
            }
catch (Exception ex)
            {
return$"Error creating task: {ex.Message}";
            }
        }

privatestringExecuteTaskUpdate(IReadOnlyDictionary<string, JsonElement> args)
        {
if (_sessionManager == null)
return"Error: SessionManager not available";

if (!args.TryGetValue("task_id"outvar idElement))
return"Error: task_id is required";

var taskId = idElement.GetInt32();

var status = args.TryGetValue("status"outvar statusElement) ? statusElement.GetString() : null;

            List<int> addBlockedBy = null;
if (args.TryGetValue("addBlockedBy"outvar blockedByElement) && blockedByElement.ValueKind == JsonValueKind.Array)
            {
                addBlockedBy = blockedByElement.EnumerateArray().Select(e => e.GetInt32()).ToList();
            }

            List<int> addBlocks = null;
if (args.TryGetValue("addBlocks"outvar blocksElement) && blocksElement.ValueKind == JsonValueKind.Array)
            {
                addBlocks = blocksElement.EnumerateArray().Select(e => e.GetInt32()).ToList();
            }

try
            {
var task = _sessionManager.UpdateTask(taskId, status, addBlockedBy, addBlocks);
return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
            }
catch (Exception ex)
            {
return$"Error updating task: {ex.Message}";
            }
        }

privatestringExecuteTaskList(IReadOnlyDictionary<string, JsonElement> _)
        {
if (_sessionManager == null)
return"Error: SessionManager not available";

try
            {
var tasks = _sessionManager.ListTasks();
if (tasks.Count == 0)
return"No tasks.";

var lines = new List<string>();
foreach (var task in tasks)
                {
var marker = task.Status switch
                    {
"pending" => "[ ]",
"in_progress" => "[>]",
"completed" => "[x]",
                        _ => "[?]"
                    };

var blockedInfo = task.BlockedBy != null && task.BlockedBy.Count > 0
                        ? $" (blocked by: [{string.Join(", ", task.BlockedBy)}])"
                        : "";

                    lines.Add($"{marker} #{task.Id}{task.Subject}{blockedInfo}");
                }

returnstring.Join("\n", lines);
            }
catch (Exception ex)
            {
return$"Error listing tasks: {ex.Message}";
            }
        }

privatestringExecuteTaskGet(IReadOnlyDictionary<string, JsonElement> args)
        {
if (_sessionManager == null)
return"Error: SessionManager not available";

if (!args.TryGetValue("task_id"outvar idElement))
return"Error: task_id is required";

var taskId = idElement.GetInt32();

try
            {
var task = _sessionManager.GetTask(taskId);
if (task == null)
return$"Task #{taskId} not found in current session";

return JsonSerializer.Serialize(task, new JsonSerializerOptions { WriteIndented = true });
            }
catch (Exception ex)
            {
return$"Error getting task: {ex.Message}";
            }
        }

privatestringExecuteBackgroundRun(IReadOnlyDictionary<string, JsonElement> args)
        {
if (_backgroundTaskManager == null)
return"Error: BackgroundManager not available";

if (!args.TryGetValue("command"outvar commandElement))
return"Error: command is required";

var command = commandElement.GetString();
if (string.IsNullOrEmpty(command))
return"Error: command cannot be empty";

try
            {
return _backgroundTaskManager.Run(command);
            }
catch (Exception ex)
            {
return$"Error starting background task: {ex.Message}";
            }
        }

privatestringExecuteCheckBackground(IReadOnlyDictionary<string, JsonElement> args)
        {
if (_backgroundTaskManager == null)
return"Error: BackgroundManager not available";

try
            {
var taskId = args.TryGetValue("task_id"outvar idElement)
                    ? idElement.GetString()
                    : null;

return _backgroundTaskManager.Check(taskId);
            }
catch (Exception ex)
            {
return$"Error checking background tasks: {ex.Message}";
            }
        }

privatestringExecuteSkill(IReadOnlyDictionary<string, JsonElement> args)
        {
if (_skillAgent == null)
return"Error: SkillAgent not available";

if (!args.TryGetValue("skill"outvar skillElement))
return"Error: skill name is required";

var skillName = skillElement.GetString();

try
            {
// 委托给 SkillAgent 处理
return _skillAgent.LoadSkill(skillName);
            }
catch (Exception ex)
            {
return$"Error loading skill: {ex.Message}";
            }
        }

#endregion

publicstaticstringGetSkillDescription(SkillType skillType)
        {
return skillType switch
            {
                SkillType.Explore => "只读代理,用于探索代码、查找文件、搜索",
                SkillType.Code => "完整代理,用于实现功能和修复bug",
                SkillType.Plan => "规划代理,用于设计实现策略",
                _ => "未知技能类型"
            };
        }

publicstatic Dictionary<SkillType, stringGetAllSkillDescriptions()
        {
returnnew Dictionary<SkillType, string>
            {
                { SkillType.Explore, GetSkillDescription(SkillType.Explore) },
                { SkillType.Code, GetSkillDescription(SkillType.Code) },
                { SkillType.Plan, GetSkillDescription(SkillType.Plan) }
            };
        }
    }
}

UIToolkit

接下来简单做个界面来承载这些功能,UI 使用的 UIToolkit。

虽然使用的是 Anthropic SDK,但这里使用的大预言模式是 DeepSeek。

ClaudeConfig.cs

using System.IO;
using UnityEditor;
using UnityEngine;

namespaceClaudeCodeForUnity
{
    [System.Serializable]
publicclassClaudeConfig
    {
public ApiConfig api;
public UiConfig ui;
public PathsConfig paths;

        [System.Serializable]
publicclassApiConfig
        {
publicstring baseUrl;
publicstring apiKey;
publicstring model;
        }

        [System.Serializable]
publicclassUiConfig
        {
publicstring language;
publicstring theme;
        }

        [System.Serializable]
publicclassPathsConfig
        {
publicstring skillsDirectory;
publicstring sessionsDirectory;
        }
    }

publicclassConfigManager
    {
privatestatic ClaudeConfig _config;
privatestaticreadonlystring ConfigFilePath = Path.Combine(
            Application.dataPath,
"Claude Code for Unity",
"Editor",
"config",
"claude_config.json"
        );

// 默认配置常量
privateconststring DEFAULT_BASE_URL = "https://api.deepseek.com/anthropic";
privateconststring DEFAULT_API_KEY = "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
privateconststring DEFAULT_MODEL = "deepseek-reasoner";

// EditorPrefs 键名
privateconststring PREF_BASE_URL = "ClaudeCode.BaseUrl";
privateconststring PREF_API_KEY = "ClaudeCode.ApiKey";
privateconststring PREF_MODEL = "ClaudeCode.Model";

staticConfigManager()
        {
            LoadConfig();
        }

privatestaticvoidLoadConfig()
        {
try
            {
if (File.Exists(ConfigFilePath))
                {
var json = File.ReadAllText(ConfigFilePath);
                    _config = JsonUtility.FromJson<ClaudeConfig>(json);
                    UnityEngine.Debug.Log($"[ConfigManager] Loaded config from: {ConfigFilePath}");
                }
else
                {
// 创建默认配置
                    _config = new ClaudeConfig
                    {
                        api = new ClaudeConfig.ApiConfig
                        {
                            baseUrl = DEFAULT_BASE_URL,
                            apiKey = DEFAULT_API_KEY,
                            model = DEFAULT_MODEL
                        },
                        ui = new ClaudeConfig.UiConfig
                        {
                            language = "Chinese",
                            theme = "dark"
                        },
                        paths = new ClaudeConfig.PathsConfig
                        {
                            skillsDirectory = "Assets/Claude Code for Unity/Editor/skills",
                            sessionsDirectory = ".sessions"
                        }
                    };
                    SaveConfig();
                    UnityEngine.Debug.Log($"[ConfigManager] Created default config at: {ConfigFilePath}");
                }
            }
catch (System.Exception ex)
            {
                UnityEngine.Debug.LogError($"[ConfigManager] Failed to load config: {ex.Message}");
// 使用默认配置
                _config = new ClaudeConfig
                {
                    api = new ClaudeConfig.ApiConfig
                    {
                        baseUrl = DEFAULT_BASE_URL,
                        apiKey = DEFAULT_API_KEY,
                        model = DEFAULT_MODEL
                    },
                    ui = new ClaudeConfig.UiConfig
                    {
                        language = "Chinese",
                        theme = "dark"
                    },
                    paths = new ClaudeConfig.PathsConfig
                    {
                        skillsDirectory = "Assets/Claude Code for Unity/Editor/skills",
                        sessionsDirectory = ".sessions"
                    }
                };
            }
        }

publicstaticvoidSaveConfig()
        {
try
            {
var json = JsonUtility.ToJson(_config, true);
                File.WriteAllText(ConfigFilePath, json);
                UnityEngine.Debug.Log($"[ConfigManager] Saved config to: {ConfigFilePath}");
            }
catch (System.Exception ex)
            {
                UnityEngine.Debug.LogError($"[ConfigManager] Failed to save config: {ex.Message}");
            }
        }

publicstaticvoidReloadConfig()
        {
            _config = null;
            _workingDirectory = null;
            _skillsDirectory = null;
            _sessionsDirectory = null;
            LoadConfig();
            UnityEngine.Debug.Log("[ConfigManager] Configuration reloaded from file");
        }

publicstaticstring BaseUrl
        {
get
            {
if (EditorPrefs.HasKey(PREF_BASE_URL))
return EditorPrefs.GetString(PREF_BASE_URL);
return _config?.api?.baseUrl ?? DEFAULT_BASE_URL;
            }
        }

publicstaticstring ApiKey
        {
get
            {
if (EditorPrefs.HasKey(PREF_API_KEY))
return EditorPrefs.GetString(PREF_API_KEY);
return _config?.api?.apiKey ?? DEFAULT_API_KEY;
            }
        }

publicstaticstring Model
        {
get
            {
if (EditorPrefs.HasKey(PREF_MODEL))
return EditorPrefs.GetString(PREF_MODEL);
return _config?.api?.model ?? DEFAULT_MODEL;
            }
        }

publicstaticstring Language => _config?.ui?.language ?? "Chinese";

publicstaticvoidSetApiConfig(string baseUrl, string apiKey, string model)
        {
            EditorPrefs.SetString(PREF_BASE_URL, baseUrl);
            EditorPrefs.SetString(PREF_API_KEY, apiKey);
            EditorPrefs.SetString(PREF_MODEL, model);
        }

publicstaticvoidResetToDefaults()
        {
            EditorPrefs.DeleteKey(PREF_BASE_URL);
            EditorPrefs.DeleteKey(PREF_API_KEY);
            EditorPrefs.DeleteKey(PREF_MODEL);
        }

// 目录路径
privatestaticstring _workingDirectory;
privatestaticstring _skillsDirectory;
privatestaticstring _sessionsDirectory;

publicstaticstring WorkingDirectory
        {
get
            {
if (string.IsNullOrEmpty(_workingDirectory))
                {
                    _workingDirectory = System.Environment.CurrentDirectory;
                }
return _workingDirectory;
            }
        }

publicstaticstring SkillsDirectory
        {
get
            {
if (string.IsNullOrEmpty(_skillsDirectory))
                {
var configPath = _config?.paths?.skillsDirectory ?? "Assets/Claude Code for Unity/Editor/skills";

// 如果是相对路径,基于 Application.dataPath 解析
if (!Path.IsPathRooted(configPath))
                    {
// 如果路径以 "Assets/" 开头,替换为实际的 dataPath
if (configPath.StartsWith("Assets/") || configPath.StartsWith("Assets\\"))
                        {
                            configPath = configPath.Substring("Assets/".Length);
                            _skillsDirectory = Path.Combine(Application.dataPath, configPath);
                        }
else
                        {
                            _skillsDirectory = Path.Combine(WorkingDirectory, configPath);
                        }
                    }
else
                    {
                        _skillsDirectory = configPath;
                    }
                }
return _skillsDirectory;
            }
        }

publicstaticstring SessionsDirectory
        {
get
            {
if (string.IsNullOrEmpty(_sessionsDirectory))
                {
var configPath = _config?.paths?.sessionsDirectory ?? ".sessions";

// 如果是相对路径,基于 WorkingDirectory 解析
if (!Path.IsPathRooted(configPath))
                    {
                        _sessionsDirectory = Path.Combine(WorkingDirectory, configPath);
                    }
else
                    {
                        _sessionsDirectory = configPath;
                    }
                }
return _sessionsDirectory;
            }
        }

publicstaticstringGetRelativePath(string fullPath)
        {
if (fullPath.StartsWith(WorkingDirectory))
            {
return fullPath.Substring(WorkingDirectory.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
            }
return fullPath;
        }

publicstaticvoidEnsureDirectoryExists(string path)
        {
if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
        }
    }
}

在 Unity Editor 启动是就加载配置文件。

ClaudeCodeInitializer.cs

using UnityEditor;
using UnityEngine;

namespaceClaudeCodeForUnity
{
    [InitializeOnLoad]
publicstaticclassClaudeCodeInitializer
    {
staticClaudeCodeInitializer()
        {
// 延迟执行,确保 Unity 编辑器完全启动
            EditorApplication.delayCall += Initialize;
        }

privatestaticvoidInitialize()
        {
            Debug.Log("[Claude Code] Initializing...");

try
            {
// 1. 触发 ConfigManager 的静态构造函数,加载配置文件
var baseUrl = ConfigManager.BaseUrl;
var model = ConfigManager.Model;
var language = ConfigManager.Language;

                Debug.Log($"[Claude Code] Configuration loaded successfully");
                Debug.Log($"  - API Base URL: {baseUrl}");
                Debug.Log($"  - Model: {model}");
                Debug.Log($"  - Language: {language}");
                Debug.Log($"  - Skills Directory: {ConfigManager.SkillsDirectory}");
                Debug.Log($"  - Sessions Directory: {ConfigManager.SessionsDirectory}");

// 2. 确保必要的目录存在
                ConfigManager.EnsureDirectoryExists(ConfigManager.SessionsDirectory);
                Debug.Log($"[Claude Code] Session directory verified: {ConfigManager.SessionsDirectory}");

// 3. 如果需要,可以在这里进行其他初始化工作
// 例如:检查技能目录、验证配置等

                Debug.Log("[Claude Code] Initialization complete");
            }
catch (System.Exception ex)
            {
                Debug.LogError($"[Claude Code] Initialization failed: {ex.Message}");
                Debug.LogException(ex);
            }
        }

    }
}

一个简单的状态栏

StatusBar.cs

using UnityEngine;
using UnityEngine.UIElements;

namespaceClaudeCodeForUnity
{
///<summary>
/// 状态栏组件,参考VSCode Claude Code状态栏
///</summary>
publicclassStatusBar : VisualElement
    {
private Label _modelLabel;
private Label _connectionLabel;
private Label _tokenLabel;
private Label _statusLabel;

///<summary>
/// 创建状态栏实例
///</summary>
publicStatusBar()
        {
// 状态栏样式
            AddToClassList("status-bar");
            style.flexDirection = FlexDirection.Row;
            style.justifyContent = Justify.SpaceBetween;
            style.paddingLeft = 12;
            style.paddingRight = 12;
            style.paddingTop = 4;
            style.paddingBottom = 4;
            style.backgroundColor = new Color(0.1f0.1f0.1f1.0f);
            style.borderTopWidth = 1;
            style.borderTopColor = new Color(0.3f0.3f0.3f1.0f);

// 左侧:模型信息
var leftContainer = new VisualElement();
            leftContainer.style.flexDirection = FlexDirection.Row;
            leftContainer.style.alignItems = Align.Center;

            _modelLabel = CreateStatusLabel("Model: Unknown");
            _modelLabel.name = "model-label";
            leftContainer.Add(_modelLabel);

            Add(leftContainer);

// 中间:连接状态
var centerContainer = new VisualElement();
            centerContainer.style.flexDirection = FlexDirection.Row;
            centerContainer.style.alignItems = Align.Center;

            _connectionLabel = CreateStatusLabel("🔗 Connecting...");
            _connectionLabel.name = "connection-label";
            centerContainer.Add(_connectionLabel);

            Add(centerContainer);

// 右侧:Token使用和状态
var rightContainer = new VisualElement();
            rightContainer.style.flexDirection = FlexDirection.Row;
            rightContainer.style.alignItems = Align.Center;

            _tokenLabel = CreateStatusLabel("Tokens: 0/0");
            _tokenLabel.name = "token-label";
            rightContainer.Add(_tokenLabel);

            _statusLabel = CreateStatusLabel("Ready");
            _statusLabel.name = "status-label";
            _statusLabel.style.marginLeft = 12;
            rightContainer.Add(_statusLabel);

            Add(rightContainer);
        }

///<summary>
/// 创建状态标签
///</summary>
private Label CreateStatusLabel(string text)
        {
var label = new Label(text);
            label.AddToClassList("status-label");
            label.style.fontSize = 11;
            label.style.unityFontStyleAndWeight = FontStyle.Normal;
            label.style.color = new Color(0.7f0.7f0.7f1.0f);
return label;
        }

///<summary>
/// 更新模型显示
///</summary>
publicvoidUpdateModel(string modelName)
        {
            _modelLabel.text = $"Model: {modelName}";
        }

///<summary>
/// 更新连接状态
///</summary>
publicvoidUpdateConnectionStatus(bool connected, string message = null)
        {
if (connected)
            {
                _connectionLabel.text = "🔗 Connected";
                _connectionLabel.style.color = new Color(0.2f0.8f0.2f1.0f);
            }
else
            {
                _connectionLabel.text = message ?? "🔌 Disconnected";
                _connectionLabel.style.color = new Color(0.8f0.2f0.2f1.0f);
            }
        }

///<summary>
/// 更新Token使用情况
///</summary>
publicvoidUpdateTokenUsage(int used, int total)
        {
            _tokenLabel.text = $"Tokens: {used}/{total}";

// 根据使用率改变颜色
if (total > 0)
            {
float usage = (float)used / total;
if (usage > 0.9f)
                    _tokenLabel.style.color = new Color(0.8f0.2f0.2f1.0f); // 红色
elseif (usage > 0.7f)
                    _tokenLabel.style.color = new Color(0.8f0.8f0.2f1.0f); // 黄色
else
                    _tokenLabel.style.color = new Color(0.2f0.8f0.2f1.0f); // 绿色
            }
        }

///<summary>
/// 更新状态消息
///</summary>
publicvoidUpdateStatus(string status, StatusType type = StatusType.Info)
        {
            _statusLabel.text = status;

switch (type)
            {
case StatusType.Info:
                    _statusLabel.style.color = new Color(0.7f0.7f0.7f1.0f);
break;
case StatusType.Warning:
                    _statusLabel.style.color = new Color(0.8f0.8f0.2f1.0f);
break;
case StatusType.Error:
                    _statusLabel.style.color = new Color(0.8f0.2f0.2f1.0f);
break;
case StatusType.Success:
                    _statusLabel.style.color = new Color(0.2f0.8f0.2f1.0f);
break;
            }
        }

///<summary>
/// 显示工具执行状态
///</summary>
publicvoidShowToolExecution(string toolName, int current, int total)
        {
if (total > 0)
            {
                UpdateStatus($"🔧 {toolName} ({current}/{total})", StatusType.Info);
            }
else
            {
                UpdateStatus($"🔧 {toolName}", StatusType.Info);
            }
        }

///<summary>
/// 显示思考状态
///</summary>
publicvoidShowThinking()
        {
            UpdateStatus("💭 Thinking...", StatusType.Info);
        }

///<summary>
/// 显示空闲状态
///</summary>
publicvoidShowIdle()
        {
            UpdateStatus("✅ Ready", StatusType.Success);
        }

///<summary>
/// 状态类型
///</summary>
publicenum StatusType
        {
            Info,
            Warning,
            Error,
            Success
        }
    }
}

UI 样式文件:

ClaudeCodeWindow.uss

/* 根容器 - Claude Code 深色主题 */
.unity-editor-window {
background-colorrgb(303030);
}

/* 标题栏 */
.header {
background-colorrgb(373738);
padding8px12px;
border-bottom-width1px;
border-bottom-colorrgb(606060);
flex-direction: row;
align-items: center;
}

.title {
font-size13px;
    -unity-font-style: bold;
colorrgb(204204204);
}

/* 会话工具栏 */
.session-toolbar {
flex-direction: row;
align-items: center;
padding-left8px;
padding-right8px;
padding-top4px;
padding-bottom4px;
background-colorrgb(515157);
border-bottom-width1px;
border-bottom-colorrgb(767676);
}

.session-icon {
margin-right6px;
font-size14px;
}

.session-selector {
flex-grow1;
min-width200px;
}

.session-button {
width30px;
height22px;
margin-left4px;
}

/* 聊天滚动视图 - Timeline 样式 */
.chat-scroll-view {
flex-grow1;
background-colorrgb(303030);
padding0;
}

.chat-scroll-view > .unity-scroll-view__content-container {
padding0;
}

/* 消息容器基础样式 */
.message-container {
margin-bottom0;
padding10px16px;
background-color: transparent;
flex-direction: row;
align-items: flex-start;
}

.message-container:hover {
background-colorrgba(2552552550.03);
}

/* 消息头像/图标区域 */
.message-icon {
width20px;
height20px;
margin-right8px;
margin-top2px;
    -unity-text-align: middle-center;
font-size14px;
flex-shrink0;
}

/* 用户消息 - 右对齐 */
.user-message {
flex-direction: row-reverse;
justify-content: flex-start;
border-right-width3px;
border-right-colorrgb(0122204);
background-colorrgba(01222040.08);
}

.user-message.message-icon {
margin-right0;
margin-left8px;
}

.user-message.message-content {
colorrgb(230230230);
white-space: normal;
    -unity-text-align: upper-right;
}

/* 助手消息 - 左对齐 */
.assistant-message {
background-color: transparent;
}

/* 助手消息的内容容器 - 浅绿色圆角框包围整个 markdown 内容 */
.assistant-content {
border-width2px;
border-colorrgb(144238144);
border-radius10px;
background-colorrgba(1442381440.08);
padding14px16px;
flex-direction: column;
flex-shrink0;
margin-top6px;
margin-bottom6px;
}

/* 助手消息中的普通文本 */
.assistant-message.message-content {
colorrgb(204204204);
white-space: normal;
padding0;
margin0;
}

/* Markdown 代码块内容样式(在绿框内部)*/
.markdown-code-block {
background-color: transparent;
border-width0;
padding4px;
margin0;
colorrgb(220220220);
font-size13px;
    -unity-font-style: normal;
white-space: pre;
line-height1.5;
}

/* ScrollView 在绿框内 */
.assistant-content.unity-scroll-view {
background-color: transparent;
border-width0;
}

.assistant-content.unity-scroll-view__content-container {
background-color: transparent;
}

/* 系统消息 - 居中 */
.system-message {
border-left-width0;
background-color: transparent;
padding6px16px;
justify-content: center;
}

.system-message.message-icon {
colorrgb(10615385);
}

.system-message.message-content {
colorrgb(140140140);
white-space: normal;
font-size12px;
    -unity-text-align: middle-center;
}

/* 错误消息 - 居中 */
.error-message {
border-left-width3px;
border-left-colorrgb(2447171);
background-colorrgba(24471710.1);
}

.error-message.message-content {
colorrgb(244150150);
white-space: normal;
}

/* Thinking 状态样式 - 左对齐 */
.thinking-message {
border-left-width3px;
border-left-colorrgb(13992246);
background-color: transparent;
}

.thinking-message.message-content {
colorrgb(140140140);
white-space: normal;
    -unity-font-style: italic;
}

/* 工具调用消息 - 左对齐 */
.tool-message {
border-left-width3px;
border-left-colorrgb(10615385);
background-colorrgba(106153850.05);
padding8px16px;
}

.tool-message.message-content {
colorrgb(180180180);
font-size12px;
}

/* 消息内容 */
.message-content {
font-size13px;
padding0;
white-space: normal;
line-height1.4;
flex-grow1;
}

/* 工具指示点(绿点) */
.tool-dot {
width8px;
height8px;
border-radius4px;
background-colorrgb(10615385);
margin-right6px;
margin-top5px;
flex-shrink0;
}

/* 输入区域 */
.input-container {
background-colorrgb(373738);
padding10px12px;
border-top-width1px;
border-top-colorrgb(606060);
flex-direction: column;
flex-shrink0;
}

/* 输入行 */
.input-row {
flex-direction: row;
flex-grow1;
}

/* 模式选择器容器 */
.mode-selector-container {
flex-direction: row;
align-items: center;
margin-bottom6px;
padding-bottom6px;
}

.mode-label {
font-size12px;
colorrgb(180180180);
margin-right8px;
min-width40px;
}

.mode-selector {
min-width100px;
max-width120px;
height24px;
}

.mode-selector > .unity-base-field__input {
background-colorrgb(606060);
border-colorrgb(808080);
border-width1px;
border-radius3px;
padding2px6px;
colorrgb(204204204);
font-size11px;
height24px;
}

.mode-selector > .unity-base-field__input:hover {
border-colorrgb(100100100);
background-colorrgb(656565);
}

.mode-selector > .unity-base-field__input:focus {
border-colorrgb(0122204);
}

.input-field {
flex-grow1;
min-height36px;
max-height100px;
background-colorrgb(606060);
border-colorrgb(808080);
border-width1px;
border-radius4px;
padding8px10px;
colorrgb(204204204);
font-size13px;
margin-right8px;
flex-shrink1;
}

.input-field:focus {
border-colorrgb(0122204);
border-width1px;
background-colorrgb(505050);
}

.input-field > .unity-base-text-field__input {
background-color: transparent;
border-width0;
padding0;
colorrgb(204204204);
white-space: normal;
}

/* 发送按钮 */
.send-button {
background-colorrgb(0122204);
border-radius4px;
padding8px20px;
colorrgb(255255255);
font-size13px;
border-width0;
min-width60px;
height36px;
flex-shrink0;
align-self: flex-start;
}

.send-button:hover {
background-colorrgb(28151234);
}

.send-button:active {
background-colorrgb(0100170);
}

.send-button:disabled {
background-colorrgb(606060);
colorrgb(120120120);
}

/* 状态栏样式 */
.status-bar {
background-colorrgb(0122204);
border-top-width0;
padding-left12px;
padding-right12px;
padding-top3px;
padding-bottom3px;
flex-direction: row;
justify-content: space-between;
height22px;
}

.status-label {
font-size11px;
    -unity-font-style: normal;
colorrgb(255255255);
}

/* 工具执行指示器容器 */
.tool-indicator {
background-colorrgba(106153850.15);
border-left-width3px;
border-left-colorrgb(10615385);
padding6px12px;
margin0;
flex-direction: row;
align-items: center;
}

.tool-indicator-label {
font-size12px;
colorrgb(180180180);
margin-left4px;
}

/* 进度条 */
.progress-bar {
height2px;
background-colorrgb(0122204);
margin-top0;
}

/* Todo 消息容器 */
.todo-message-container {
background-colorrgba(106153850.08);
border-left-width3px;
border-left-colorrgb(10615385);
}

.todo-list-content {
padding4px0;
}

.todo-status-mark {
font-size12px;
colorrgb(180180180);
font-family: monospace;
}

.todo-item-text {
font-size12px;
colorrgb(204204204);
white-space: normal;
line-height1.4;
}

.todo-progress-text {
font-size11px;
colorrgb(140140140);
margin-top4px;
}

ClaudeCodeWindow.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Anthropic;
using Anthropic.Models.Messages;
using Anthropic.Core;

namespaceClaudeCodeForUnity
{
publicclassClaudeCodeWindow : EditorWindow
    {
private ScrollView _chatScrollView;
private TextField _inputField;
private Button _sendButton;
private StatusBar _statusBar;
private VisualElement _toolIndicator;
private DropdownField _modeSelector;
private VisualElement _currentTodoMessage;
private VisualElement _currentTaskMessage;
private DropdownField _sessionSelector;

private CancellationTokenSource _cancellationTokenSource;
privatebool _isTaskRunning;

private AnthropicClient _client;
private AgentLoop _agentLoop;
private BashAgent _bashAgent;
private FileAgent _fileAgent;
private TodoManager _todoManager;
private TaskManager _taskManager;
private SessionManager _sessionManager;
private ToolManager _toolManager;
private SubAgentManager _subAgentManager;
private BackgroundManager _backgroundTaskManager;
private SkillLoader _skillLoader;
private SkillAgent _skillAgent;
private TextManager _textManager;

privatestring _currentBaseUrl;
privatestring _currentApiKey;
privatestring _currentModel;
privatestring _currentSubAgentMode = "code";

        [MenuItem("Window/Claude Code")]
publicstaticvoidShowWindow()
        {
            ClaudeCodeWindow window = GetWindow<ClaudeCodeWindow>();
            window.titleContent = new GUIContent("Claude Code");
            window.minSize = new Vector2(500600);
        }

publicvoidCreateGUI()
        {
            _currentBaseUrl = ConfigManager.BaseUrl;
            _currentApiKey = ConfigManager.ApiKey;
            _currentModel = ConfigManager.Model;
            _textManager = new TextManager();

var root = rootVisualElement;
            root.style.flexDirection = FlexDirection.Column;
            root.style.flexGrow = 1;

var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Claude Code for Unity/Editor/UI/ClaudeCodeWindow.uss");
if (styleSheet != null)
                root.styleSheets.Add(styleSheet);

            root.Add(CreateHeader());
            root.Add(CreateSessionToolbar());
            _chatScrollView = CreateChatScrollView();
            root.Add(_chatScrollView);
            _toolIndicator = CreateToolIndicator();
            root.Add(_toolIndicator);
            root.Add(CreateInputContainer());
            _statusBar = new StatusBar();
            root.Add(_statusBar);

            InitializeModules();
            AddSystemMessage(_textManager.Get("welcome.message"));
            AddSystemMessage($"{_textManager.Get("welcome.model")}{_currentModel} | {_textManager.Get("welcome.directory")}{ConfigManager.WorkingDirectory}");
if (_sessionManager != null)
            {
                AddSystemMessage($"📂 {_textManager.Get("welcome.session")}{_sessionManager.CurrentSession.Name}");
                AddSystemMessage(_textManager.Get("welcome.session.hint"));
            }
            AddSystemMessage(_textManager.Get("welcome.tasks.hint"));
        }

privatevoidInitializeModules()
        {
try
            {
                _statusBar.UpdateStatus($"🔄 {_textManager.Get("status.initializing")}", StatusBar.StatusType.Info);
                _statusBar.UpdateConnectionStatus(false);

                _client = new AnthropicClient(new ClientOptions
                {
                    AuthToken = _currentApiKey,
                    BaseUrl = new Uri(_currentBaseUrl),
                    HttpClient = new System.Net.Http.HttpClient()
                });

var workingDirectory = ConfigManager.WorkingDirectory;
                _bashAgent = new BashAgent(workingDirectory);
                _fileAgent = new FileAgent(workingDirectory);
                _todoManager = new TodoManager();
                _todoManager.OnTodoListUpdated += OnTodoListUpdated;

                _sessionManager = new SessionManager(workingDirectory);
                _sessionManager.OnSessionCreated += OnSessionCreated;
                _sessionManager.OnSessionSwitched += OnSessionSwitched;
                _sessionManager.OnSessionUpdated += OnSessionUpdated;
                _sessionManager.OnTaskCreated += OnTaskCreatedInSession;
                _sessionManager.OnTaskUpdated += OnTaskUpdatedInSession;

                _taskManager = new TaskManager(_client, _currentModel, _todoManager, workingDirectory);
                _taskManager.OnCompactCompleted += OnCompactCompleted;

                _backgroundTaskManager = new BackgroundManager(workingDirectory);
                _skillLoader = new SkillLoader(ConfigManager.SkillsDirectory);
                _skillAgent = new SkillAgent(_skillLoader);
                _subAgentManager = new SubAgentManager(workingDirectory, _client, _currentModel);

                _toolManager = new ToolManager(workingDirectory, _bashAgent, _fileAgent, _todoManager,
                    _taskManager, _sessionManager, _subAgentManager, _backgroundTaskManager, _skillAgent);

var systemPrompt = CreateSystemPrompt();
                _agentLoop = new AgentLoop(_client, _currentModel, systemPrompt, _toolManager,
                    ToolManager.SkillType.Code, _backgroundTaskManager);
                _agentLoop.OnTokenUsageUpdated += OnTokenUsageUpdated;

                _statusBar.UpdateModel(_currentModel);
                _statusBar.UpdateConnectionStatus(true);
                _statusBar.UpdateTokenUsage(04096);
                _statusBar.ShowIdle();

                RefreshSessionSelector();
                RefreshTaskMessage();

                Debug.Log($"[Claude Code] Initialized - Session: {_sessionManager.CurrentSession.Name}, Model: {_currentModel}");
            }
catch (Exception ex)
            {
                Debug.LogError($"{_textManager.Get("error.init_failed")}{ex.Message}");
                Debug.LogException(ex);
                _statusBar.UpdateStatus($"{_textManager.Get("error.init_failed")}{ex.Message}", StatusBar.StatusType.Error);
                _statusBar.UpdateConnectionStatus(false, _textManager.Get("error.connection_failed"));
            }
        }

privatestringCreateSystemPrompt()
        {
var workingDirectory = ConfigManager.WorkingDirectory;
var sessionInfo = _sessionManager != null
                ? $"\n当前会话: {_sessionManager.CurrentSession.Name} (ID: {_sessionManager.CurrentSession.Id})"
                : "";

var skillInfo = _skillAgent != null && _skillAgent.SkillCount > 0
                ? $@"

**可用技能**(任务匹配时用 Skill 工具调用):
{_skillAgent.GetSkillDescriptions()}"

                : "";

return$@"你是一个位于 {workingDirectory} 的编程代理。{sessionInfo}

循环: 规划 -> 使用工具行动 -> 报告。
{skillInfo}

你可以使用以下工具:
- bash: 运行shell命令(快速命令)
- read_file: 读取文件内容
- write_file: 写入内容到文件
- edit_file: 替换文件中的精确文本
- TodoWrite: 更新短期任务列表(临时,压缩时清空)
- Task: 为专注的子任务生成子代理
- task_create: 创建持久化任务(自动关联到当前会话)
- task_update: 更新任务状态或依赖关系
- task_list: 列出当前会话的所有任务
- task_get: 获取任务详情
- background_run: 在后台运行长时间命令(如 build, test, install)
- check_background: 检查后台任务状态
- Skill: 加载特定领域的专业知识(当任务匹配技能描述时使用)

会话与任务系统:
- 每个会话有独立的任务列表
- 使用 task_create 创建的任务会自动关联到当前会话
- task_list 默认只显示当前会话的任务
- 切换会话时,会显示对应会话的任务

任务创建示例:
  task_create(subject=""实现新功能X"", description=""详细说明"")
  task_update(task_id=1, status=""in_progress"")
  task_update(task_id=2, status=""completed"")

任务状态流转:
  pending(待办) -> in_progress(进行中) -> completed(已完成)

后台任务使用:
- 对可能超过几秒的命令使用 background_run(如 build, test, npm install)
- 快速命令使用 bash
- 后台任务完成时,结果会自动注入对话(<background-results>)

技能使用:
- 当用户任务匹配技能描述时立即使用 Skill 工具
- 技能内容将注入对话,提供详细指令和最佳实践
- 技能不修改系统提示(保持 prompt cache 效率)

规则:
- 行动优先,不要只是解释。
- 不要凭空想象文件路径。如果不确定,先用 bash ls/find 查看。
- 最小化修改。不要过度工程化。
- 对多步骤计划,首先用 task_create 创建任务,然后逐步完成
- 完成后,总结做了什么改动。"
;
        }

#region UI创建方法

privatevoidUpdateAgentSkillType()
        {
if (_agentLoop == nullreturn;

var skillType = _currentSubAgentMode switch
            {
"explore" => ToolManager.SkillType.Explore,
"plan" => ToolManager.SkillType.Plan,
"code" => ToolManager.SkillType.Code,
                _ => ToolManager.SkillType.Code
            };

            _agentLoop.SetSkillType(skillType);
        }

private VisualElement CreateHeader()
        {
var header = new VisualElement();
            header.AddToClassList("header");

var titleLabel = new Label("Claude Code");
            titleLabel.AddToClassList("title");

            header.Add(titleLabel);
return header;
        }

private VisualElement CreateSessionToolbar()
        {
var toolbar = new VisualElement();
            toolbar.AddToClassList("session-toolbar");

// 会话图标
var sessionIcon = new Label("📂");
            sessionIcon.AddToClassList("session-icon");

// 会话选择器
            _sessionSelector = new DropdownField(_textManager.Get("window.session"));
            _sessionSelector.AddToClassList("session-selector");
            _sessionSelector.choices = new List<string> { _textManager.Get("window.session.default") };
            _sessionSelector.value = _textManager.Get("window.session.default");
            _sessionSelector.RegisterValueChangedCallback(evt =>
            {
                OnSessionDropdownChanged(evt.newValue);
            });

// 新建会话按钮
var newSessionButton = new Button(() => CreateNewSession());
            newSessionButton.text = "➕";
            newSessionButton.AddToClassList("session-button");

// 重命名会话按钮
var renameButton = new Button(() => RenameCurrentSession());
            renameButton.text = "✏️";
            renameButton.AddToClassList("session-button");

// 删除会话按钮
var deleteButton = new Button(() => DeleteCurrentSession());
            deleteButton.text = "🗑️";
            deleteButton.AddToClassList("session-button");

            toolbar.Add(sessionIcon);
            toolbar.Add(_sessionSelector);
            toolbar.Add(newSessionButton);
            toolbar.Add(renameButton);
            toolbar.Add(deleteButton);

return toolbar;
        }

privatevoidOnSessionDropdownChanged(string selectedValue)
        {
if (_sessionManager == nullreturn;

var sessions = _sessionManager.GetAllSessions();
var selectedIndex = _sessionSelector.choices.IndexOf(selectedValue);

if (selectedIndex >= 0 && selectedIndex < sessions.Count)
            {
var session = sessions[selectedIndex];
if (session.Id != _sessionManager.CurrentSession.Id)
                {
                    _sessionManager.SwitchToSession(session.Id);
                }
            }
        }

privatevoidCreateNewSession()
        {
if (_sessionManager == nullreturn;

var newName = $"Session {DateTime.Now:HH:mm:ss}";
var session = _sessionManager.CreateSession(newName);

// 自动切换到新会话
            _sessionManager.SwitchToSession(session.Id);
        }

privatevoidRenameCurrentSession()
        {
if (_sessionManager == nullreturn;

// 简单实现:通过系统消息提示
            AddSystemMessage(_textManager.Get("session.rename.hint"));
            AddSystemMessage(_textManager.Get("session.rename.hint2"));
        }

privatevoidDeleteCurrentSession()
        {
if (_sessionManager == nullreturn;

var currentSession = _sessionManager.CurrentSession;
if (currentSession == null)
            {
                AddSystemMessage(_textManager.Get("session.no_active"));
return;
            }

// 检查是否是最后一个会话
var allSessions = _sessionManager.GetAllSessions();
if (allSessions.Count <= 1)
            {
                AddSystemMessage(_textManager.Get("session.cannot_delete_last"));
return;
            }

// 确认删除
var sessionName = currentSession.Name;
var tasks = _sessionManager.ListTasks();
var taskCount = tasks.Count;

// 确认删除会话
var deleteSession = EditorUtility.DisplayDialog(
                _textManager.Get("session.delete.title"),
                _textManager.GetFormat("session.delete.message", sessionName, taskCount),
                _textManager.Get("session.delete.button_yes"),
                _textManager.Get("session.delete.button_cancel")
            );

if (!deleteSession)
            {
                AddSystemMessage(_textManager.Get("session.delete.cancelled"));
return;
            }

var sessionIdToDelete = currentSession.Id;

// 切换到另一个会话
var otherSession = allSessions.FirstOrDefault(s => s.Id != sessionIdToDelete);
if (otherSession != null)
            {
                _sessionManager.SwitchToSession(otherSession.Id);
            }

// 删除会话(会自动删除所有关联文件,包括任务)
if (_sessionManager.DeleteSession(sessionIdToDelete))
            {
                AddSystemMessage(_textManager.GetFormat("session.deleted", sessionName, taskCount));

// 刷新 UI
                RefreshSessionSelector();
                RefreshTaskMessage();
            }
else
            {
                AddSystemMessage(_textManager.GetFormat("session.delete_failed", sessionName));
            }
        }

private ScrollView CreateChatScrollView()
        {
var scrollView = new ScrollView(ScrollViewMode.Vertical);
            scrollView.AddToClassList("chat-scroll-view");
return scrollView;
        }

private VisualElement CreateToolIndicator()
        {
var container = new VisualElement();
            container.AddToClassList("tool-indicator");
            container.style.display = DisplayStyle.None; // 默认隐藏

// 绿点指示器
var dot = new VisualElement();
            dot.AddToClassList("tool-dot");

var label = new Label(_textManager.Get("tool.executing"));
            label.AddToClassList("tool-indicator-label");

            container.Add(dot);
            container.Add(label);
return container;
        }

privatevoidOnTodoListUpdated()
        {
            EditorApplication.delayCall += () =>
            {
if (_todoManager == nullreturn;

// 移除旧的 Todo 消息(如果存在)
if (_currentTodoMessage != null && _chatScrollView.Contains(_currentTodoMessage))
                {
                    _chatScrollView.Remove(_currentTodoMessage);
                }

// 添加新的 Todo 消息
                _currentTodoMessage = AddTodoMessage();
            };
        }

privatevoidOnTokenUsageUpdated(long inputTokens, long outputTokens, long totalTokens)
        {
            EditorApplication.delayCall += () =>
            {
if (_statusBar != null)
                {
// 假设最大 token 为 128k (128000)
                    _statusBar.UpdateTokenUsage((int)totalTokens, 128000);
                }
            };
        }

privatevoidOnTaskCreatedInSession(int _, int __, string ___)
        {
            EditorApplication.delayCall += () =>
            {
                RefreshTaskMessage();
            };
        }

privatevoidOnTaskUpdatedInSession(int _, int __, string ___)
        {
            EditorApplication.delayCall += () =>
            {
                RefreshTaskMessage();
            };
        }

privatevoidOnCompactCompleted(string transcriptPath, string summary)
        {
            EditorApplication.delayCall += () =>
            {
                AddSystemMessage(_textManager.GetFormat("compact.completed", transcriptPath));
                AddSystemMessage(_textManager.GetFormat("compact.summary", summary));
            };
        }

privatevoidOnSessionCreated(int _, string name)
        {
            EditorApplication.delayCall += () =>
            {
                AddSystemMessage(_textManager.GetFormat("session.created", name));
                RefreshSessionSelector();
            };
        }

privatevoidOnSessionSwitched(int _, string name)
        {
            EditorApplication.delayCall += () =>
            {
// 清空当前聊天显示
                _chatScrollView.Clear();

// 加载会话的对话历史(如果需要显示)
                AddSystemMessage(_textManager.GetFormat("session.switched", name));

// 刷新任务显示(显示当前会话的任务)
                RefreshTaskMessage();
            };
        }

privatevoidOnSessionUpdated(int _)
        {
            EditorApplication.delayCall += () =>
            {
                RefreshSessionSelector();
            };
        }


privatevoidRefreshSessionSelector()
        {
if (_sessionSelector == null || _sessionManager == nullreturn;

var sessions = _sessionManager.GetAllSessions();
var choices = sessions.Select(s => $"{s.Name} ({s.GetFormattedUpdatedAt()})").ToList();

            _sessionSelector.choices = choices;

var currentSession = _sessionManager.CurrentSession;
if (currentSession != null && choices.Count > 0)
            {
var currentChoice = $"{currentSession.Name} ({currentSession.GetFormattedUpdatedAt()})";
if (choices.Contains(currentChoice))
                {
                    _sessionSelector.value = currentChoice;
                }
            }
        }

privatevoidRefreshTaskMessage()
        {
if (_taskManager == nullreturn;

// 移除旧的任务消息
if (_currentTaskMessage != null && _chatScrollView != null && _chatScrollView.Contains(_currentTaskMessage))
            {
                _chatScrollView.Remove(_currentTaskMessage);
                _currentTaskMessage = null;
            }

// 添加新的任务消息
            _currentTaskMessage = AddTaskMessage();
        }

private VisualElement AddTodoMessage()
        {
if (_todoManager == null || _todoManager.Items.Count == 0)
returnnull;

var messageContainer = CreateMessageContainer("todo-message-container");

// 图标
var icon = new Label("📋");
            icon.AddToClassList("message-icon");

// 内容容器
var contentContainer = new VisualElement();
            contentContainer.AddToClassList("todo-list-content");
            contentContainer.style.flexGrow = 1;
            contentContainer.style.flexDirection = FlexDirection.Column;

var items = _todoManager.Items;

// 添加每个任务项
foreach (var item in items)
            {
var itemElement = new VisualElement();
                itemElement.style.flexDirection = FlexDirection.Row;
                itemElement.style.marginBottom = 4;

// 状态标记
var statusMark = item.Status switch
                {
"completed" => "[✓]",
"in_progress" => "[▶]",
                    _ => "[ ]"
                };

var statusLabel = new Label(statusMark);
                statusLabel.AddToClassList("todo-status-mark");
                statusLabel.style.marginRight = 6;
                statusLabel.style.minWidth = 30;

// 任务内容
var contentText = item.Status == "in_progress"
                    ? $"{item.Content} ← {item.ActiveForm}"
                    : item.Content;

var contentLabel = new Label(contentText);
                contentLabel.AddToClassList("todo-item-text");
                contentLabel.style.flexGrow = 1;

// 根据状态设置样式
if (item.Status == "completed")
                {
                    contentLabel.style.color = new Color(0.5f0.5f0.5f);
                    contentLabel.style.unityTextOutlineColor = new Color(0.4f0.4f0.4f);
                }
elseif (item.Status == "in_progress")
                {
                    contentLabel.style.color = new Color(0.7f0.7f1f);
                }

                itemElement.Add(statusLabel);
                itemElement.Add(contentLabel);
                contentContainer.Add(itemElement);
            }

// 进度统计
var completed = items.Count(t => t.Status == "completed");
var progressLabel = new Label($"\n({completed}/{items.Count} completed)");
            progressLabel.AddToClassList("todo-progress-text");
            progressLabel.style.color = new Color(0.6f0.6f0.6f);
            progressLabel.style.fontSize = 11;
            contentContainer.Add(progressLabel);

            messageContainer.Add(icon);
            messageContainer.Add(contentContainer);
            _chatScrollView.Add(messageContainer);
            ScrollToBottom();

return messageContainer;
        }

private VisualElement AddTaskMessage()
        {
if (_sessionManager == null)
returnnull;

// 从 SessionManager 获取当前会话的任务
var tasks = _sessionManager.ListTasks();
if (tasks == null || tasks.Count == 0)
returnnull;

var messageContainer = CreateMessageContainer("task-message-container");

// 图标
var icon = new Label("📁");
            icon.AddToClassList("message-icon");

// 内容容器
var contentContainer = new VisualElement();
            contentContainer.AddToClassList("task-list-content");
            contentContainer.style.flexGrow = 1;
            contentContainer.style.flexDirection = FlexDirection.Column;

// 标题
var titleLabel = new Label(_textManager.Get("task.list.persistent"));
            titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
            titleLabel.style.marginBottom = 4;
            titleLabel.style.color = new Color(0.8f0.8f1f);
            contentContainer.Add(titleLabel);

// 添加每个任务
foreach (var task in tasks)
            {
var itemElement = new VisualElement();
                itemElement.style.flexDirection = FlexDirection.Row;
                itemElement.style.marginBottom = 3;

// 状态标记
var marker = task.Status switch
                {
"pending" => "[ ]",
"in_progress" => "[>]",
"completed" => "[x]",
                    _ => "[?]"
                };

// 阻塞信息
var blockedInfo = task.BlockedBy != null && task.BlockedBy.Count > 0
                    ? $" (blocked by: [{string.Join(", ", task.BlockedBy)}])"
                    : "";

var taskText = $"{marker} #{task.Id}{task.Subject}{blockedInfo}";
var itemLabel = new Label(taskText);
                itemLabel.AddToClassList("task-item-text");
                itemLabel.style.flexGrow = 1;
                itemLabel.style.fontSize = 12;

// 根据状态着色
if (task.Status == "completed")
                {
                    itemLabel.style.color = new Color(0.5f0.8f0.5f); // 绿色 - 完成
                }
elseif (task.Status == "in_progress")
                {
                    itemLabel.style.color = new Color(0.7f0.7f1f); // 蓝色 - 进行中
                }
elseif (task.BlockedBy != null && task.BlockedBy.Count > 0)
                {
                    itemLabel.style.color = new Color(1f0.7f0.4f); // 橙色 - 阻塞
                }
else
                {
                    itemLabel.style.color = new Color(0.8f0.8f0.8f); // 白色 - 待办
                }

                itemElement.Add(itemLabel);
                contentContainer.Add(itemElement);
            }

// 统计信息
var stats = _sessionManager.GetTaskStats();
var statsLabel = new Label($"\n{stats}");
            statsLabel.style.color = new Color(0.6f0.6f0.6f);
            statsLabel.style.fontSize = 11;
            statsLabel.style.unityFontStyleAndWeight = FontStyle.Italic;
            contentContainer.Add(statsLabel);

            messageContainer.Add(icon);
            messageContainer.Add(contentContainer);
            _chatScrollView.Add(messageContainer);
            ScrollToBottom();

return messageContainer;
        }

private VisualElement CreateInputContainer()
        {
var inputContainer = new VisualElement();
            inputContainer.AddToClassList("input-container");

// 创建模式选择器区域(左下角)
var modeContainer = new VisualElement();
            modeContainer.AddToClassList("mode-selector-container");

var modeLabel = new Label(_textManager.Get("window.mode"));
            modeLabel.AddToClassList("mode-label");

            _modeSelector = new DropdownField();
            _modeSelector.AddToClassList("mode-selector");
            _modeSelector.choices = new List<string> { "explore""code""plan" };
            _modeSelector.value = _currentSubAgentMode;
            _modeSelector.RegisterValueChangedCallback(evt =>
            {
                _currentSubAgentMode = evt.newValue;
                UpdateAgentSkillType();
                Debug.Log($"SubAgent 模式已切换为: {_currentSubAgentMode}");
            });

            modeContainer.Add(modeLabel);
            modeContainer.Add(_modeSelector);

// 创建输入行(输入框 + 发送按钮)
var inputRow = new VisualElement();
            inputRow.AddToClassList("input-row");

            _inputField = new TextField();
            _inputField.multiline = true;
            _inputField.AddToClassList("input-field");
            _inputField.RegisterCallback<KeyDownEvent>(OnInputKeyDown);

            _sendButton = new Button(OnSendButtonClicked);
            _sendButton.text = "Send";
            _sendButton.AddToClassList("send-button");

            inputRow.Add(_inputField);
            inputRow.Add(_sendButton);

            inputContainer.Add(modeContainer);
            inputContainer.Add(inputRow);
return inputContainer;
        }

#endregion

#region 事件处理

privatevoidOnInputKeyDown(KeyDownEvent evt)
        {
// Ctrl+Enter 发送消息
if (evt.keyCode == KeyCode.Return && evt.ctrlKey)
            {
                OnSendButtonClicked();
                evt.StopPropagation();
            }
        }

privateasyncvoidOnSendButtonClicked()
        {
// 如果任务正在运行,停止任务
if (_isTaskRunning)
            {
                StopTask();
return;
            }

string userMessage = _inputField.value?.Trim();
if (string.IsNullOrEmpty(userMessage))
            {
return;
            }

// 清空输入框
            _inputField.value = string.Empty;

// 显示用户消息
            AddUserMessage(userMessage);

// 创建新的 CancellationTokenSource
            _cancellationTokenSource = new System.Threading.CancellationTokenSource();
            _isTaskRunning = true;

// 切换按钮为停止模式
            _sendButton.text = _textManager.Get("window.stop");
            _sendButton.style.backgroundColor = new Color(0.96f0.28f0.28f); // 红色

// 更新状态栏
            _statusBar.ShowThinking();

            VisualElement thinkingMessage = null;

try
            {
// 显示思考中的占位符
                thinkingMessage = AddThinkingMessage();

// 处理消息并获取响应(支持取消)
var response = await _agentLoop.ProcessMessageAsync(userMessage, _cancellationTokenSource.Token);

// 移除思考中的占位符
if (thinkingMessage != null && _chatScrollView.Contains(thinkingMessage))
                {
                    _chatScrollView.Remove(thinkingMessage);
                }

// 显示助手响应
                AddAssistantMessage(response);

// 更新状态栏
                _statusBar.ShowIdle();
            }
catch (OperationCanceledException)
            {
// 任务被取消
if (thinkingMessage != null && _chatScrollView.Contains(thinkingMessage))
                {
                    _chatScrollView.Remove(thinkingMessage);
                }

                AddSystemMessage($"⚠️ {_textManager.Get("status.task_cancelled")}");
                _statusBar.UpdateStatus(_textManager.Get("status.task_cancelled"), StatusBar.StatusType.Warning);
                Debug.Log("[AgentLoop] Task cancelled by user");
            }
catch (Exception ex)
            {
// 移除思考中的占位符
if (thinkingMessage != null && _chatScrollView.Contains(thinkingMessage))
                {
                    _chatScrollView.Remove(thinkingMessage);
                }

                AddErrorMessage($"{_textManager.Get("status.error")}{ex.Message}");
                Debug.LogException(ex);

// 更新状态栏显示错误
                _statusBar.UpdateStatus($"{_textManager.Get("status.error")}{ex.Message}", StatusBar.StatusType.Error);
            }
finally
            {
// 恢复发送按钮状态
                _isTaskRunning = false;
                _sendButton.text = _textManager.Get("window.send");
                _sendButton.style.backgroundColor = new Color(0f0.478f0.8f); // 蓝色
                _sendButton.SetEnabled(true);

// 清理 CancellationTokenSource
                _cancellationTokenSource?.Dispose();
                _cancellationTokenSource = null;

                _inputField.Focus();
            }
        }

privatevoidStopTask()
        {
if (_cancellationTokenSource != null && !_cancellationTokenSource.IsCancellationRequested)
            {
                Debug.Log("[ClaudeCodeWindow] Stopping task...");
                _cancellationTokenSource.Cancel();

// 立即更新 UI 状态
                _sendButton.text = "Stopping...";
                _sendButton.SetEnabled(false);
            }
        }

#endregion

#region 消息显示方法

privatevoidAddUserMessage(string message)
        {
var messageContainer = CreateMessageContainer("user-message");

// 用户图标
var icon = new Label("👤");
            icon.AddToClassList("message-icon");

var label = new Label(message);
            label.AddToClassList("message-content");

            messageContainer.Add(icon);
            messageContainer.Add(label);
            _chatScrollView.Add(messageContainer);
            ScrollToBottom();
        }

private VisualElement AddAssistantMessage(string message)
        {
var messageContainer = CreateMessageContainer("assistant-message");

// 助手图标
var icon = new Label("🤖");
            icon.AddToClassList("message-icon");

// 创建主容器(不带框)
var mainContainer = new VisualElement();
            mainContainer.style.flexDirection = FlexDirection.Column;
            mainContainer.style.flexGrow = 1;

// 解析 markdown 并添加内容(会自动分离普通文本和代码块)
            ParseMarkdownContent(mainContainer, message);

            messageContainer.Add(icon);
            messageContainer.Add(mainContainer);
            _chatScrollView.Add(messageContainer);
            ScrollToBottom();
return messageContainer;
        }

privatevoidParseMarkdownContent(VisualElement container, string message)
        {
// 检测是否包含 markdown 标题(以 # 开头的行)
var lines = message.Split('\n');
int markdownStartIndex = -1;

for (int i = 0; i < lines.Length; i++)
            {
var line = lines[i].TrimStart();
if (line.StartsWith("# "))
                {
                    markdownStartIndex = i;
break;
                }
            }

// 如果找到了 markdown 标题
if (markdownStartIndex > 0)
            {
// 前面的文字(说明文字,不加框)
var beforeLines = lines.Take(markdownStartIndex).ToArray();
var beforeText = string.Join("\n", beforeLines).Trim();
if (!string.IsNullOrEmpty(beforeText))
                {
var textLabel = new Label(beforeText);
                    textLabel.AddToClassList("message-content");
                    container.Add(textLabel);
                }

// 后面的 markdown 内容(加绿框)
var markdownLines = lines.Skip(markdownStartIndex).ToArray();
var markdownContent = string.Join("\n", markdownLines);

var markdownContainer = new VisualElement();
                markdownContainer.AddToClassList("assistant-content");

var scrollView = new ScrollView(ScrollViewMode.Vertical);
                scrollView.style.maxHeight = 600;
                scrollView.style.flexGrow = 1;

var codeLabel = new Label(markdownContent);
                codeLabel.AddToClassList("markdown-code-block");
                codeLabel.enableRichText = false;
                codeLabel.style.whiteSpace = WhiteSpace.Pre;

                scrollView.Add(codeLabel);
                markdownContainer.Add(scrollView);
                container.Add(markdownContainer);
            }
else
            {
// 没有 markdown 标题,使用旧逻辑处理代码块
var codeBlockPattern = @"```([\w]*)\s*\n([\s\S]*?)\n```";
var matches = System.Text.RegularExpressions.Regex.Matches(message, codeBlockPattern);

if (matches.Count == 0)
                {
// 没有代码块,直接显示普通文本
var textLabel = new Label(message);
                    textLabel.AddToClassList("message-content");
                    container.Add(textLabel);
return;
                }

int lastIndex = 0;
foreach (System.Text.RegularExpressions.Match match in matches)
                {
// 添加代码块之前的普通文本(不加框)
if (match.Index > lastIndex)
                    {
var beforeText = message[lastIndex..match.Index].Trim();
if (!string.IsNullOrEmpty(beforeText))
                        {
var textLabel = new Label(beforeText);
                            textLabel.AddToClassList("message-content");
                            container.Add(textLabel);
                        }
                    }

// 创建绿色框容器
var markdownContainer = new VisualElement();
                    markdownContainer.AddToClassList("assistant-content");

var codeContent = match.Groups[2].Value;

var scrollView = new ScrollView(ScrollViewMode.Vertical);
                    scrollView.style.maxHeight = 600;
                    scrollView.style.flexGrow = 1;

var codeLabel = new Label(codeContent);
                    codeLabel.AddToClassList("markdown-code-block");
                    codeLabel.enableRichText = false;
                    codeLabel.style.whiteSpace = WhiteSpace.Pre;

                    scrollView.Add(codeLabel);
                    markdownContainer.Add(scrollView);
                    container.Add(markdownContainer);

                    lastIndex = match.Index + match.Length;
                }

// 添加最后一个代码块之后的文本
if (lastIndex < message.Length)
                {
var afterText = message[lastIndex..].Trim();
if (!string.IsNullOrEmpty(afterText))
                    {
var textLabel = new Label(afterText);
                        textLabel.AddToClassList("message-content");
                        container.Add(textLabel);
                    }
                }
            }
        }

private VisualElement AddThinkingMessage()
        {
var messageContainer = CreateMessageContainer("thinking-message");

// 思考图标
var icon = new Label("💭");
            icon.AddToClassList("message-icon");

var label = new Label(_textManager.Get("status.thinking"));
            label.AddToClassList("message-content");

            messageContainer.Add(icon);
            messageContainer.Add(label);
            _chatScrollView.Add(messageContainer);
            ScrollToBottom();
return messageContainer;
        }

privatevoidAddSystemMessage(string message)
        {
var messageContainer = CreateMessageContainer("system-message");

// 系统图标
var icon = new Label("ℹ️");
            icon.AddToClassList("message-icon");

var label = new Label(message);
            label.AddToClassList("message-content");

            messageContainer.Add(icon);
            messageContainer.Add(label);
            _chatScrollView.Add(messageContainer);
            ScrollToBottom();
        }

privatevoidAddErrorMessage(string message)
        {
var messageContainer = CreateMessageContainer("error-message");

// 错误图标
var icon = new Label("❌");
            icon.AddToClassList("message-icon");

var label = new Label(message);
            label.AddToClassList("message-content");

            messageContainer.Add(icon);
            messageContainer.Add(label);
            _chatScrollView.Add(messageContainer);
            ScrollToBottom();
        }

private VisualElement CreateMessageContainer(string className)
        {
var container = new VisualElement();
            container.AddToClassList("message-container");
            container.AddToClassList(className);
return container;
        }

privatevoidScrollToBottom()
        {
// 延迟滚动以确保布局已更新
            EditorApplication.delayCall += () =>
            {
                _chatScrollView.scrollOffset = new Vector2(0, _chatScrollView.contentContainer.layout.height);
            };
        }

#endregion

privatevoidOnDestroy()
        {
if (_client != null)
            {
// 清理客户端资源
                _client.HttpClient?.Dispose();
            }

// 取消订阅事件
if (_todoManager != null)
            {
                _todoManager.OnTodoListUpdated -= OnTodoListUpdated;
            }

if (_agentLoop != null)
            {
                _agentLoop.OnTokenUsageUpdated -= OnTokenUsageUpdated;
            }
        }
    }
}

至此我们完成了一个极简的 Claude Code for Unity 的编写,结合之前的 MCP for Unity,你便可以在 Unity 中为所欲为了~

从 AI 工具的使用者到开发者,再到基建的搭建者——我们正在跨越最关键的一步。接下来,才是真正的硬核之旅:从零手搓大模型!(C++ 实现)

脚踢AI数学,手撕深度学习,生吞大模型……牛皮先吹下了,剩下的,就用代码和汗水来填坑吧!

已经开始带着六岁的小儿子学 C 语言编程了,小家伙还算感兴趣,磕磕绊绊也能写出第一个 “Hello World ”!

我这半吊子编程水平,总算有了传承的着落。,希望他今后在图形学、引擎、AI 领域早有所成。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » AI 编辑器 CC for Unity

猜你喜欢

  • 暂无文章