# Terminal.Gui 终端 UI > 所属项目: free-code .NET 10 重写 > 所属模块: [UI 与扩展设计](UI与扩展设计.md) > 原始代码: `../../src/screens/REPL.tsx` + `../../src/components/`(50+ React/Ink 组件) > 交叉参考: [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md) --- ## 概述 本模块用 Terminal.Gui 重新实现原始项目的 React/Ink 终端 UI 层。原始 `REPL.tsx` 是约 800 行的 React 函数式组件,依赖 Ink 的虚拟 DOM 将 JSX 渲染到终端。.NET 重写将其拆分为 `TerminalApp`(应用入口与主题)和 `REPLScreen`(主交互窗口),并将 50+ Ink 组件中最关键的两个(`PermissionDialog`、`CompanionSpriteView`)映射为 Terminal.Gui 的 `Dialog` 和 `View` 子类。 --- ## 15.1 应用启动 `TerminalApp` 是整个 UI 的入口,对应原始 `cli.tsx` 到 `REPL.tsx` 的初始化链路。 ```csharp /// /// Terminal.Gui 应用入口 /// 对应原始 cli.tsx → REPL.tsx 的初始化 /// public static class TerminalApp { public static void Run(IHost host) { Application.Init(); Application.UseSystemConsole = true; // 全局主题 ApplyTheme(); // 创建主窗口 var top = Application.Top; var replScreen = new REPLScreen(host.Services); top.Add(replScreen); // 全局键绑定 Application.KeyBindings.Add(Key.Esc, Command.Quit); Application.KeyBindings.Add(Key.CtrlMask | Key.C, Command.Cancel); Application.Run(top); Application.Shutdown(); } private static void ApplyTheme() { var colors = Colors.ColorSchemes["Base"]; colors.Normal = new Attribute(Color.White, Color.Black); colors.Focus = new Attribute(Color.Green, Color.Black); colors.HotNormal = new Attribute(Color.BrightCyan, Color.Black); colors.HotFocus = new Attribute(Color.BrightGreen, Color.Black); } } ``` **设计意图** Terminal.Gui 的 `Application.Init()` 对应 Ink 的 `render()` 调用。`UseSystemConsole = true` 确保在大多数终端模拟器下使用原生控制台输出,避免 Terminal.Gui 的 curses 后端与某些终端的兼容性问题。主题颜色选取刻意与原始 Ink 样式保持一致:白色前景、黑色背景、绿色高亮。 --- ## 15.2 REPL 主屏幕 `REPLScreen` 对应原始 `REPL.tsx` 的全部交互逻辑,包括消息流渲染、斜杠命令路由和状态订阅。 ```csharp /// /// REPL 主屏幕 — Terminal.Gui 版本 /// 对应原始 REPL.tsx(~800 行 React/Ink 组件) /// public sealed class REPLScreen : Window { private readonly IQueryEngine _queryEngine; private readonly IAppStateStore _stateStore; private readonly ICommandRegistry _commands; private readonly IBackgroundTaskManager _taskManager; private readonly TextView _promptInput; private readonly ListView _messageList; private readonly StatusBar _statusBar; private readonly Label _spinner; private readonly IDisposable _stateSubscription; public REPLScreen(IServiceProvider services) : base("free-code") { _queryEngine = services.GetRequiredService(); _stateStore = services.GetRequiredService(); _commands = services.GetRequiredService(); _taskManager = services.GetRequiredService(); // 布局: 上方消息列表 + 下方输入框 + 底部状态栏 var messagesFrame = new FrameView("Messages") { X = 0, Y = 0, Width = Dim.Fill(), Height = Dim.Fill() - 5 }; _messageList = new ListView() { X = 0, Y = 0, Width = Dim.Fill(), Height = Dim.Fill(), }; messagesFrame.Add(_messageList); _spinner = new Label("●") { X = 0, Y = Pos.Bottom(messagesFrame), Visible = false }; _promptInput = new TextView() { X = 2, Y = Pos.Bottom(messagesFrame), Width = Dim.Fill(), Height = 4, }; _promptInput.KeyPress += OnPromptKeyPress; _statusBar = new StatusBar() { X = 0, Y = Pos.Bottom(_promptInput), }; Add(messagesFrame, _spinner, _promptInput, _statusBar); // 订阅状态变更 _stateSubscription = _stateStore.Subscribe(OnStateChanged); } private void OnPromptKeyPress(KeyEventEventArgs e) { // Enter 提交, Shift+Enter 换行 if (e.KeyEvent.Key == Key.Enter && !e.KeyEvent.IsShift) { e.Handled = true; var input = _promptInput.Text.ToString()?.Trim(); if (string.IsNullOrEmpty(input)) return; // 斜杠命令检测 if (input.StartsWith('/')) _ = HandleCommandAsync(input); else _ = HandleQueryAsync(input); _promptInput.Text = ""; } // Ctrl+C 取消 if (e.KeyEvent.Key == (Key.CtrlMask | Key.C)) { _ = _queryEngine.CancelAsync(); e.Handled = true; } } private async Task HandleQueryAsync(string input) { _spinner.Visible = true; _spinner.Text = "◉ Thinking..."; try { await foreach (var msg in _queryEngine.SubmitMessageAsync(input)) { switch (msg) { case SDKMessage.StreamingDelta delta: UpdateAssistantMessage(delta.Text); break; case SDKMessage.ToolUseStart tus: AddToolUseMessage(tus.Name, tus.Input); break; case SDKMessage.ToolUseResult tur: UpdateToolResult(tur.ToolUseId, tur.Output); break; case SDKMessage.AssistantMessage: _spinner.Visible = false; break; } } } catch (OperationCanceledException) { AddSystemMessage("[Cancelled]"); } _spinner.Visible = false; } private async Task HandleCommandAsync(string input) { var parts = input.Split(' ', 2); var commandName = parts[0][1..]; // 去掉 '/' var args = parts.Length > 1 ? parts[1] : null; var commands = await _commands.GetEnabledCommandsAsync(); var command = commands.FirstOrDefault(c => c.Name == commandName || c.Aliases?.Contains(commandName) == true); if (command == null) { AddSystemMessage($"Unknown command: /{commandName}"); return; } var result = await command.ExecuteAsync( new CommandContext(_stateStore, _queryEngine), args); if (result.Output != null) AddSystemMessage(result.Output); } private void OnStateChanged(AppState state) { // 更新状态栏(须切回主线程) Application.MainLoop.Invoke(() => { var model = state.MainLoopModelForSession ?? "claude-sonnet-4-6"; var bgTasks = state.Tasks.Values .Count(t => t.Status is TaskStatus.Running or TaskStatus.Pending); _statusBar.Text = $" Model: {model} | Tasks: {bgTasks} | Dir: {Directory.GetCurrentDirectory()}"; }); } } ``` **设计意图** 原始 `REPL.tsx` 通过 React 状态(`useState`/`useReducer`)驱动 UI 更新。`REPLScreen` 将这套推送模型转换为 `IAppStateStore.Subscribe` 订阅回调,并通过 `Application.MainLoop.Invoke` 确保 UI 变更在主线程执行,与 Ink 的 React reconciler 在语义上等价。 消息流处理通过 `await foreach` 消费 `IQueryEngine.SubmitMessageAsync` 返回的 `IAsyncEnumerable`,完整保留原始流式渲染语义。Ctrl+C 调用 `_queryEngine.CancelAsync()` 对应原始组件中的 `AbortController` 取消机制。 --- ## 15.3 组件库 对应原始 `../../src/components/` 目录中的 50+ React/Ink 组件。以下列出两个核心组件的 .NET 实现。 ### PermissionDialog ```csharp /// /// 权限对话框 — 用户审批工具使用 /// 对应原始 PermissionRequest 组件 /// public sealed class PermissionDialog : Dialog { public PermissionResponse Response { get; private set; } public PermissionDialog(string toolName, string description, string input) : base($"Permission: {toolName}", 60, 12) { var descLabel = new Label(description) { X = 1, Y = 1, Width = Dim.Fill() - 2 }; var inputLabel = new Label(Truncate(input, 200)) { X = 1, Y = 3, Width = Dim.Fill() - 2 }; var allowBtn = new Button("Allow (y)") { X = 5, Y = 8 }; var denyBtn = new Button("Deny (n)") { X = 25, Y = 8 }; var alwaysBtn = new Button("Always Allow (a)") { X = 40, Y = 8 }; allowBtn.Clicked += () => { Response = PermissionResponse.AllowOnce; RequestStop(); }; denyBtn.Clicked += () => { Response = PermissionResponse.Deny; RequestStop(); }; alwaysBtn.Clicked += () => { Response = PermissionResponse.AllowAlways; RequestStop(); }; Add(descLabel, inputLabel, allowBtn, denyBtn, alwaysBtn); } } ``` **设计意图** 原始 `PermissionRequest` 组件通过 React props 回调传递用户选择。`PermissionDialog` 改用 `PermissionResponse` 属性,调用方在 `Application.Run(dialog)` 返回后读取该属性,取得与原始回调语义等价的结果,但完全同步。 ### CompanionSpriteView ```csharp /// /// 同伴精灵渲染 — ASCII art sprite /// 对应原始 CompanionSprite 组件 /// public sealed class CompanionSpriteView : View { private readonly Companion _companion; public CompanionSpriteView(Companion companion) { _companion = companion; Width = 20; Height = 10; } public override void Draw() { base.Draw(); var sprite = CompanionSpriteRenderer.Render(_companion); Move(0, 0); for (var i = 0; i < sprite.Length; i++) { var line = sprite[i]; Driver.AddStr(line); Move(0, i + 1); } } } ``` **设计意图** 原始 `CompanionSprite` 是纯渲染组件,通过 Ink 的 `` 将 ASCII art 输出到终端。`CompanionSpriteView` 重写 `Draw()` 方法,用 Terminal.Gui 的 `Driver.AddStr` 逐行写入,保持与原始输出完全相同的视觉效果。`CompanionSpriteRenderer.Render` 静态方法封装精灵图案生成逻辑,独立于 UI 层测试。 --- ## 组件映射一览 | 原始 Ink 组件 | .NET Terminal.Gui 实现 | 说明 | |---|---|---| | `REPL.tsx` | `REPLScreen : Window` | 主屏幕 | | `PermissionRequest` | `PermissionDialog : Dialog` | 工具审批对话框 | | `CompanionSprite` | `CompanionSpriteView : View` | ASCII 精灵渲染 | | `StatusBar` | `StatusBar`(Terminal.Gui 内置)| 底部状态栏 | | `Spinner` | `Label`(动态文本)| 思考中指示器 | | `MessageList` | `ListView` | 消息历史列表 | | `PromptInput` | `TextView` | 多行输入框 | 其余 43+ 组件(`FileDiff`、`TokenBudget`、`HistoryPicker` 等)由对应的特性开关控制加载,在各自模块文档中描述。 --- ## 参考资料 - [UI 与扩展设计 — 总览](UI与扩展设计.md) - [核心模块设计 — 查询引擎 (QueryEngine)](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md) - [核心模块设计 — 命令系统](../核心模块设计/核心模块设计-命令系统.md) - [UI 与扩展设计 — 特性开关系统](UI与扩展设计-特性开关系统.md)