Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3943988
Terminal M0: libghostty-vt core, Unix PTY, ConPTY, view, pty host sel…
239573049 Oct 16, 2026
e5e1f7a
Point StreamMark submodule at the alpha.16 commit (8265089)
239573049 Oct 16, 2026
af6275c
Pty tests: check SIGPIPE via bash PIPESTATUS (dash has no trap -p)
239573049 Oct 16, 2026
89a8295
Terminal M1 (A): generated bindings, incremental frame capture, O5 ov…
239573049 Oct 16, 2026
c99f83f
Terminal M1 (B): effects, core-answered queries, command tracking; mo…
239573049 Oct 16, 2026
75c4f9d
Terminal M1 (C): input modes, key/mouse/focus encoding, paste
239573049 Oct 16, 2026
aa0affd
Terminal M1 (D): selection, viewport scroll, search, hyperlinks in fr…
239573049 Oct 16, 2026
40c31dc
Terminal M1 (E): bundled ConPTY layout, Windows foreground via Toolhe…
239573049 Oct 16, 2026
6ae7381
Terminal M1 (F): .grid snapshots, golden replays (xterm.js, Alacritty…
239573049 Oct 16, 2026
f1fb403
Windows PTY: close the read-thread handle and the process handle afte…
239573049 Oct 16, 2026
82ff8e0
Pty stress T12: take the handle baseline after a warm-up batch (threa…
239573049 Oct 16, 2026
1bd5a31
Terminal M2 (1-3): IPC protocol, PtyHostClient, host server with sess…
239573049 Oct 17, 2026
dab1490
Terminal M2 (4): discovery, detached start, singleton lock, grace per…
239573049 Oct 17, 2026
ade092a
Terminal M2 (5): revive persistence (trimmed snapshot + tail.vt fallb…
239573049 Oct 17, 2026
4b5fb73
Terminal M2 (6-7): env/log regression tests, foreign-build draining; …
239573049 Oct 17, 2026
0e7b586
Terminal M3 (first wiring): terminal tab in the workbench on the pty …
239573049 Oct 17, 2026
fe2845f
CI: run the terminal-related Desktop tests in the terminal workflow
239573049 Oct 17, 2026
51d63b0
Host tests: replica sink ignores callbacks after dispose (use-after-f…
239573049 Oct 17, 2026
db9e3b0
Protocol: drain queued control frames before closing (host/goodbye wa…
239573049 Oct 17, 2026
3983e1b
Terminal M3: IME (preedit at cursor, cursor rectangle), paste and clo…
239573049 Oct 17, 2026
04509c7
esctest runner: feed output byte by byte so a resize request (CSI 8 t…
239573049 Oct 17, 2026
a8cfdaa
Terminal M3: search bar (find, count, step, highlights)
239573049 Oct 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
185 changes: 185 additions & 0 deletions .github/workflows/terminal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# 终端组件(docs/terminal)的 CI:原生库(libghostty-vt、libtcodepty)→ 三平台测试 → AOT 发布与自检。
# 原生库在 Linux 上用 zig 交叉编译 linux / windows 目标;macOS 目标需要 Apple SDK,在 macOS runner 上构建。
name: terminal

on:
push:
branches: [main]
paths: [ "src/TCode.Terminal.**", "tests/TCode.Terminal.**", "native/**", "tools/terminal/**", ".github/workflows/terminal.yml", "Directory.*.props" ]
pull_request:
paths: [ "src/TCode.Terminal.**", "tests/TCode.Terminal.**", "native/**", "tools/terminal/**", ".github/workflows/terminal.yml", "Directory.*.props" ]
workflow_dispatch:
# 每晚:PTY 压力测试按文档规模(03 §14 T10–T14),差分测试扫 200 个种子,结构化 fuzz 跑 20 分钟
schedule:
- cron: "17 19 * * *"

permissions:
contents: read

env:
DOTNET_NOLOGO: "1"
DOTNET_CLI_TELEMETRY_OPTOUT: "1"

jobs:
native:
name: native (${{ matrix.host }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- host: linux
os: ubuntu-24.04
rids: linux-x64 linux-arm64 win-x64 win-arm64
pty_rids: linux-x64 linux-arm64
- host: macos
os: macos-15
rids: osx-arm64 osx-x64
pty_rids: osx-arm64 osx-x64
env:
ZIG_GLOBAL_CACHE_DIR: ${{ github.workspace }}/.zig-cache-global
steps:
- uses: actions/checkout@v4
- name: Install llvm-strip
if: matrix.host == 'linux'
run: sudo apt-get update -q && sudo apt-get install -y -q llvm
- name: Cache Zig toolchain and packages
uses: actions/cache@v4
with:
path: |
~/.cache/tcode/zig
${{ github.workspace }}/.zig-cache-global
key: zig-${{ matrix.host }}-${{ hashFiles('native/toolchain.json', 'native/ghostty/ghostty-upstream.json') }}
- name: Build libghostty-vt
run: for rid in ${{ matrix.rids }}; do tools/terminal/build-ghostty.sh "$rid"; done
- name: Build libtcodepty and spawn helper
run: for rid in ${{ matrix.pty_rids }}; do native/tcodepty/build.sh "$rid"; done
- name: Check Linux glibc floor (2.28)
if: matrix.host == 'linux'
run: |
for f in artifacts/native/linux-*/lib*.so; do
max=$(objdump -T "$f" | grep -o 'GLIBC_[0-9.]*' | sort -uV | tail -1)
echo "$f requires $max"
[ "$(printf '%s\nGLIBC_2.28\n' "$max" | sort -V | tail -1)" = "GLIBC_2.28" ]
done
- uses: actions/upload-artifact@v4
with:
name: native-${{ matrix.host }}
path: |
artifacts/native/*/*ghostty-vt.*
artifacts/native/*/*tcodepty.*
artifacts/native/*/tcode-pty-spawn
artifacts/native/*/ghostty-commit.txt
if-no-files-found: error

test:
name: test (${{ matrix.rid }})
needs: native
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
rid: linux-x64
- os: macos-15
rid: osx-arm64
- os: windows-2022
rid: win-x64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- uses: actions/download-artifact@v4
with:
pattern: native-*
path: artifacts/native
merge-multiple: true
- name: Restore executable bits
if: runner.os != 'Windows'
run: chmod +x artifacts/native/*/tcode-pty-spawn
- name: Install vim (self-test exercises a full-screen program)
if: runner.os == 'Linux'
run: sudo apt-get install -y -q vim
- name: Test core
run: dotnet test tests/TCode.Terminal.Core.Tests
env:
TCODE_DIFF_SEEDS: ${{ github.event_name == 'schedule' && '200' || '' }}
TCODE_FUZZ_MINUTES: ${{ github.event_name == 'schedule' && '20' || '' }}
- name: Test PTY
run: dotnet test tests/TCode.Terminal.Pty.Tests --logger "console;verbosity=normal"
env:
TCODE_PTY_STRESS: ${{ github.event_name == 'schedule' && '1' || '' }}
- name: Test protocol
run: dotnet test tests/TCode.Terminal.Protocol.Tests
- name: Test pty host (end to end)
run: dotnet test tests/TCode.Terminal.Host.Tests
- name: Test view (headless snapshots)
run: dotnet test tests/TCode.Terminal.Avalonia.Tests
- uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results-${{ matrix.rid }}
path: artifacts/test-results/
if-no-files-found: ignore
- uses: actions/upload-artifact@v4
if: always()
with:
name: snapshots-${{ matrix.rid }}
path: tests/TCode.Terminal.Avalonia.Tests/bin/Debug/net10.0/snapshots/
if-no-files-found: ignore
- name: AOT publish tcode-ptyhost (0 IL warnings)
shell: bash
run: |
dotnet publish src/TCode.Terminal.Host -c Release -r ${{ matrix.rid }} -o artifacts/publish/ptyhost 2>&1 | tee publish.log
if grep -E 'warning IL[0-9]{4}' publish.log; then echo "::error::AOT/trim warnings"; exit 1; fi
- name: Check ConPTY publish layout (03 §14 W-T6)
if: runner.os == 'Windows'
shell: bash
run: |
d=artifacts/publish/ptyhost
need="conpty.dll arm64/OpenConsole.exe"
[ "${{ matrix.rid }}" = win-x64 ] && need="$need x64/OpenConsole.exe"
for f in $need; do [ -f "$d/$f" ] || { echo "::error::missing $f"; exit 1; }; done
- name: Discovery tests against the AOT host (04 §15)
shell: bash
run: dotnet test tests/TCode.Terminal.Host.Tests --filter "FullyQualifiedName~DiscoveryTests"
env:
TCODE_PTYHOST_DIR: ${{ github.workspace }}/artifacts/publish/ptyhost
- name: Run AOT self-test
shell: bash
run: artifacts/publish/ptyhost/tcode-ptyhost --self-test

# esctest2(11 §1.1.4):真实 PTY + 一致性测试模式的核心。新增失败(单独重跑仍失败)阻断;已知失败见 known-failures.txt
esctest:
name: esctest2 (linux-x64)
needs: native
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- uses: actions/download-artifact@v4
with:
pattern: native-*
path: artifacts/native
merge-multiple: true
- name: Restore executable bits
run: chmod +x artifacts/native/*/tcode-pty-spawn
- name: Fetch esctest2 (pinned, sha256 checked)
id: fetch
run: echo "dir=$(tools/terminal/fetch-esctest.sh)" >> "$GITHUB_OUTPUT"
- name: Run esctest2
run: |
dotnet run --project tools/terminal/EscTestRunner -c Release -- \
--esctest "${{ steps.fetch.outputs.dir }}" \
--known tests/TCode.Terminal.Core.Tests/EscTest/known-failures.txt \
--log artifacts/esctest.log
- uses: actions/upload-artifact@v4
if: always()
with:
name: esctest-log
path: artifacts/esctest.log*
if-no-files-found: ignore
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ obj/
.idea/
*.user
.DS_Store

# 终端原生构建与上游源码(tools/terminal/*.sh)
artifacts/

# StreamMark 本地包(tools/streammark/pack.sh 生成)
packages/streammark/*.nupkg
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "external/StreamMark"]
path = external/StreamMark
url = https://github.com/239573049/StreamMark.git
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<PackageVersion Include="StreamMark.Theme.AtomUI" Version="$(StreamMarkVersion)" />
<!-- 网页工具:HTML 解析(web_fetch 的正文提取与 Markdown 转换为自写代码,见 docs/design/builtin-agent-tools.md §8) -->
<PackageVersion Include="AngleSharp" Version="1.8.4" />
<!-- 终端:随包 ConPTY(conpty.dll + OpenConsole.exe 成对升级,见 docs/terminal/12-build-and-ci.md §8.2) -->
<PackageVersion Include="Microsoft.Windows.Console.ConPTY" Version="1.25.260930003" />
<!-- 宿主 / 进程通信 -->
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.13" />
<!-- 测试 -->
Expand Down
10 changes: 10 additions & 0 deletions TCode.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@
<Project Path="src/TCode.Config/TCode.Config.csproj" />
<Project Path="src/TCode.Runtimes/TCode.Runtimes.csproj" />
<Project Path="src/TCode.Desktop/TCode.Desktop.csproj" />
<Project Path="src/TCode.Terminal.Core/TCode.Terminal.Core.csproj" />
<Project Path="src/TCode.Terminal.Pty/TCode.Terminal.Pty.csproj" />
<Project Path="src/TCode.Terminal.Protocol/TCode.Terminal.Protocol.csproj" />
<Project Path="src/TCode.Terminal.Host/TCode.Terminal.Host.csproj" />
<Project Path="src/TCode.Terminal.Avalonia/TCode.Terminal.Avalonia.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/TCode.Agent.Tests/TCode.Agent.Tests.csproj" />
<Project Path="tests/TCode.AgentHost.Tests/TCode.AgentHost.Tests.csproj" />
<Project Path="tests/TCode.Ai.Tests/TCode.Ai.Tests.csproj" />
<Project Path="tests/TCode.Runtimes.Tests/TCode.Runtimes.Tests.csproj" />
<Project Path="tests/TCode.Desktop.Tests/TCode.Desktop.Tests.csproj" />
<Project Path="tests/TCode.Terminal.Core.Tests/TCode.Terminal.Core.Tests.csproj" />
<Project Path="tests/TCode.Terminal.Pty.Tests/TCode.Terminal.Pty.Tests.csproj" />
<Project Path="tests/TCode.Terminal.Avalonia.Tests/TCode.Terminal.Avalonia.Tests.csproj" />
<Project Path="tests/TCode.Terminal.Protocol.Tests/TCode.Terminal.Protocol.Tests.csproj" />
<Project Path="tests/TCode.Terminal.Host.Tests/TCode.Terminal.Host.Tests.csproj" />
</Folder>
</Solution>
56 changes: 56 additions & 0 deletions docs/terminal/00-current-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# TCode 终端:现状分析

> 日期:2026-10-16。本文只描述仓库里已有的东西和缺口,不做选型;选型依据见 [research/](research/)。

## 1. 结论

TCode 目前**没有终端**:

- 没有 PTY;
- 没有 VT 解析或屏幕模型;
- 没有渲染控件;
- 没有终端配置。

能看到的只有工作台"+"菜单里的一个"终端"占位标签。和终端相关的现有代码都属于 Agent 的**非交互**命令执行(`shell -c`、重定向管道),它和交互式终端是两条不同的路径,不能直接复用为终端。

## 2. 已有代码清单

| 位置 | 内容 | 与终端的关系 |
|---|---|---|
| [WorkbenchViewModel.cs](../../src/TCode.Desktop/ViewModels/WorkbenchViewModel.cs) | `WorkbenchTabKind.Terminal`;`AddTool` 用 `PlaceholderTabViewModel` 打开占位标签 | `AddTool` 按 Kind 查找已有标签,**同一种工具只能开一个标签**。多终端需要改成按实例管理 |
| [MainWindow.axaml:692](../../src/TCode.Desktop/Views/MainWindow.axaml#L692) | `PlaceholderTabViewModel` 的 DataTemplate:图标 + 标题 + 描述 + "即将推出" | 终端控件接入后,换成专门的 `TerminalTabViewModel` 模板 |
| [strings.tsv:78](../../src/TCode.Desktop/Localization/strings.tsv#L78) | `TerminalDescription`:"终端将基于 Iciclecreek.Avalonia.Terminal(XTerm.NET + Porta.Pty)实现,并与 Agent 的 bash 工具共享会话" | 早期设想,未经调研,需要按最终选型改写 |
| [architecture.md §8](../architecture.md) | 路线图:"终端(Iciclecreek.Avalonia.Terminal)" | 同上 |
| [MainWindowSnapshotTests.cs:123](../../tests/TCode.Desktop.Tests/MainWindowSnapshotTests.cs#L123) | 截图测试会打开终端占位标签 | 终端控件接入后需要专门的截图测试 |
| [ShellResolver.cs](../../src/TCode.Agent.Coding/Tools/Shell/ShellResolver.cs) | Agent 用的 shell:Unix 取 `$SHELL`,但**只接受 bash / zsh**,否则 `/bin/bash` → `/bin/sh`;Windows 依次是 Git Bash → pwsh → Windows PowerShell,并带 `-NoProfile -NonInteractive` | 这套选择是为"让模型稳定执行命令"设计的,和"与系统终端一致"的要求相反:fish / nushell 用户会被换成 bash,Windows 默认也不是 PowerShell。交互式终端需要另写一套 profile 探测 |
| [BashTool.cs](../../src/TCode.Agent.Coding/Tools/Shell/BashTool.cs) | `Process` + 重定向 stdin/stdout/stderr,设置 `PAGER=cat`、`GIT_TERMINAL_PROMPT=0`、`TCODE_AGENT=1` | 没有 TTY,程序会关闭颜色和交互。"与 bash 工具共享会话"如果要做,需要另行设计(见 §4) |
| [ProcessRunner.cs](../../src/TCode.Agent.Coding/Processes/ProcessRunner.cs) | rg、git 等辅助命令的运行与进程树终止 | 无关;但说明 UI 和 Host 两个进程里都在用 `System.Diagnostics.Process`。自己管理 PTY 子进程时,要考虑与 .NET 运行时的 SIGCHLD 处理互相影响(见 research/02) |
| [ToolPresentation.cs](../../src/TCode.Desktop/ViewModels/ToolPresentation.cs) | 把 `bash` / `exec_command` / `run_terminal_cmd` / `terminal` 等工具名归为 Command 类 | 将来"在终端中打开 / 重新运行"的入口 |
| [AcpSession.cs:94](../../src/TCode.Runtimes/Acp/AcpSession.cs#L94) | ACP `initialize` 声明 `terminal:false`、`auth.terminal:false`;`ToolCallContent` 中的 `terminal` 显示为"[terminal output unavailable]" | [acp-v1.md](../protocols/acp-v1.md) §4.3 写明:等内嵌终端上线后,把 `auth.terminal` 改为 `true`,让需要在终端中登录的 Agent 可以在 TCode 内完成登录 |
| [CodexSession.cs:830](../../src/TCode.Runtimes/Codex/CodexSession.cs#L830) | Codex 的 `writeStdin` 审批显示为"Write to terminal";只处理了 `item/commandExecution/outputDelta`([第 423 行](../../src/TCode.Runtimes/Codex/CodexSession.cs#L423)),`terminalInteraction` 没有处理 | Codex 的交互式命令输出(含 ANSI)目前按纯文本处理 |
| [JsonRpcConnection.cs:82](../../src/TCode.Rpc.Contracts/JsonRpcConnection.cs#L82) | UI ↔ Host 是 stdio 上按行分帧的 JSON-RPC,发送队列是**无界** `Channel` | 如果把 PTY 放到 Host 进程,终端字节流走这条通道会有两个问题:JSON 里的 base64 开销,以及没有背压(`cat` 大文件时队列会无限增长)。见 research/06 |
| [settings.md](../settings.md) | `~/.tcode/settings.json`(JSONC + Schema + 热重载) | 终端配置应作为其中的 `terminal` 节,沿用同样的宽容解析和热重载规则 |

## 3. 缺口

| 层 | 需要的能力 | 现状 |
|---|---|---|
| 进程 / PTY | Unix:openpty + 控制终端 + 安全启动子进程;Windows:ConPTY;resize、退出码、杀进程树、前台进程与 cwd 查询 | 无 |
| 终端核心 | VT 解析状态机、主 / 备用屏幕、回滚、reflow、字符集、模式、应答(DA / CPR / DECRQM…)、Unicode 宽度与 grapheme | 无 |
| 渲染 | 等宽网格自绘、字体回退、宽字符 / emoji、box drawing、光标、选区、搜索高亮、HiDPI | 无。团队有 Avalonia 自绘的经验:[CodeView](../../src/TCode.Desktop/Controls/CodeView.cs),以及 StreamMark |
| 输入 | 键盘编码(legacy / Kitty)、IME 组合输入、鼠标上报、粘贴(bracketed paste)、与全局快捷键(⌘K 等)的冲突处理 | 无 |
| 系统一致性 | 按平台探测用户真正的 shell 与 profile(WSL / Git Bash / pwsh…)、login shell、环境变量、各平台的复制粘贴与 Option 键习惯、默认字体 | 只有 Agent 用的 `ShellResolver`,目标不同 |
| 配置 | profiles、字体、光标、回滚、键位、响铃、shell 集成… | 无 |
| 工作台 | 多个终端标签、分屏、标题跟随前台进程 / OSC、关闭确认 | 每种工具只能开一个占位标签 |
| 传输抽象 | 本地 PTY、SSH、WSL、`docker exec` 等共用一个连接接口 | 无 |
| 测试 | 字节流回放 → 屏幕快照断言;vttest / esctest;吞吐基准;截图测试 | 无 |

## 4. 与终端相关的后续集成点

这些不属于终端组件本身,但会影响它的接口设计,要在设计阶段预留:

1. **ACP 终端登录**:`auth.terminal:true` 之后,需要以"指定命令 + 参数 + 环境变量"启动一个终端,并拿到退出码([acp-v1.md](../protocols/acp-v1.md) §4)。终端 API 需要支持"运行一条命令并等待它退出",而不只是启动交互式 shell。
2. **Agent 命令在可见终端中运行**:占位文案中"与 bash 工具共享会话"的设想。可以做到的程度取决于 PTY 放在哪个进程(UI / AgentHost / 独立 pty host),见 research/06。
3. **路径与链接**:终端输出中的 `path:line:col` 需要在 TCode 工作台中打开。所以链接识别要有可插拔的 provider,不能写死成打开浏览器。
4. **SSH**:远程终端以后要能与远程文件树(SFTP)共用一条连接。所以传输层不能假设"连接 = 本地进程"。
5. **NativeAOT**:终端库的每个依赖都要通过 AOT 发布检查(0 个 IL 警告),并实际运行一次发布后的二进制。
Loading
Loading