diff --git a/CMakeLists.txt b/CMakeLists.txt index 2454a7b8..6e7b3df0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,6 +134,10 @@ if(WIN32) add_executable(conpty_backend_probe experiments/conpty/backend_probe.cpp) termforge_warnings(conpty_backend_probe) target_compile_definitions(conpty_backend_probe PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00) + add_executable(conpty_resize_native_probe experiments/conpty/resize_native_probe.cpp) + termforge_warnings(conpty_resize_native_probe) + target_compile_definitions(conpty_resize_native_probe PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00) + target_link_libraries(conpty_resize_native_probe PRIVATE bcrypt) add_executable(presentation_probe experiments/presentation/presentation.cpp) termforge_warnings(presentation_probe) target_compile_definitions(presentation_probe PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN UNICODE _UNICODE) diff --git a/docs/EVIDENCE.md b/docs/EVIDENCE.md index 2614b33b..fefbba94 100644 --- a/docs/EVIDENCE.md +++ b/docs/EVIDENCE.md @@ -55,6 +55,7 @@ R1 固定总出口的源码为 `ce9b24d`,早期 `2dc8d6d` / `9b16acc` 仍分 | 实验输入 worker 持块与停止(#62) | [sender fence 合同](research/sender-fence.md)、[固定源码与实际字段](../experiments/shell/results/2026-10-26-sender-fence/validation.json) | `519a3f3` 的系统/官方各两进程、每次 stop/cancel/竞争,共 12 条实际 worker 记录,均 join/Job0;另有各一次原 focus 单测和各 2/2 纯 CTest。明确修正旧 never-read/output-first 不能保证 16KiB 的夹具假设,同 EXE 旧顺序仍红。实现仅在 PR #50 实验整合,main 未包含;probe 未链接候选 VT,纯 EOF 为模拟,#53 UI/3MB/768B/八会话及完整官方矩阵未验收。 | | 实验 UI 未完成粘贴(#53) | [机制与边界](research/ui-paste-fence.md)、[旧固定记录](../experiments/shell/results/2026-10-26-ui-paste-fence/validation.json)、[组合修复](research/resize-replay-cost.md)、[最终同源码记录](../experiments/shell/results/2026-10-26-replay-cost-and-close/validation.json) | `eae4b6a` 单窗口两轮、真实EOF/无GO负例及两轮原八会话data-only通过;行键复用及关闭期间不回复分别有诊断/因果回归,原completed/stage45/退出错误保留。完整CTest54/56仍有Vim59、resize43、main7/ui52与shell180秒期限;未计真实CF/IME、完整后端或性能通过。 | | Native Vim 捕获目录隔离 | [原因与复验](research/vim-fixture-isolation.md)、[固定原件审计](../experiments/conpty/results/2026-10-26-vim-fixture-isolation/validation.json) | 实验分支同旧 EXE 的兄弟运行一绿一红;修正新目录隔离后完整模块9/9。仅闭合 #52 native 夹具原因;GUI stage59 仍开放,未知与受扰动探针分列。 | +| 原生 viewport resize 与默认策略隔离(#51) | [原因与复现](research/native-viewport-resize.md)、[固定验证及六组合成原件](../experiments/conpty/results/2026-10-27-native-viewport-resize/validation.json) | 原 `.4` GUI stage43 与独立 native/replay 红;`.5` 原 resize/Unicode、历史/空格复制、六组 native 对照通过。默认策略填充误判经实际红绿修复;最终完整回归 54/56、12 项内部 skip,#52/#88 及此前 #87 的失败独立保留,不计完整后端或 WindTerm 等价。 | | 工作区几何与交互的固定记录 | [#38](https://github.com/JavaZeroo/termforge/issues/38)、[工作区几何与输出验证](../experiments/shell/results/2026-10-26-workspace-geometry/validation.json)、[resize 限定证据](../experiments/core/results/2026-10-26-resize-replay/validation.json) | 固定 27f376d 的客户区/布局/输出、默认/候选恢复及窄屏记录;其完整回归仍有 #36/#38 红项,不代表当前所有配置或 R2 通过。 | | 短窗口选区夹具与 hosted 四 shell 前置(#38) | [固定锚点回归](../experiments/shell/test_shell.py)、[准备入口](../tools/prepare_hosted_shells.py)、[夹具收尾合同](research/hosted-wsl-fixture-cleanup.md)、[云端结果与剩余阻塞](https://github.com/JavaZeroo/termforge/issues/38) | 短窗口保留相同选择断言;仅指定一次性 hosted VM 准备固定 Ubuntu。夹具强制收尾与产品自然退出分列,未知归属/回收仍失败。 | | 工作区恢复的运输完成端点(#38 夹具修正) | [限定合同](research/workspace-restore-transport-oracle.md)、[原失败及默认/候选验证](../experiments/shell/results/2026-10-26-workspace-transport-oracle/validation.json) | OSC 标题中的重复标记与 column-one CUP 误导旧端点;新判定保留完整 UTF-8、期限、负载及恢复断言。仅修夹具,不宣称修复产品恢复。 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 90d01e56..5724f5eb 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -14,7 +14,7 @@ |---|---|---| | 默认 main:自研核心 + 系统 ConPTY | 默认构建与已发布 Alpha 的基础路线,不需要 Rust | 已有 R1 限定验收;VT/Unicode 能力仍为有限子集,不能引用候选核心的字素或 TUI 结果 | | 显式候选 main:`termforge.4` + 系统 ConPTY | `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON`;按 [ADR-0003](adr/0003-core-reference-and-reuse.md)保留为可评估实验,正式核心选型待用户确认 | [固定来源](../experiments/candidates/alacritty/PROVENANCE.md)、[适配边界](../experiments/core/CANDIDATE.md)与 [#39 字素合同](research/streaming-graphemes.md);候选正确性通过不关闭系统 ConPTY 或 R2 父项 | -| 实验分支:`termforge.4` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),联合源码 `3e5b96d` 已重建并[冻结真实基线](research/joint-conpty-baseline.md),尚未合并或通过产品整体验收 | [#47](https://github.com/JavaZeroo/termforge/issues/47) 跟踪后端整合;终端 margin 与原生状态分列。[resize 残尾 #51](https://github.com/JavaZeroo/termforge/issues/51)、[Console-width Vim #52](https://github.com/JavaZeroo/termforge/issues/52)、[未完成粘贴 #53](https://github.com/JavaZeroo/termforge/issues/53)的[UI fence](research/ui-paste-fence.md)与[行键复用/关闭回复修复](research/resize-replay-cost.md)已在同源码验证单窗口两轮、真实EOF/无GO负例及两轮原八会话data-only;完整CTest仍54/56,resize43、Vim59、main7/ui52与shell180秒期限保留,不能据此验收整个后端;原completed与组合失败记录保留。worker gate [#62](https://github.com/JavaZeroo/termforge/issues/62) 已有[独立夹具修正与双后端实际 worker 验证](research/sender-fence.md),默认产品路径不启用 fence,也不替代 #53 的 UI 取消/关闭;工作区 [#63](https://github.com/JavaZeroo/termforge/issues/63) 回归仍开放 | +| 实验分支:`termforge.5` + 固定官方 ConPTY | [PR #50(Draft)](https://github.com/JavaZeroo/termforge/pull/50),`.4` 联合源码 `3e5b96d` 的[真实基线](research/joint-conpty-baseline.md)保留,尚未合并 main 或通过产品整体验收 | [#51 原生 viewport 修复](research/native-viewport-resize.md)保留历史并对齐宿主坐标,原 resize/Unicode 两轮及六组原生样本通过;最终完整回归 54/56,保留 [Vim #52](https://github.com/JavaZeroo/termforge/issues/52)、[恢复 stage1 #88](https://github.com/JavaZeroo/termforge/issues/88)和此前[八会话半行 #87](https://github.com/JavaZeroo/termforge/issues/87),同源码记录见该证据。[#53 UI fence](research/ui-paste-fence.md)与[行键复用/关闭回复](research/resize-replay-cost.md)、[#62 worker 夹具](research/sender-fence.md)各自限定通过。终端 margin 与原生状态分列,#47/#63 及完整后端仍开放;旧残尾、超时和组合失败记录保留 | 以下能力默认指已合并实现;某个配置的通过结果不能转用于另一个配置。R1 的真实系统 CF/拼音验收属于固定自研核心配置,不外推到候选 `.4` 或实验后端联合配置。 diff --git a/docs/PROVENANCE.md b/docs/PROVENANCE.md index c99fff4f..f962d8f9 100644 --- a/docs/PROVENANCE.md +++ b/docs/PROVENANCE.md @@ -24,7 +24,7 @@ - `experiments/core/unicode_width.hpp`:通过原创生成器从 Python 3.12.10 `unicodedata` 所带 Unicode 15.0.0 属性数据生成,含项目自己的宽度/拒绝策略。未复制 CPython 实现源码。Unicode 数据保留[官方许可](../experiments/core/UNICODE-LICENSE.txt)与归属,来源为 [Unicode](https://www.unicode.org/);正常构建使用固定生成文件。此数据不归项目 MIT 许可覆盖。 -- 外部核心候选当前使用 `alacritty_terminal 0.26.1-dev+termforge.4`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域和流式字素补丁;精确传递依赖及校验值由 `Cargo.lock` 固定。未复制上游 46 MiB 行为录制,候选不进入当前产品包;默认 CMake 不依赖 Rust,显式适配器构建使用 staticlib。原始 0.26.0 身份、当前来源、许可证和补丁清单见[候选来源台账](../experiments/candidates/alacritty/PROVENANCE.md)。 +- 外部核心候选当前使用 `alacritty_terminal 0.26.1-dev+termforge.5`(Apache-2.0),固定上游 commit `542147067dd016fc5f49120ef0ba605a98a4e3a7` 的 manifest/changelog/许可证与 `src/`,并保留 ED、稳定行身份/淘汰、安全 swap、每缓冲区滚动区域和流式字素补丁;精确传递依赖及校验值由 `Cargo.lock` 固定。未复制上游 46 MiB 行为录制,候选不进入当前产品包;默认 CMake 不依赖 Rust,显式适配器构建使用 staticlib。原始 0.26.0 身份、当前来源、许可证和补丁清单见[候选来源台账](../experiments/candidates/alacritty/PROVENANCE.md)。 - 候选新增精确锁定 `unicode-segmentation 1.13.3`(Unicode 17,MIT OR Apache-2.0),官方来源为 [unicode-rs](https://github.com/unicode-rs/unicode-segmentation) 与 [crates.io 固定版本](https://crates.io/crates/unicode-segmentation/1.13.3)。保留原依赖许可证;与既有 `unicode-width 0.2.2` 同使用 Unicode 17,默认自研核心仍使用 Unicode 15。版本、包哈希、MSRV、依赖成本和修改边界见[候选来源台账](../experiments/candidates/alacritty/PROVENANCE.md)。 diff --git a/docs/milestones/R2-local-terminal.md b/docs/milestones/R2-local-terminal.md index a1b81700..f4f47f93 100644 --- a/docs/milestones/R2-local-terminal.md +++ b/docs/milestones/R2-local-terminal.md @@ -44,7 +44,7 @@ flowchart LR ## 下一批的任务边界 -1. **在已冻结基线上逐项闭合后端回归。** #55 已交付含红的真实基线;接续 #51–#53 与 #63 的独立失败。#62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)不降低原 4MiB/16KiB/期限,#53 已接入默认关闭的 fence,[单窗口 UI/3MB/768B 与控制故障切片](../research/ui-paste-fence.md)及[同源码两轮原八会话data-only](../research/resize-replay-cost.md)已通过;完整官方回归仍保留resize/Vim/工作区失败和shell聚合期限,真实CF/IME组合独立验收。#51–#53 可以用原冻结输入独立定位,最终修复验收须在联合配置重建;一次未复现不自动构成因果修复。 +1. **在已冻结基线上逐项闭合后端回归。** #55 已交付含红的真实基线;接续 #51–#53、#63、[#87 八会话半行观测](https://github.com/JavaZeroo/termforge/issues/87)与 [#88 恢复 stage1](https://github.com/JavaZeroo/termforge/issues/88)的独立失败。#62 的[真实 writer 持块、取消与停止夹具](../research/sender-fence.md)不降低原 4MiB/16KiB/期限,#53 已接入默认关闭的 fence,[单窗口 UI/3MB/768B 与控制故障切片](../research/ui-paste-fence.md)及[同源码两轮原八会话data-only](../research/resize-replay-cost.md)已通过;[#51 原生 viewport 修复](../research/native-viewport-resize.md)的原 resize 与 Unicode 限定回归通过,完整官方回归中的 Vim/#88、此前 #87、历史工作区失败及聚合期限证据分别保留,真实 CF/IME 组合独立验收。#51–#53 可以用原冻结输入独立定位,最终修复验收须在联合配置重建;一次未复现不自动构成因果修复。 2. **并行处理原版对照、平台语义与性能。** [原生语义处理路径 #56](https://github.com/JavaZeroo/termforge/issues/56)先明确 #41 原合同需要什么、哪些路径可支持;保留必要的上游或独立实现工作,不用“平台不同”豁免缺口。#3/#4 优先建立原版 Windows 本地终端基线;#54 现在冻结重复方法与完成端点并定位吞吐成本,数值预算作为提案交用户确认,不等到 #34 才补性能。 3. **在主线推进独立行为合同。** #29 核对极窄/极短、软硬行、历史/淘汰、选择/Find、连续 resize 和 child ack;#30 核对 IME 组合/提交/取消与输出/resize/切换、应用键盘/粘贴接收、实际复制及同次八会话。已验收的 #39 只提供首批字素基础,最终配置的交叉验收仍需执行。 4. **补终端模式和应用矩阵。** #27 明确 DECOM/DECAWM、pending-wrap、保存恢复、异常序列与 margin×resize;#28 先固定应用版本/入口/动作及协议字节预期,再逐应用完成进入、操作、resize、退出、主屏恢复。Vim 的一个修复不替代完整 TUI 验收。 diff --git a/docs/research/native-viewport-resize.md b/docs/research/native-viewport-resize.md new file mode 100644 index 00000000..8214825a --- /dev/null +++ b/docs/research/native-viewport-resize.md @@ -0,0 +1,48 @@ +# 显式官方后端的原生 viewport resize + +本项处理 [#51](https://github.com/JavaZeroo/termforge/issues/51) 的连续 GUI resize 残尾。固定原基线为 `e0b4d67`、候选 `termforge.4`、官方 ConPTY `1.25.260930003`、Console glyph width flags `0x18`;修复为同一候选的 `.5`。默认 system 后端和正式核心选择不由此项决定。 + +## 观察与原因 + +原场景末屏出现 `TF_SIZE_100x28OW`,原断言要求整行恰为 `TF_SIZE_100x28`。原生 `ReadConsoleOutputW` 捕获在同一次失败中显示该行正确,候选屏幕却保留 `OW`。原运输与 resize 时序可以逐步重现失败;移除中间两次请求仍出现同样终态,所以不能仅归因于请求合并或遗漏 host resize。 + +候选的通用 reflow 会在增高或增宽合并行时把历史拉回可见区,并可能为了保持光标行而删掉底部行。原生控制台没有终端已经保留的历史;随后由 host 发送的绝对坐标更新仍针对其原生可见区。两套布局先分歧、再接受局部覆盖,造成旧文字残留。六组独立原生样本分别覆盖增高、可见软行合并、光标所在软行合并、历史/可见区续行边界和部分合并后的底行保留。 + +| 初始→目标(列×行) | 关注点 | 原生末屏(省略行尾填充) | 原生光标(0 起算) | +|---|---|---|---| +| 8×3→8×6 | 增高不拉回 H0/H1 | TOP / MID / BOT / 空 / 空 / 空 | 2,3 | +| 8×3→16×3 | 合并后不拉回 H1 | 123456789 / P> / 空 | 1,2 | +| 8×3→16×3 | 光标位于被合并行 | H2 / 123456789 / 空 | 1,9 | +| 8×3→5×3 | 历史续行不能改变原生可见区 | 90123 / 45678 / 90 | 2,2 | +| 8×4→10×4 | 部分合并不能丢 BOTTOM | H1 / ABCDEFGHIJ / KLMNO / BOTTOM | 1,9 | +| 8×3→16×3 | 增宽不跨历史边界合并 | 9012345678901234 / 567890 / 空 | 1,6 | + +这些是 ASCII 合成用例的原生快照,不是由候选算法计算的预期。原生采样会影响时序;微型探针不回答 CPR,固定 host 的有界等待亦影响时序。因此本记录不作为吞吐、交互延迟或原版性能证据。 + +## 实现边界 + +新增显式 `native_viewport` 策略,仅由官方后端的候选窗口选择。历史和可见区分别 reflow,增高/合并产生的空行置于可见区底部;保留光标、底行、历史身份和有界淘汰语义。跨边界续行保留逻辑换行元数据,以独立标记区分布局填充与文本;任何可变单元格访问都会使该标记失效。保留原跨度内的尾空格,保证完整复制及后续 resize 不增删空格。默认 `reflow_history`、系统候选和默认自研核心保持各自合同。 + +本项目独立实现这些修改,保留第三方许可与版本身份;不是恢复出的 WindTerm 源码,也不是确认 WindTerm 运行时一致。完整 resize/选择/Find、备用屏与模式交叉仍归 #29/#27,正式选型归 #34。 + +## 可复现入口 + +先按[显式官方后端说明](experimental-conpty-backend.md)构建候选配置。以下输出目录须不存在,后端目录必须是经过双 SHA 校验的固定包;探针不会下载或回退。 + +```powershell +cmake --build build/candidate --config Release --target conpty_resize_native_probe candidate_resize_replay candidate_core_test +python -m experiments.conpty.resize_native_run --exe build/candidate/bin/Release/conpty_resize_native_probe.exe --replay build/candidate/bin/Release/candidate_resize_replay.exe --backend build/candidate/bin/Release/conpty-backend --out local/resize-native-new +ctest --test-dir build/candidate -C Release -R '^candidate_(core|resize_replay)$' --output-on-failure +``` + +探针先等待监督器 GO 和自有 Job,再启动私有控制台子进程。每组保存源 VT、前后原生 UTF-16、真实 transport、resize plan、两策略末端状态、二进制/源码哈希和清理状态。child 必须确认真实新尺寸、自然退出 37;父级检查 EOF、无强制取消、无溢出和 Job 清空。内层等待最多 5 秒、外层 20 秒,失败不扩大预算。回归中的预期来自上表,默认策略对照的差异单列。 + +## 验证与限制 + +[固定验证记录](../../experiments/conpty/results/2026-10-27-native-viewport-resize/validation.json)区分原 `.4` 真失败、正式 `.5` 与早期临时探针。原 GUI 的精确 marker、Unicode、位置/历史、child ack、Job/desktop 回收断言及原期限保持;未删除 `OW`、改成子串、清屏或关闭 reflow。 + +诊断中有两组结果不进入因果证据:只保留错误字符串的缩减最终删掉全部 resize,不能作为此缺陷的最小复现;另一次用保留旧 mtime 的文件复制恢复源码,增量构建复用了旧修复二进制。后者已按实际二进制哈希排除,重新写入源码、实际编译后恢复了原 stage43 red。公开记录保留这些排除原因。 + +六组合成探针可公开复现;原 GUI 原件保留本地,只发布审核后的状态、哈希和固定断言结果。完整 R1/R2、实际呈现、真实 CF/IME 组合与 WindTerm 对照不从本项推导,`presented_done` 保持 null。其他真实失败由对应 issue 继续跟踪。 + +最终产品源 `92eac12` 的显式官方配置完整 CTest 为 54/56、411.59 秒,另有 12 项内部 skip;失败为 Vim stage59(#52)和第二轮工作区恢复 stage1(#88)。同次八会话工作流通过,但此前两次编号半行失败仍由 #87 跟踪。原 resize、resize-close 和 Unicode 检查通过只验收本项限定修复,不覆盖这些独立失败。 diff --git a/experiments/candidates/alacritty/Cargo.lock b/experiments/candidates/alacritty/Cargo.lock index 6501189a..7a002fd0 100644 --- a/experiments/candidates/alacritty/Cargo.lock +++ b/experiments/candidates/alacritty/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "alacritty_terminal" -version = "0.26.1-dev+termforge.4" +version = "0.26.1-dev+termforge.5" dependencies = [ "arrayvec", "base64", diff --git a/experiments/candidates/alacritty/PROVENANCE.md b/experiments/candidates/alacritty/PROVENANCE.md index 3470237b..ef53938f 100644 --- a/experiments/candidates/alacritty/PROVENANCE.md +++ b/experiments/candidates/alacritty/PROVENANCE.md @@ -1,6 +1,6 @@ # 固定候选来源与本地补丁 -当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.4`,不是 crates.io 原始 `0.26.0`。 +当前适配版本为 `alacritty_terminal 0.26.1-dev+termforge.5`,不是 crates.io 原始 `0.26.0`。 ## 上游身份 @@ -53,6 +53,15 @@ 完整控制/宽度/资源和字体策略见[流式字素合同](../../../docs/research/streaming-graphemes.md)。固定 WindTerm 的运行时字素/宽度仍未知。 +## termforge.5 补丁 + +1. `Term::resize_native_viewport` 与 Grid 的显式策略:增高在可见区底部补空行;增宽合并可见行时移动光标并保留底行;历史和原生可见区分别 reflow,不把历史拉进原生控制台的坐标。 +2. `src/grid/row.rs` 显式标记本策略产生的边界填充,clone/整行移动保留,mutable cell access、reset、拆分/合并和尺寸改变时失效;不从普通 `wrap_columns` 推断填充。保留历史跨边界的软换行及实际文本跨度,后续 reflow 只移除该标记指明的默认填充。C ABI 同步返回填充的未写格/空文本,并保留原文本跨度中的实际尾空格。 +3. 原 `Term::resize`、C ABI 和 C++ 默认策略继续采用 `reflow_history`。只有显式官方后端候选 `ShellWindow` 选择 `native_viewport`;新增 ABI 而不改变原函数签名。未新增第三方依赖。 +4. 独立原生 `ReadConsoleOutputW` 的六组合成样本、公开适配器回归与原 GUI 反馈见[原生 resize 对照](../../../docs/research/native-viewport-resize.md)。修改为本项目依据观察独立编写,未复制 WindTerm 或 Microsoft 私有代码;既有上游 Apache-2.0 归属保留。 + +这是固定后端的兼容实验,不是 WindTerm resize 等价或最终产品策略确认;#29/#34 继续承担完整合同及选型。 + ## 历史基线 #24–#26 的原始候选为 crates.io `0.26.0`,crate SHA-256 `bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59`,对应上游 commit `94e7c8874e526b1e67b349d9ba30ddf81669119e`。当时的 90 match + 5 reference_difference、project/interactive 计量和平台/维护审查保持原批次身份,不改写为 termforge.1/2 结果。 diff --git a/experiments/candidates/alacritty/README.md b/experiments/candidates/alacritty/README.md index c6c711a9..a4021b17 100644 --- a/experiments/candidates/alacritty/README.md +++ b/experiments/candidates/alacritty/README.md @@ -6,8 +6,8 @@ | 项目 | 当前固定值 | 来源 | |---|---|---| -| `alacritty_terminal` | `0.26.1-dev+termforge.4`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | -| 本地补丁 | ED1/ED2、稳定 Row ID/换行元数据/淘汰、安全完整 Row swap、每缓冲区滚动区域、流式字素与明确剪裁 | `vendor/alacritty_terminal/src/`;所有文件哈希进入结果 manifest | +| `alacritty_terminal` | `0.26.1-dev+termforge.5`,Apache-2.0 | [上游 commit `5421470`](https://github.com/alacritty/alacritty/commit/542147067dd016fc5f49120ef0ba605a98a4e3a7)与[来源/补丁台账](PROVENANCE.md) | +| 本地补丁 | ED1/ED2、稳定 Row ID/换行元数据/淘汰、安全完整 Row swap、每缓冲区滚动区域、流式字素与明确剪裁、显式原生 viewport resize | `vendor/alacritty_terminal/src/`;所有文件哈希进入结果 manifest | | 工具链 | Rust/cargo 1.92.0 | 本机与候选 CI 显式选择;当前锁定图最高声明 Rust 1.85 | | 传递依赖 | 59 个注册表包 + 根适配器与固定 vendor 两个 path crate,共 61 个包 | `Cargo.lock` 与 `cargo metadata --locked --format-version 1` | diff --git a/experiments/candidates/alacritty/include/termforge_candidate.h b/experiments/candidates/alacritty/include/termforge_candidate.h index 9ab501e9..70e6a421 100644 --- a/experiments/candidates/alacritty/include/termforge_candidate.h +++ b/experiments/candidates/alacritty/include/termforge_candidate.h @@ -38,6 +38,7 @@ void tf_terminal_free(void* terminal); int tf_terminal_feed(void* terminal, const uint8_t* data, size_t len, uint8_t* error, size_t error_capacity); int tf_terminal_finish(void* terminal, uint8_t* error, size_t error_capacity); int tf_terminal_resize(void* terminal, size_t rows, size_t columns, uint8_t* error, size_t error_capacity); +int tf_terminal_resize_native_viewport(void* terminal, size_t rows, size_t columns, uint8_t* error, size_t error_capacity); int tf_terminal_meta(void* terminal, TfTerminalMeta* output); int tf_terminal_row_meta(void* terminal, size_t index, TfRowMeta* output); int tf_terminal_cell_meta(void* terminal, size_t index, size_t column, TfCellMeta* output); diff --git a/experiments/candidates/alacritty/run.py b/experiments/candidates/alacritty/run.py index 7b7c609e..25bf3584 100644 --- a/experiments/candidates/alacritty/run.py +++ b/experiments/candidates/alacritty/run.py @@ -11,7 +11,7 @@ ROOT = Path(__file__).resolve().parents[3] HERE = Path(__file__).resolve().parent -VERSION = "0.26.1-dev+termforge.4" +VERSION = "0.26.1-dev+termforge.5" UPSTREAM_COMMIT = "542147067dd016fc5f49120ef0ba605a98a4e3a7" BASE_CRATE_SHA256 = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59" diff --git a/experiments/candidates/alacritty/src/ffi.rs b/experiments/candidates/alacritty/src/ffi.rs index 4f56d33f..1ae5ce4f 100644 --- a/experiments/candidates/alacritty/src/ffi.rs +++ b/experiments/candidates/alacritty/src/ffi.rs @@ -177,17 +177,39 @@ pub unsafe extern "C" fn tf_terminal_resize( columns: usize, error: *mut c_uchar, error_capacity: usize, +) -> c_int { + unsafe { resize_terminal(value, rows, columns, false, error, error_capacity) } +} + +unsafe fn resize_terminal( + value: *mut c_void, rows: usize, columns: usize, preserve_viewport: bool, + error: *mut c_uchar, error_capacity: usize, ) -> c_int { guard(error, error_capacity, || { if rows == 0 || rows > 200 || columns == 0 || columns > 500 { return Err("invalid candidate dimensions".into()); } let value = unsafe { terminal(value)? }; - value.term.resize(Size { columns, rows }); + if preserve_viewport { + value.term.resize_native_viewport(Size { columns, rows }); + } else { + value.term.resize(Size { columns, rows }); + } drain_events(value) }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn tf_terminal_resize_native_viewport( + value: *mut c_void, + rows: usize, + columns: usize, + error: *mut c_uchar, + error_capacity: usize, +) -> c_int { + unsafe { resize_terminal(value, rows, columns, true, error, error_capacity) } +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn tf_terminal_meta(value: *mut c_void, output: *mut TerminalMeta) -> c_int { if output.is_null() { @@ -253,6 +275,28 @@ fn cell_text(cell: &Cell) -> String { text } +fn wrap_padding(row: &alacritty_terminal::grid::Row, column: usize) -> bool { + // A short retained continuation can need physical padding to keep the + // native viewport boundary. Its wrap marker does not turn that padding + // into a written space. Other cell contents or attributes remain visible. + row.native_wrap_padding() && row.wrap_columns() > 0 && row.wrap_columns() < row.len() && column >= row.wrap_columns() + && row.last().is_some_and(|cell| cell.flags.contains(Flags::WRAPLINE)) + && row[Column(column)] == Cell { flags: Flags::WRAPLINE, ..Cell::default() } +} + +fn row_cell_text(row: &alacritty_terminal::grid::Row, column: usize) -> String { + if wrap_padding(row, column) { + return String::new(); + } + let cell = &row[Column(column)]; + // Moving a wrap marker onto padding must not make the original trailing + // spaces unwritten. They still belong to the retained logical span. + if row.native_wrap_padding() && column < row.wrap_columns() && cell.is_empty() { + return " ".into(); + } + cell_text(cell) +} + #[unsafe(no_mangle)] pub unsafe extern "C" fn tf_terminal_cell_meta( value: *mut c_void, @@ -270,7 +314,8 @@ pub unsafe extern "C" fn tf_terminal_cell_meta( } let cell = &row[Column(column)]; let _ = line; - let text = cell_text(cell); + let padding = wrap_padding(row, column); + let text = row_cell_text(row, column); let flags = cell.flags(); let result = CellMeta { width: if flags.contains(Flags::WIDE_CHAR) { @@ -284,8 +329,8 @@ pub unsafe extern "C" fn tf_terminal_cell_meta( bg: color(cell.bg), bold: flags.contains(Flags::BOLD), marks: cell.zerowidth().map_or(0, |marks| marks.len()), - written: !cell.is_empty() || !text.is_empty() || - flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER), + written: !padding && (!cell.is_empty() || !text.is_empty() || + flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)), text_len: text.len(), clipped: flags.contains(Flags::CLIPPED_GRAPHEME), grapheme_fragment: cell.grapheme_fragment(), @@ -312,7 +357,7 @@ pub unsafe extern "C" fn tf_terminal_cell_text( if column >= value.term.grid().columns() { return 1; } - let text = cell_text(&row[Column(column)]); + let text = row_cell_text(row, column); unsafe { *needed = text.len() }; if capacity < text.len() || (output.is_null() && !text.is_empty()) { return 1; diff --git a/experiments/candidates/alacritty/test_candidate.py b/experiments/candidates/alacritty/test_candidate.py index 096f8e4c..8e3f56a9 100644 --- a/experiments/candidates/alacritty/test_candidate.py +++ b/experiments/candidates/alacritty/test_candidate.py @@ -32,7 +32,7 @@ def test_fixed_streaming_grapheme_samples(self): self.assertFalse(summary["performance_measured"]) def test_identity_and_limitations_are_recorded(self): - self.assertEqual(self.summary["version"], "0.26.1-dev+termforge.4") + self.assertEqual(self.summary["version"], "0.26.1-dev+termforge.5") self.assertEqual(self.summary["license"], "Apache-2.0") self.assertEqual(self.summary["upstream_commit"], "542147067dd016fc5f49120ef0ba605a98a4e3a7") self.assertEqual(len(self.summary["trace_sha256"]), 64) diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml b/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml index 58192a19..0755589b 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alacritty_terminal" -version = "0.26.1-dev+termforge.4" +version = "0.26.1-dev+termforge.5" authors = ["Christian Duerr ", "Joe Wilm "] license = "Apache-2.0" description = "Library for writing terminal emulators" diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs index aa70c4ec..e1811d9a 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs @@ -15,19 +15,28 @@ impl Grid { where T: ResetDiscriminant, D: PartialEq, + { + self.resize_with_viewport(reflow, lines, columns, false); + } + + pub(crate) fn resize_with_viewport( + &mut self, reflow: bool, lines: usize, columns: usize, preserve_viewport: bool, + ) where + T: ResetDiscriminant, + D: PartialEq, { // Use empty template cell for resetting cells due to resize. let template = mem::take(&mut self.cursor.template); match self.lines.cmp(&lines) { - Ordering::Less => self.grow_lines(lines), + Ordering::Less => self.grow_lines(lines, preserve_viewport), Ordering::Greater => self.shrink_lines(lines), Ordering::Equal => (), } match self.columns.cmp(&columns) { - Ordering::Less => self.grow_columns(reflow, columns), - Ordering::Greater => self.shrink_columns(reflow, columns), + Ordering::Less => self.grow_columns(reflow, columns, preserve_viewport), + Ordering::Greater => self.shrink_columns(reflow, columns, preserve_viewport), Ordering::Equal => (), } @@ -40,11 +49,30 @@ impl Grid { /// Alacritty keeps the cursor at the bottom of the terminal as long as there /// is scrollback available. Once scrollback is exhausted, new lines are /// simply added to the bottom of the screen. - fn grow_lines(&mut self, target: usize) + fn grow_lines(&mut self, target: usize, preserve_viewport: bool) where T: ResetDiscriminant, D: PartialEq, { + if preserve_viewport { + let original_lines = self.lines; + let history_size = self.history_size(); + let added = target - original_lines; + self.raw.grow_visible_lines(target); + self.lines = target; + + // Storage growth exposes preceding history at the top. Move every + // retained row back to its original logical coordinate and append + // empty rows below the viewport without scrolling or eviction. + for row in (-(history_size as i32)..original_lines as i32).map(Line) { + self.raw.swap(row, row + added); + } + for row in (original_lines as i32..target as i32).map(Line) { + self.raw[row].renew_identity(); + self.raw[row].reset(&self.cursor.template); + } + return; + } let lines_added = target - self.lines; // Need to resize before updating buffer. @@ -97,8 +125,27 @@ impl Grid { self.lines = target; } + /// Remove only padding introduced at a retained/native boundary before a + /// later reflow. A cell with actual text or attributes prevents removal. + fn trim_reflow_padding(row: &mut Row) { + let used = row.wrap_columns(); + if !row.native_wrap_padding() || used == 0 || used >= row.len() { + return; + } + let empty = T::default(); + let mut marker = T::default(); + marker.flags_mut().insert(Flags::WRAPLINE); + let last = row.len() - 1; + if row[Column(last)] != marker || (used..last).any(|column| row[Column(column)] != empty) { + return; + } + row[Column(last)].flags_mut().remove(Flags::WRAPLINE); + row.shrink(used); + row[Column(used - 1)].flags_mut().insert(Flags::WRAPLINE); + } + /// Grow number of columns in each row, reflowing if necessary. - fn grow_columns(&mut self, reflow: bool, columns: usize) { + fn grow_columns(&mut self, reflow: bool, columns: usize, preserve_viewport: bool) { // Check if a row needs to be wrapped. let should_reflow = |row: &Row| -> bool { let len = Column(row.len()); @@ -109,6 +156,7 @@ impl Grid { let mut reversed: Vec> = Vec::with_capacity(self.raw.len()); let mut cursor_line_delta = 0; + let mut removed_visible = 0; // Remove the linewrap special case, by moving the cursor outside of the grid. if self.cursor.input_needs_wrap && reflow { @@ -119,9 +167,26 @@ impl Grid { let mut rows = self.raw.take_all(); for (i, mut row) in rows.drain(..).enumerate().rev() { + if preserve_viewport && i >= self.lines { + Self::trim_reflow_padding(&mut row); + } + // A native console cannot reflow text that only the terminal's + // scrollback retains. Preserve the continuation without merging + // the first visible row into its historical predecessor. + if preserve_viewport && i + 1 == self.lines { + if let Some(previous) = reversed.last_mut().filter(|row| should_reflow(row)) { + if let Some(cell) = previous.last_mut() { + cell.flags_mut().remove(Flags::WRAPLINE); + } + previous.set_wrap_columns(previous.len()); + previous.grow(columns); + previous[Column(columns - 1)].flags_mut().insert(Flags::WRAPLINE); + previous.mark_native_wrap_padding(); + } + } // Check if reflowing should be performed. let last_row = match reversed.last_mut() { - Some(last_row) if should_reflow(last_row) => last_row, + Some(last_row) if (!preserve_viewport || i + 1 != self.lines) && should_reflow(last_row) => last_row, _ => { reversed.push(row); continue; @@ -163,6 +228,9 @@ impl Grid { // Add removed cells to previous row and reflow content. last_row.append(&mut cells); + if preserve_viewport { + last_row.set_wrap_columns(last_row.len()); + } let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; @@ -182,11 +250,17 @@ impl Grid { let line_delta = self.cursor.point.line - target.line; if line_delta != 0 && row.is_clear() { + if preserve_viewport && i < self.lines { + removed_visible += 1; + } continue; } cursor_line_delta += line_delta.0 as usize; } else if row.is_clear() { + if preserve_viewport && i < self.lines { + removed_visible += 1; + } if i < self.display_offset { // Since we removed a line, rotate down the viewport. self.display_offset = self.display_offset.saturating_sub(1); @@ -209,6 +283,13 @@ impl Grid { reversed.push(row); } + // Merged visible rows become empty rows below the viewport. They do + // not create space into which historical rows may be pulled. + if removed_visible != 0 { + reversed.resize_with(reversed.len() + removed_visible, || Row::new(columns)); + self.cursor.point.line = max(self.cursor.point.line - removed_visible, Line(0)); + } + // Make sure we have at least the viewport filled. if reversed.len() < self.lines { let delta = (self.lines - reversed.len()) as i32; @@ -219,11 +300,17 @@ impl Grid { // Pull content down to put cursor in correct position, or move cursor up if there's no // more lines to delete below the cursor. if cursor_line_delta != 0 { - let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; - let available = min(cursor_buffer_line, reversed.len() - self.lines); - let overflow = cursor_line_delta.saturating_sub(available); - reversed.truncate(reversed.len() + overflow - cursor_line_delta); - self.cursor.point.line = max(self.cursor.point.line - overflow, Line(0)); + if preserve_viewport { + // A partial merge can move the cursor while both rows remain. + // No row below the cursor may be discarded to hold it still. + self.cursor.point.line = max(self.cursor.point.line - cursor_line_delta, Line(0)); + } else { + let cursor_buffer_line = self.lines - self.cursor.point.line.0 as usize - 1; + let available = min(cursor_buffer_line, reversed.len() - self.lines); + let overflow = cursor_line_delta.saturating_sub(available); + reversed.truncate(reversed.len() + overflow - cursor_line_delta); + self.cursor.point.line = max(self.cursor.point.line - overflow, Line(0)); + } } // Reverse iterator and fill all rows that are still too short. @@ -242,7 +329,7 @@ impl Grid { } /// Shrink number of columns in each row, reflowing if necessary. - fn shrink_columns(&mut self, reflow: bool, columns: usize) { + fn shrink_columns(&mut self, reflow: bool, columns: usize, preserve_viewport: bool) { self.columns = columns; // Remove the linewrap special case, by moving the cursor outside of the grid. @@ -256,6 +343,9 @@ impl Grid { let mut rows = self.raw.take_all(); for (i, mut row) in rows.drain(..).enumerate().rev() { + if preserve_viewport && i >= self.lines { + Self::trim_reflow_padding(&mut row); + } // Append lines left over from the previous row. if let Some(buffered) = buffered.take() { // Add a column for every cell added before the cursor, if it goes beyond the new @@ -287,6 +377,19 @@ impl Grid { Vec::new() } else { // Since it fits, just push the existing line without any reflow. + if preserve_viewport && row.len() < columns { + let used = row.len(); + let wraps = row.last().is_some_and(|cell| cell.flags().contains(Flags::WRAPLINE)); + if wraps { + row[Column(used - 1)].flags_mut().remove(Flags::WRAPLINE); + } + row.grow(columns); + if wraps { + row[Column(columns - 1)].flags_mut().insert(Flags::WRAPLINE); + row.set_wrap_columns(used); + row.mark_native_wrap_padding(); + } + } new_raw.push(row); break; } @@ -344,11 +447,13 @@ impl Grid { cell.flags_mut().insert(Flags::WRAPLINE); } + let viewport_boundary = preserve_viewport && i == self.lines && wrapped.last() + .is_some_and(|cell| cell.flags().contains(Flags::WRAPLINE)); if wrapped .last() .map(|c| c.flags().contains(Flags::WRAPLINE) && i >= 1) .unwrap_or(false) - && wrapped.len() < columns + && wrapped.len() < columns && !viewport_boundary { // Make sure previous wrap flag doesn't linger around. if let Some(cell) = wrapped.last_mut() { @@ -376,10 +481,24 @@ impl Grid { // Make sure new row is at least as long as new width. let occ = wrapped.len(); + if viewport_boundary && occ < columns { + if let Some(cell) = wrapped.last_mut() { + cell.flags_mut().remove(Flags::WRAPLINE); + } + } if occ < columns { wrapped.resize_with(columns, T::default); } + if viewport_boundary && occ < columns { + wrapped[columns - 1].flags_mut().insert(Flags::WRAPLINE); + } row = Row::from_vec(wrapped, occ); + if viewport_boundary && occ < columns { + // Padding keeps physical columns aligned with the + // native viewport, but is not part of the logical line. + row.set_wrap_columns(occ); + row.mark_native_wrap_padding(); + } if i < self.display_offset { // Since we added a new line, rotate up the viewport. diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs index d0e3fbdb..ab304efe 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs @@ -37,6 +37,10 @@ pub struct Row { hard_break: bool, #[cfg_attr(feature = "serde", serde(skip))] wrap_columns: usize, + /// Explicit padding at a retained/native viewport boundary. Any mutable + /// cell access invalidates it; ordinary wrap_columns never implies padding. + #[cfg_attr(feature = "serde", serde(skip))] + native_wrap_padding: bool, } impl Default for Row { @@ -55,7 +59,7 @@ impl Row { /// Create a new terminal row. pub fn new(columns: usize) -> Row { let inner = iter::repeat_with(T::default).take(columns).collect(); - Row { inner, occ: 0, id: next_row_id(), hard_break: false, wrap_columns: 0 } + Row { inner, occ: 0, id: next_row_id(), hard_break: false, wrap_columns: 0, native_wrap_padding: false } } /// Increase the number of columns in the row. @@ -65,6 +69,7 @@ impl Row { return; } + self.native_wrap_padding = false; self.inner.resize_with(columns, T::default); } @@ -79,6 +84,7 @@ impl Row { return None; } + self.native_wrap_padding = false; // Split off cells for a new row. let mut new_row = self.inner.split_off(columns); let index = new_row.iter().rposition(|c| !c.is_empty()).map_or(0, |i| i + 1); @@ -112,6 +118,7 @@ impl Row { self.occ = 0; self.hard_break = false; self.wrap_columns = 0; + self.native_wrap_padding = false; } } @@ -119,7 +126,7 @@ impl Row { impl Row { #[inline] pub fn from_vec(vec: Vec, occ: usize) -> Row { - Row { inner: vec, occ, id: next_row_id(), hard_break: false, wrap_columns: 0 } + Row { inner: vec, occ, id: next_row_id(), hard_break: false, wrap_columns: 0, native_wrap_padding: false } } #[inline] @@ -137,6 +144,16 @@ impl Row { self.wrap_columns } + #[inline] + pub fn native_wrap_padding(&self) -> bool { + self.native_wrap_padding + } + + #[inline] + pub(crate) fn mark_native_wrap_padding(&mut self) { + self.native_wrap_padding = true; + } + #[inline] pub(crate) fn renew_identity(&mut self) { self.id = next_row_id(); @@ -144,11 +161,13 @@ impl Row { #[inline] pub(crate) fn set_hard_break(&mut self, value: bool) { + self.native_wrap_padding = false; self.hard_break = value; } #[inline] pub(crate) fn set_wrap_columns(&mut self, value: usize) { + self.native_wrap_padding = false; self.wrap_columns = value; } @@ -164,6 +183,7 @@ impl Row { #[inline] pub fn last_mut(&mut self) -> Option<&mut T> { + self.native_wrap_padding = false; self.occ = self.inner.len(); self.inner.last_mut() } @@ -173,12 +193,14 @@ impl Row { where T: GridCell, { + self.native_wrap_padding = false; self.occ += vec.len(); self.inner.append(vec); } #[inline] pub fn append_front(&mut self, mut vec: Vec) { + self.native_wrap_padding = false; self.occ += vec.len(); vec.append(&mut self.inner); @@ -196,6 +218,7 @@ impl Row { #[inline] pub fn front_split_off(&mut self, at: usize) -> Vec { + self.native_wrap_padding = false; self.occ = self.occ.saturating_sub(at); let mut split = self.inner.split_off(at); @@ -220,6 +243,7 @@ impl<'a, T> IntoIterator for &'a mut Row { #[inline] fn into_iter(self) -> slice::IterMut<'a, T> { + self.native_wrap_padding = false; self.occ = self.len(); self.inner.iter_mut() } @@ -237,6 +261,7 @@ impl Index for Row { impl IndexMut for Row { #[inline] fn index_mut(&mut self, index: Column) -> &mut T { + self.native_wrap_padding = false; self.occ = max(self.occ, *index + 1); &mut self.inner[index.0] } @@ -254,6 +279,7 @@ impl Index> for Row { impl IndexMut> for Row { #[inline] fn index_mut(&mut self, index: Range) -> &mut [T] { + self.native_wrap_padding = false; self.occ = max(self.occ, *index.end); &mut self.inner[(index.start.0)..(index.end.0)] } @@ -271,6 +297,7 @@ impl Index> for Row { impl IndexMut> for Row { #[inline] fn index_mut(&mut self, index: RangeTo) -> &mut [T] { + self.native_wrap_padding = false; self.occ = max(self.occ, *index.end); &mut self.inner[..(index.end.0)] } @@ -288,6 +315,7 @@ impl Index> for Row { impl IndexMut> for Row { #[inline] fn index_mut(&mut self, index: RangeFrom) -> &mut [T] { + self.native_wrap_padding = false; self.occ = self.len(); &mut self.inner[(index.start.0)..] } @@ -305,6 +333,7 @@ impl Index for Row { impl IndexMut for Row { #[inline] fn index_mut(&mut self, _: RangeFull) -> &mut [T] { + self.native_wrap_padding = false; self.occ = self.len(); &mut self.inner[..] } @@ -322,6 +351,7 @@ impl Index> for Row { impl IndexMut> for Row { #[inline] fn index_mut(&mut self, index: RangeToInclusive) -> &mut [T] { + self.native_wrap_padding = false; self.occ = max(self.occ, *index.end + 1); &mut self.inner[..=(index.end.0)] } diff --git a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs index e0139580..eead143b 100644 --- a/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs +++ b/experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs @@ -665,6 +665,16 @@ impl Term { /// Resize terminal to new dimensions. pub fn resize(&mut self, size: S) { + self.resize_with_viewport(size, false); + } + + /// Resize against a native console whose viewport has no scrollback. + /// Retain terminal history while keeping it outside native coordinates. + pub fn resize_native_viewport(&mut self, size: S) { + self.resize_with_viewport(size, true); + } + + fn resize_with_viewport(&mut self, size: S, preserve_viewport: bool) { self.seal_grapheme(); let old_cols = self.columns(); let old_lines = self.screen_lines(); @@ -684,11 +694,14 @@ impl Term { let mut delta = num_lines as i32 - old_lines as i32; let min_delta = cmp::min(0, num_lines as i32 - self.grid.cursor.point.line.0 - 1); delta = cmp::min(cmp::max(delta, min_delta), history_size as i32); + if preserve_viewport { + delta = cmp::min(delta, 0); + } self.vi_mode_cursor.point.line += delta; let is_alt = self.mode.contains(TermMode::ALT_SCREEN); - self.grid.resize(!is_alt, num_lines, num_cols); - self.inactive_grid.resize(is_alt, num_lines, num_cols); + self.grid.resize_with_viewport(!is_alt, num_lines, num_cols, preserve_viewport); + self.inactive_grid.resize_with_viewport(is_alt, num_lines, num_cols, preserve_viewport); // Invalidate selection and tabs only when necessary. if old_cols != num_cols { diff --git a/experiments/conpty/resize_native_probe.cpp b/experiments/conpty/resize_native_probe.cpp new file mode 100644 index 00000000..6bcf8db8 --- /dev/null +++ b/experiments/conpty/resize_native_probe.cpp @@ -0,0 +1,146 @@ +// Synthetic native resize observations; original implementation, MIT. +// No CPR reply is sent: the fixed host's bounded query wait affects timing. +#include "session.hpp" +#include +#include +#include + +namespace fs = std::filesystem; +namespace { +struct Case { + short columns, rows, new_columns, new_rows; + const char* text; +}; +constexpr Case cases[] = { + {8,3,8,6,"H0\r\nH1\r\nTOP\r\nMID\r\nBOT"}, + {8,3,16,3,"H0\r\nH1\r\n123456789\r\nP>"}, + {8,3,16,3,"H0\r\nH1\r\nH2\r\n123456789"}, + {8,3,5,3,"H0\r\n123456789012345678901234567890"}, + {8,4,10,4,"H0\r\nH1\r\nABCDEFGHIJKLMNO\r\nBOTTOM\x1b[3;2H"}, + {8,3,16,3,"H0\r\n123456789012345678901234567890"}, +}; + +void require(bool value, const char* message) { + if (!value) throw std::runtime_error(message); +} + +void save(const fs::path& path, const std::string& value) { + std::ofstream out(path, std::ios::binary); + out << value; + require(bool(out), "file write"); +} + +void output(const std::string& value) { + DWORD written = 0; + require(WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), value.data(), + static_cast(value.size()), &written, nullptr) && written == value.size(), + "console write"); +} + +void snapshot(const fs::path& path, const CONSOLE_SCREEN_BUFFER_INFO& info) { + require(info.dwSize.X > 0 && info.dwSize.X <= 100 && info.dwSize.Y > 0 && info.dwSize.Y <= 50, + "snapshot bounds"); + std::vector cells(info.dwSize.X * info.dwSize.Y); + SMALL_RECT region{0, 0, static_cast(info.dwSize.X - 1), static_cast(info.dwSize.Y - 1)}; + require(ReadConsoleOutputW(GetStdHandle(STD_OUTPUT_HANDLE), cells.data(), info.dwSize, {0,0}, ®ion), + "snapshot read"); + require(region.Left == 0 && region.Top == 0 && region.Right == info.dwSize.X - 1 && + region.Bottom == info.dwSize.Y - 1, "snapshot rectangle"); + std::ofstream out(path); + out << "{\"rows\":" << info.dwSize.Y << ",\"columns\":" << info.dwSize.X + << ",\"cursor\":[" << info.dwCursorPosition.Y << ',' << info.dwCursorPosition.X + << "],\"utf16_rows\":["; + for (int row = 0; row < info.dwSize.Y; ++row) { + if (row) out << ','; + out << '['; + for (int column = 0; column < info.dwSize.X; ++column) { + if (column) out << ','; + out << static_cast(cells[row * info.dwSize.X + column].Char.UnicodeChar); + } + out << ']'; + } + out << "]}\n"; + require(bool(out), "snapshot save"); +} + +int child(const fs::path& out, const Case& item) { + const HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); + DWORD mode = 0; + require(GetConsoleMode(handle, &mode), "output mode"); + require(SetConsoleMode(handle, mode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING), + "set output mode"); + const std::string source = std::string("\x1b[2J\x1b[H") + item.text; + save(out / L"source.bin", source); + output(source); + CONSOLE_SCREEN_BUFFER_INFO before{}; + require(GetConsoleScreenBufferInfo(handle, &before), "before info"); + snapshot(out / L"native-before.json", before); + output("\x1b]0;TF_R51_READY\x07"); + const auto deadline = GetTickCount64() + 5000; + while (!fs::exists(out / L"go")) { + require(GetTickCount64() < deadline, "go deadline"); + Sleep(1); + } + CONSOLE_SCREEN_BUFFER_INFO after{}; + for (;;) { + require(GetConsoleScreenBufferInfo(handle, &after), "after info"); + if (after.dwSize.X == item.new_columns && after.dwSize.Y == item.new_rows) break; + require(GetTickCount64() < deadline, "resize ack deadline"); + Sleep(1); + } + snapshot(out / L"native-after.json", after); + output("\x1b]0;TF_R51_DONE\x07"); + return 37; +} +} // namespace + +int wmain(int argc, wchar_t** argv) { + try { + require(argc == 4 || argc == 5, "arguments"); + const std::wstring role = argv[1]; + require((role == L"--parent" && argc == 5) || (role == L"--child" && argc == 4), "role"); + const fs::path out = argv[2]; + const std::wstring index_text = argv[3]; + require(index_text.size() == 1 && index_text[0] >= L'0' && index_text[0] <= L'5', "case range"); + const int index = index_text[0] - L'0'; + const auto& item = cases[index]; + if (role == L"--parent") { + char gate[4]{}; + require(bool(std::cin.getline(gate, sizeof(gate))) && std::string(gate) == "GO", "supervisor gate"); + } + BOOL contained = FALSE; + require(IsProcessInJob(GetCurrentProcess(), nullptr, &contained) && contained, "owned Job required"); + if (role == L"--child") return child(out, item); + require(out.is_absolute() && fs::create_directory(out), "fresh absolute output"); + wchar_t image[32768]{}; + const auto length = GetModuleFileNameW(nullptr, image, 32768); + require(length > 0 && length < 32768, "image path"); + tf_conpty::Session session(tf_conpty::Backend::reviewed_side_by_side( + fs::path(argv[4]), tf_conpty::Backend::GlyphWidth::Console)); + session.start(image, L"--child \"" + out.wstring() + L"\" " + std::to_wstring(index), {}, + out.wstring(), {item.columns, item.rows}); + require(session.wait_text("TF_R51_READY\x07", 5000), "ready output"); + const auto offset = session.output().size(); + session.resize(item.new_columns, item.new_rows); + save(out / L"go", "go"); + require(session.wait_text("TF_R51_DONE\x07", 5000), "done output"); + require(session.wait_exit(5000) && session.exit_code == 37, "child natural exit"); + session.close(); + require(session.cleanup_complete && session.read_error == ERROR_BROKEN_PIPE && + !session.forced_reader_cancel && !session.forced_process_cancel && + !session.capture_overflow && session.active_after == 0, "EOF and cleanup"); + const auto bytes = session.output(); + save(out / L"transport.bin", bytes); + save(out / L"plan.txt", "R " + std::to_string(item.rows) + " " + std::to_string(item.columns) + + "\nF 0 " + std::to_string(offset) + "\nR " + std::to_string(item.new_rows) + " " + + std::to_string(item.new_columns) + "\nF " + std::to_string(offset) + " " + + std::to_string(bytes.size() - offset) + "\n"); + std::cout << "{\"case\":" << index << ",\"child_exit\":37,\"active_children\":" << session.active_after + << ",\"output_eof\":true,\"bytes\":" << bytes.size() << ",\"resize_offset\":" << offset + << ",\"creation_flags\":" << session.backend_creation_flags() << "}\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 2; + } +} diff --git a/experiments/conpty/resize_native_run.py b/experiments/conpty/resize_native_run.py new file mode 100644 index 00000000..c9029ed8 --- /dev/null +++ b/experiments/conpty/resize_native_run.py @@ -0,0 +1,215 @@ +"""Bounded synthetic native resize comparison for the explicit reviewed backend.""" +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +import hashlib +import json +from pathlib import Path +import platform +import subprocess +import time + +from experiments.conpty.margin_run import PACKAGE, backend_path +from experiments.conpty.run import OwnedJob + + +# Literal native observations, independent of candidate reflow. Each row is +# padded to the acknowledged native width before exact comparison. +CASES = ( + ((8, 3), (8, 6), "H0\r\nH1\r\nTOP\r\nMID\r\nBOT", ("TOP", "MID", "BOT", "", "", ""), (2, 3)), + ((8, 3), (16, 3), "H0\r\nH1\r\n123456789\r\nP>", ("123456789", "P>", ""), (1, 2)), + ((8, 3), (16, 3), "H0\r\nH1\r\nH2\r\n123456789", ("H2", "123456789", ""), (1, 9)), + ((8, 3), (5, 3), "H0\r\n123456789012345678901234567890", ("90123", "45678", "90"), (2, 2)), + ((8, 4), (10, 4), "H0\r\nH1\r\nABCDEFGHIJKLMNO\r\nBOTTOM\x1b[3;2H", + ("H1", "ABCDEFGHIJ", "KLMNO", "BOTTOM"), (1, 9)), + ((8, 3), (16, 3), "H0\r\n123456789012345678901234567890", ("9012345678901234", "567890", ""), (1, 6)), +) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def write_json(path, value): + path.write_text(json.dumps(value, indent=2) + "\n", encoding="utf-8") + + +def capture(exe, backend, out, index, *, gate=b"GO\n", timeout=20): + # The native parent creates this fresh case directory only after GO. + job = OwnedJob() + process = None + assigned = False + stdout = stderr = b"" + report = dict(case=index, exit_code=None, timed_out=False, forced_outer_cleanup=False, + job_after=None, job_after_forced_cleanup=None, process_reaped=False, + streams_drained=False, cleanup_errors=[]) + try: + process = subprocess.Popen([str(exe), "--parent", str(out), str(index), str(backend)], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + creationflags=subprocess.CREATE_NO_WINDOW) + job.assign(process.pid) + assigned = True + try: + stdout, stderr = process.communicate(gate, timeout=timeout) + report["streams_drained"] = True + except subprocess.TimeoutExpired: + report["timed_out"] = True + finally: + deadline = time.monotonic() + 5 + + def fault(code): + report["cleanup_errors"].append(code) + + def running(): + try: + return process.poll() is None + except Exception: + fault("process_poll") + return True + + def collect_process(): + nonlocal stdout, stderr + try: + stdout, stderr = process.communicate(timeout=max(0, deadline - time.monotonic())) + report["streams_drained"] = True + except Exception: + fault("process_communicate") + + try: + if process is not None and running(): + report["forced_outer_cleanup"] = True + try: + if assigned: + job.terminate() + else: + process.kill() # This root is gated before explicit ConPTY creation. + except Exception: + fault("process_terminate") + # A failed termination operation must not skip the wait. + collect_process() + if assigned: + try: + report["job_after"] = job.drain(timeout=max(0, deadline - time.monotonic())) + except Exception: + fault("job_accounting") + if report["job_after"] is not None and report["job_after"]["active"]: + report["forced_outer_cleanup"] = True + try: + job.terminate() + except Exception: + fault("job_terminate") + try: + report["job_after_forced_cleanup"] = job.drain(timeout=max(0, deadline - time.monotonic())) + if report["job_after_forced_cleanup"]["active"]: + fault("job_not_empty") + except Exception: + fault("job_accounting_after_termination") + finally: + try: + job.close() # Kill-on-close fallback, even after a failed terminate/wait. + except Exception: + fault("job_close") + if process is not None: + if running() or not report["streams_drained"]: + collect_process() + try: + report["exit_code"] = process.poll() + report["process_reaped"] = report["exit_code"] is not None + except Exception: + fault("process_final_poll") + try: + out.mkdir(exist_ok=True) + except Exception: + fault("evidence_directory") + for name, data in (("parent.stdout.bin", stdout), ("parent.stderr.bin", stderr)): + try: + (out / name).write_bytes(data) + except Exception: + fault("evidence_stream_write") + try: + write_json(out / "capture.json", report) + except Exception: + fault("evidence_report_write") + if (report["exit_code"] != 0 or report["timed_out"] or report["forced_outer_cleanup"] or + report["cleanup_errors"] or not report["process_reaped"] or not report["streams_drained"] or + report["job_after"] != {"active": 0, "total": 4, "terminated": 0}): + raise ValueError("native_capture_failed") + native = json.loads(stdout) + if (native["case"] != index or native["child_exit"] != 37 or native["active_children"] != 0 or + native["output_eof"] is not True or native["creation_flags"] != 24): + raise ValueError("native_lifecycle_failed") + report["native"] = native + return report + + +def inspect(out, replay, index): + initial, final, source, expected_rows, cursor = CASES[index] + if (out / "source.bin").read_bytes() != ("\x1b[2J\x1b[H" + source).encode("ascii"): + raise ValueError("source_changed") + for stage, dimensions in (("before", initial), ("after", final)): + native = json.loads((out / f"native-{stage}.json").read_text()) + columns, rows = dimensions + values = native["utf16_rows"] + if (native["columns"] != columns or native["rows"] != rows or len(values) != rows or + any(len(row) != columns or any(type(c) is not int or not 0 <= c < 128 for c in row) + for row in values)): + raise ValueError("native_snapshot_shape") + expected = [row.ljust(final[0]) for row in expected_rows] + if ["".join(map(chr, row)) for row in native["utf16_rows"]] != expected or native["cursor"] != list(cursor): + raise ValueError("native_observation_changed") + results = {} + for policy, options in (("reflow_history", []), ("native_viewport", ["--native-viewport"])): + run = subprocess.run([str(replay), str(out / "transport.bin"), str(out / "plan.txt"), *options], + capture_output=True, timeout=5) + (out / f"{policy}.states.jsonl").write_bytes(run.stdout) + (out / f"{policy}.stderr.bin").write_bytes(run.stderr) + if run.returncode != 0: + raise ValueError("candidate_replay_failed") + state = json.loads(run.stdout.splitlines()[-1])["state"] + results[policy] = dict(native_rows_equal=state["lines"] == expected, + native_cursor_equal=[state["row"], state["column"]] == list(cursor)) + return results + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--exe", type=Path, required=True) + parser.add_argument("--replay", type=Path, required=True) + parser.add_argument("--backend", type=Path, required=True) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + exe, replay = args.exe.resolve(strict=True), args.replay.resolve(strict=True) + backend = Path(backend_path(str(args.backend / "conpty.dll"))).parent + out = args.out.resolve() + out.mkdir(parents=True, exist_ok=False) + report = dict(schema=1, started_utc=datetime.now(timezone.utc).isoformat(), + windows=platform.version(), backend=PACKAGE, creation_flags=24, + probe_sha256=sha(exe), replay_sha256=sha(replay), + sources_sha256={name: sha(Path(__file__).with_name(name)) for name in + ("resize_native_run.py", "resize_native_probe.cpp", "session.hpp", "backend.hpp")}, + cpr_replies_sent=False, diagnostic_only=True, presented_done=None, cases=[]) + success = True + for index in range(len(CASES)): + case = out / f"case-{index}" + row = dict(case=index, passed=False) + try: + row.update(capture(exe, backend, case, index)) + row["replay"] = inspect(case, replay, index) + row["passed"] = all(row["replay"]["native_viewport"].values()) + except Exception: + # The owned case retains raw diagnostics; the summary contains no + # exception messages or absolute command paths for publication. + row["failure"] = "capture_or_comparison_failed" + row["files_sha256"] = {p.name: sha(p) for p in case.iterdir() if p.is_file()} if case.exists() else {} + report["cases"].append(row) + write_json(out / "validation.json", report) + print(json.dumps({"case": index, "passed": row["passed"], "replay": row.get("replay")}), flush=True) + if not row["passed"]: + success = False + break + return 0 if success else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/.gitattributes b/experiments/conpty/results/2026-10-27-native-viewport-resize/.gitattributes new file mode 100644 index 00000000..e5497457 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/.gitattributes @@ -0,0 +1,3 @@ +*.bin binary +case-*/*.json -text whitespace=cr-at-eol +case-*/plan.txt -text diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/native-after.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/native-after.json new file mode 100644 index 00000000..0a56c3f3 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/native-after.json @@ -0,0 +1 @@ +{"rows":6,"columns":8,"cursor":[2,3],"utf16_rows":[[84,79,80,32,32,32,32,32],[77,73,68,32,32,32,32,32],[66,79,84,32,32,32,32,32],[32,32,32,32,32,32,32,32],[32,32,32,32,32,32,32,32],[32,32,32,32,32,32,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/native-before.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/native-before.json new file mode 100644 index 00000000..cfe4597b --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/native-before.json @@ -0,0 +1 @@ +{"rows":3,"columns":8,"cursor":[2,3],"utf16_rows":[[84,79,80,32,32,32,32,32],[77,73,68,32,32,32,32,32],[66,79,84,32,32,32,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/plan.txt b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/plan.txt new file mode 100644 index 00000000..17bf5bba --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/plan.txt @@ -0,0 +1,4 @@ +R 3 8 +F 0 68 +R 6 8 +F 68 36 diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/source.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/source.bin new file mode 100644 index 00000000..946f45d7 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/source.bin @@ -0,0 +1,5 @@ +H0 +H1 +TOP +MID +BOT \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/transport.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/transport.bin new file mode 100644 index 00000000..139dc445 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-0/transport.bin @@ -0,0 +1,5 @@ +[?1004h[?9001hH0 +H1 +TOP +MID +BOT]0;TF_R51_READY]0;TF_R51_DONE[?1004l[?9001l \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/native-after.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/native-after.json new file mode 100644 index 00000000..ed9dc581 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/native-after.json @@ -0,0 +1 @@ +{"rows":3,"columns":16,"cursor":[1,2],"utf16_rows":[[49,50,51,52,53,54,55,56,57,32,32,32,32,32,32,32],[80,62,32,32,32,32,32,32,32,32,32,32,32,32,32,32],[32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/native-before.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/native-before.json new file mode 100644 index 00000000..fdf085c7 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/native-before.json @@ -0,0 +1 @@ +{"rows":3,"columns":8,"cursor":[2,2],"utf16_rows":[[49,50,51,52,53,54,55,56],[57,32,32,32,32,32,32,32],[80,62,32,32,32,32,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/plan.txt b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/plan.txt new file mode 100644 index 00000000..df3f3127 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/plan.txt @@ -0,0 +1,4 @@ +R 3 8 +F 0 68 +R 3 16 +F 68 36 diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/source.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/source.bin new file mode 100644 index 00000000..9c74f37a --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/source.bin @@ -0,0 +1,4 @@ +H0 +H1 +123456789 +P> \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/transport.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/transport.bin new file mode 100644 index 00000000..8703ae19 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-1/transport.bin @@ -0,0 +1,4 @@ +[?1004h[?9001hH0 +H1 +123456789 +P>]0;TF_R51_READY]0;TF_R51_DONE[?1004l[?9001l \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/native-after.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/native-after.json new file mode 100644 index 00000000..8df00f4a --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/native-after.json @@ -0,0 +1 @@ +{"rows":3,"columns":16,"cursor":[1,9],"utf16_rows":[[72,50,32,32,32,32,32,32,32,32,32,32,32,32,32,32],[49,50,51,52,53,54,55,56,57,32,32,32,32,32,32,32],[32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/native-before.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/native-before.json new file mode 100644 index 00000000..d08f7965 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/native-before.json @@ -0,0 +1 @@ +{"rows":3,"columns":8,"cursor":[2,1],"utf16_rows":[[72,50,32,32,32,32,32,32],[49,50,51,52,53,54,55,56],[57,32,32,32,32,32,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/plan.txt b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/plan.txt new file mode 100644 index 00000000..df3f3127 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/plan.txt @@ -0,0 +1,4 @@ +R 3 8 +F 0 68 +R 3 16 +F 68 36 diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/source.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/source.bin new file mode 100644 index 00000000..7c93df51 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/source.bin @@ -0,0 +1,4 @@ +H0 +H1 +H2 +123456789 \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/transport.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/transport.bin new file mode 100644 index 00000000..8ed3278d --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-2/transport.bin @@ -0,0 +1,4 @@ +[?1004h[?9001hH0 +H1 +H2 +123456789]0;TF_R51_READY]0;TF_R51_DONE[?1004l[?9001l \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/native-after.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/native-after.json new file mode 100644 index 00000000..6c5d6af4 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/native-after.json @@ -0,0 +1 @@ +{"rows":3,"columns":5,"cursor":[2,2],"utf16_rows":[[57,48,49,50,51],[52,53,54,55,56],[57,48,32,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/native-before.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/native-before.json new file mode 100644 index 00000000..64495c16 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/native-before.json @@ -0,0 +1 @@ +{"rows":3,"columns":8,"cursor":[2,6],"utf16_rows":[[57,48,49,50,51,52,53,54],[55,56,57,48,49,50,51,52],[53,54,55,56,57,48,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/plan.txt b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/plan.txt new file mode 100644 index 00000000..4cfb2775 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/plan.txt @@ -0,0 +1,4 @@ +R 3 8 +F 0 81 +R 3 5 +F 81 36 diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/source.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/source.bin new file mode 100644 index 00000000..ee823914 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/source.bin @@ -0,0 +1,2 @@ +H0 +123456789012345678901234567890 \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/transport.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/transport.bin new file mode 100644 index 00000000..3af93aa7 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-3/transport.bin @@ -0,0 +1,2 @@ +[?1004h[?9001hH0 +123456789012345678901234567890]0;TF_R51_READY]0;TF_R51_DONE[?1004l[?9001l \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/native-after.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/native-after.json new file mode 100644 index 00000000..ac791e80 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/native-after.json @@ -0,0 +1 @@ +{"rows":4,"columns":10,"cursor":[1,9],"utf16_rows":[[72,49,32,32,32,32,32,32,32,32],[65,66,67,68,69,70,71,72,73,74],[75,76,77,78,79,32,32,32,32,32],[66,79,84,84,79,77,32,32,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/native-before.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/native-before.json new file mode 100644 index 00000000..a64cfd8e --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/native-before.json @@ -0,0 +1 @@ +{"rows":4,"columns":8,"cursor":[2,1],"utf16_rows":[[72,49,32,32,32,32,32,32],[65,66,67,68,69,70,71,72],[73,74,75,76,77,78,79,32],[66,79,84,84,79,77,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/plan.txt b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/plan.txt new file mode 100644 index 00000000..6f4a91ed --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/plan.txt @@ -0,0 +1,4 @@ +R 4 8 +F 0 84 +R 4 10 +F 84 36 diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/source.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/source.bin new file mode 100644 index 00000000..819a1ebc --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/source.bin @@ -0,0 +1,4 @@ +H0 +H1 +ABCDEFGHIJKLMNO +BOTTOM \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/transport.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/transport.bin new file mode 100644 index 00000000..d1e064f7 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-4/transport.bin @@ -0,0 +1,4 @@ +[?1004h[?9001hH0 +H1 +ABCDEFGHIJKLMNO +BOTTOM]0;TF_R51_READY]0;TF_R51_DONE[?1004l[?9001l \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/native-after.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/native-after.json new file mode 100644 index 00000000..9c308e9f --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/native-after.json @@ -0,0 +1 @@ +{"rows":3,"columns":16,"cursor":[1,6],"utf16_rows":[[57,48,49,50,51,52,53,54,55,56,57,48,49,50,51,52],[53,54,55,56,57,48,32,32,32,32,32,32,32,32,32,32],[32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/native-before.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/native-before.json new file mode 100644 index 00000000..64495c16 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/native-before.json @@ -0,0 +1 @@ +{"rows":3,"columns":8,"cursor":[2,6],"utf16_rows":[[57,48,49,50,51,52,53,54],[55,56,57,48,49,50,51,52],[53,54,55,56,57,48,32,32]]} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/plan.txt b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/plan.txt new file mode 100644 index 00000000..e05767b9 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/plan.txt @@ -0,0 +1,4 @@ +R 3 8 +F 0 81 +R 3 16 +F 81 36 diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/source.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/source.bin new file mode 100644 index 00000000..ee823914 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/source.bin @@ -0,0 +1,2 @@ +H0 +123456789012345678901234567890 \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/transport.bin b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/transport.bin new file mode 100644 index 00000000..3af93aa7 --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/case-5/transport.bin @@ -0,0 +1,2 @@ +[?1004h[?9001hH0 +123456789012345678901234567890]0;TF_R51_READY]0;TF_R51_DONE[?1004l[?9001l \ No newline at end of file diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/native-cases.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/native-cases.json new file mode 100644 index 00000000..d6b2024c --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/native-cases.json @@ -0,0 +1,330 @@ +{ + "schema": 1, + "started_utc": "2026-10-26T20:46:12.976424+00:00", + "windows": "10.0.26200", + "backend": { + "name": "Microsoft.Windows.Console.ConPTY", + "version": "1.25.260930003", + "url": "https://github.com/microsoft/terminal/releases/download/v1.25.2733.0/Microsoft.Windows.Console.ConPTY.1.25.260930003.nupkg", + "bytes": 1750533, + "sha256": "6d6f8b008c655d814c498d8205d4012dcd4d0b6dfc1096461d0a185d1e3caf03", + "files": { + "conpty.dll": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "OpenConsole.exe": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + } + }, + "creation_flags": 24, + "probe_sha256": "a570b55f7ef0f65be01b827112905a223fb32f37a0f645ce121ead1ed6555f59", + "replay_sha256": "c4cf0d9f68bb5bd688e601b87025f3237c8659f4d80e96bd4f67fa463463a13f", + "sources_sha256": { + "resize_native_run.py": "a7fdc198a9fde7cff00ce1f74031b680e83d130e5034a612e576340e037fa1f2", + "resize_native_probe.cpp": "ea0f26b3cfc322929193a89ea72d3e987a2ec9f3fda87241989f508cd431336a", + "session.hpp": "6945e6ffba1d61e66266f867d3cd124eb63c39da37e27662a5eaf544f60ed96b", + "backend.hpp": "5ee3ab95dc4b9aefce23ff7900cda2ecdd9cd09d1b1044074de2f5e572c31489" + }, + "cpr_replies_sent": false, + "diagnostic_only": true, + "presented_done": null, + "cases": [ + { + "case": 0, + "passed": true, + "exit_code": 0, + "timed_out": false, + "forced_outer_cleanup": false, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "job_after_forced_cleanup": null, + "process_reaped": true, + "streams_drained": true, + "cleanup_errors": [], + "native": { + "case": 0, + "child_exit": 37, + "active_children": 0, + "output_eof": true, + "bytes": 104, + "resize_offset": 68, + "creation_flags": 24 + }, + "replay": { + "reflow_history": { + "native_rows_equal": false, + "native_cursor_equal": false + }, + "native_viewport": { + "native_rows_equal": true, + "native_cursor_equal": true + } + }, + "files_sha256": { + "capture.json": "e742d9102fcc20cce032abb28ccba4f638bcc1869c0fc0697654e3afaedb9a8c", + "go": "4cd0e21a9a0795a14ec9aa5f0e7d1abff0492565770e43eafdf1e3e8afed1f33", + "native-after.json": "f2a0180d23afbc07ced80b775df853d494f2c2a4dd51c81ed0292d5805cc2bec", + "native-before.json": "5d5d5aad2764de6a86bbc8ae7da966a482ee551d5dd15307d15ad4e9a21dfb46", + "native_viewport.states.jsonl": "a821dce1d169cd0890b914cffadf792855d8e54c0f1da8cc81b864946aee4b95", + "native_viewport.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stdout.bin": "9ee3d5cb19a9b033cd11cfdf85e9412d93b2859a8db3beb90489bab0d97e5739", + "plan.txt": "b168dd8cf0feafe6cdf16f210328cfa2d7345dbf15babe3d14db2a842009c625", + "reflow_history.states.jsonl": "84082bce93257353c0437e76c33d67049855b327875eda74207e51726154f478", + "reflow_history.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "source.bin": "eadc97ee8bfe9eeb07261af1dfb07977b1f87b2f7884d67014e87bf31d1a57e4", + "transport.bin": "3312e6a95a1897d575bddb046cb67a63e3dca21e8003ae0885fb993b359ddac6" + } + }, + { + "case": 1, + "passed": true, + "exit_code": 0, + "timed_out": false, + "forced_outer_cleanup": false, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "job_after_forced_cleanup": null, + "process_reaped": true, + "streams_drained": true, + "cleanup_errors": [], + "native": { + "case": 1, + "child_exit": 37, + "active_children": 0, + "output_eof": true, + "bytes": 104, + "resize_offset": 68, + "creation_flags": 24 + }, + "replay": { + "reflow_history": { + "native_rows_equal": false, + "native_cursor_equal": false + }, + "native_viewport": { + "native_rows_equal": true, + "native_cursor_equal": true + } + }, + "files_sha256": { + "capture.json": "8ab700b25ab6634943f120d5332266ca7a9e59099797ca4d6e461bce29e5705b", + "go": "4cd0e21a9a0795a14ec9aa5f0e7d1abff0492565770e43eafdf1e3e8afed1f33", + "native-after.json": "2b154f802963e89b5e9f11e8e538880f2630749d30f1f9632cb19f8e7d076da0", + "native-before.json": "60f447bfdf502c98b2ea3f54cb7a33fff2cac4b53d9762d57082ebe191c526c0", + "native_viewport.states.jsonl": "15a5ca914be318e387b5fc2ccf753ce0d70c131134b29eb976047b843ef62544", + "native_viewport.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stdout.bin": "019d2c63c311d523f438bfafbcdd61674c311c22270e8d867cb808b1e786d185", + "plan.txt": "c1bcd96e9c95e3c21cc4b408e9ea40cc5021f13b88a91d743efc22ac70003c8c", + "reflow_history.states.jsonl": "009dd85a20a9b9978de33275cce7eae305d8f7563a7f60bef8916a48f6d29a10", + "reflow_history.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "source.bin": "304dcdd68af70924614c1a1bc1aee10bfdc853403af70f8923806dacc297c770", + "transport.bin": "356ea650a7caad777e9b0f3b87771c380e2026c6257402cbb349b94d1d10719f" + } + }, + { + "case": 2, + "passed": true, + "exit_code": 0, + "timed_out": false, + "forced_outer_cleanup": false, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "job_after_forced_cleanup": null, + "process_reaped": true, + "streams_drained": true, + "cleanup_errors": [], + "native": { + "case": 2, + "child_exit": 37, + "active_children": 0, + "output_eof": true, + "bytes": 104, + "resize_offset": 68, + "creation_flags": 24 + }, + "replay": { + "reflow_history": { + "native_rows_equal": false, + "native_cursor_equal": false + }, + "native_viewport": { + "native_rows_equal": true, + "native_cursor_equal": true + } + }, + "files_sha256": { + "capture.json": "3fbf908c17f86f3691db9b6a89962c229ce5cb98ebb1ba12b9638692a641fbcd", + "go": "4cd0e21a9a0795a14ec9aa5f0e7d1abff0492565770e43eafdf1e3e8afed1f33", + "native-after.json": "22b0886b647a71cd91f317097748a798905e8ebf1ee43e4105ffb7321e09807a", + "native-before.json": "c99e33188a761a2d3e0368422692bcb3692ebd9c472fa944f5f9e8aff8b162e6", + "native_viewport.states.jsonl": "a2d0e1dd831c097f1e88a7f2aa9be865ae65e016889251fd79d1eb5d35a1720f", + "native_viewport.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stdout.bin": "440e0bd7905556ef498bd333555f4cba948ff7dc92c81531627b5305c3bfd492", + "plan.txt": "c1bcd96e9c95e3c21cc4b408e9ea40cc5021f13b88a91d743efc22ac70003c8c", + "reflow_history.states.jsonl": "9fc4978a7851c6eeb2156f0493b183a6555880d8b5fde3ba0a08135266cae1c0", + "reflow_history.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "source.bin": "e218a542b2a6b18a995b9ec2673f9c54d43a2422125e382a07fffa4e7c8c156a", + "transport.bin": "899fa3110b3bb20cea20f9f8b715fc23d48c9f92adbe3438f85b2c7ed2b5017f" + } + }, + { + "case": 3, + "passed": true, + "exit_code": 0, + "timed_out": false, + "forced_outer_cleanup": false, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "job_after_forced_cleanup": null, + "process_reaped": true, + "streams_drained": true, + "cleanup_errors": [], + "native": { + "case": 3, + "child_exit": 37, + "active_children": 0, + "output_eof": true, + "bytes": 117, + "resize_offset": 81, + "creation_flags": 24 + }, + "replay": { + "reflow_history": { + "native_rows_equal": false, + "native_cursor_equal": false + }, + "native_viewport": { + "native_rows_equal": true, + "native_cursor_equal": true + } + }, + "files_sha256": { + "capture.json": "e6e425e3a94f0d9eb7d9a78ba23937ef9a1043bfca06ccfc7d6422036a584865", + "go": "4cd0e21a9a0795a14ec9aa5f0e7d1abff0492565770e43eafdf1e3e8afed1f33", + "native-after.json": "84889f050c31419780cdaac335be9e3302de905f53b42d88dc8b41c9425297a6", + "native-before.json": "c3f863c9a6c386ca80398353201857bede3e94e7fc447d56b9496d174bc8eeb3", + "native_viewport.states.jsonl": "1d318b4c79c46ca3bbfcf6b564d7cba4a1beb36519d66c8fb805cddd77fc979e", + "native_viewport.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stdout.bin": "f35d46efc6e4f4151ca25cf94ed01cdab5bc0a09ed53bf1c17e5493b8e081990", + "plan.txt": "e6a9af25ca49a6fb60ef6338fcd7c507f18cdd8ab21abe618045bc535374429a", + "reflow_history.states.jsonl": "dff7e610f39b72e58739ab6c8dd20558d59c7c272883acfbec3a7bdbde3e5c72", + "reflow_history.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "source.bin": "9cb572ecac93ddcf978058226140b4ace5fc6c969a71fc793b6904476503eb11", + "transport.bin": "56a2e81cdb18853b817137de53c7abc21b979f2f4605630e3bc10078851effc9" + } + }, + { + "case": 4, + "passed": true, + "exit_code": 0, + "timed_out": false, + "forced_outer_cleanup": false, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "job_after_forced_cleanup": null, + "process_reaped": true, + "streams_drained": true, + "cleanup_errors": [], + "native": { + "case": 4, + "child_exit": 37, + "active_children": 0, + "output_eof": true, + "bytes": 120, + "resize_offset": 84, + "creation_flags": 24 + }, + "replay": { + "reflow_history": { + "native_rows_equal": false, + "native_cursor_equal": false + }, + "native_viewport": { + "native_rows_equal": true, + "native_cursor_equal": true + } + }, + "files_sha256": { + "capture.json": "de391fe6ade5415ac487afb28e85927174a56ba1b87184da97aa0fe9cb8bf59c", + "go": "4cd0e21a9a0795a14ec9aa5f0e7d1abff0492565770e43eafdf1e3e8afed1f33", + "native-after.json": "590a730384f52d89474d7270070372991cbca06920cf552b6143b4d5407dc36c", + "native-before.json": "6b06a9e4ba13c89cbf685496c0f366f71c384b75c0da45d11c5864edfb97bc3f", + "native_viewport.states.jsonl": "bf314aeaa11317724d32569d029e891eec0b5fa6292c083a5538ddf4f93ab774", + "native_viewport.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stdout.bin": "12352f6f723177360287bd565fd73b3521a2cd62fd563874328cd6041d3ae9a5", + "plan.txt": "1c386f02ec8ae813e323d66b543f9f014154d66d7f54433611fafd85f0a16311", + "reflow_history.states.jsonl": "889bb57844746676ed1718b600cf1742979d2c31e414c1f0a357b4ed294046bf", + "reflow_history.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "source.bin": "7e419e6d600d8ec050531c34e69485ded92323815f19e9c8a76b2ccc0f9303c7", + "transport.bin": "074058e95259ab33d2853655c9a461460d60dedba5a0ea5eb21504af88754641" + } + }, + { + "case": 5, + "passed": true, + "exit_code": 0, + "timed_out": false, + "forced_outer_cleanup": false, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "job_after_forced_cleanup": null, + "process_reaped": true, + "streams_drained": true, + "cleanup_errors": [], + "native": { + "case": 5, + "child_exit": 37, + "active_children": 0, + "output_eof": true, + "bytes": 117, + "resize_offset": 81, + "creation_flags": 24 + }, + "replay": { + "reflow_history": { + "native_rows_equal": false, + "native_cursor_equal": false + }, + "native_viewport": { + "native_rows_equal": true, + "native_cursor_equal": true + } + }, + "files_sha256": { + "capture.json": "a11778c24628865e6b02a2ad8e8f4aa387db7d94e3952df170b66492065baaed", + "go": "4cd0e21a9a0795a14ec9aa5f0e7d1abff0492565770e43eafdf1e3e8afed1f33", + "native-after.json": "8bae95467855394186680f29db75b9556daa6888ca71c4237668759a60c11c86", + "native-before.json": "c3f863c9a6c386ca80398353201857bede3e94e7fc447d56b9496d174bc8eeb3", + "native_viewport.states.jsonl": "f9660bdcbb114d4bd713fa814b78e6c274845f82e827ae6da4b39131869e6ab9", + "native_viewport.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "parent.stdout.bin": "e4a81e5682d8a7fc7cd841988098ae605dca427ae0e52aa09a90b63d44218304", + "plan.txt": "4cc8c160bc1f563dc62bb127b145b45df5d25f3fdd3b9c260851cc8bb721c49e", + "reflow_history.states.jsonl": "16f7a95da0d135c06dceb9d700b491d99fa339cc4c01c4a003bdb57875050294", + "reflow_history.stderr.bin": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "source.bin": "9cb572ecac93ddcf978058226140b4ace5fc6c969a71fc793b6904476503eb11", + "transport.bin": "56a2e81cdb18853b817137de53c7abc21b979f2f4605630e3bc10078851effc9" + } + } + ] +} diff --git a/experiments/conpty/results/2026-10-27-native-viewport-resize/validation.json b/experiments/conpty/results/2026-10-27-native-viewport-resize/validation.json new file mode 100644 index 00000000..78e9acdc --- /dev/null +++ b/experiments/conpty/results/2026-10-27-native-viewport-resize/validation.json @@ -0,0 +1,1667 @@ +{ + "schema": 1, + "issue": 51, + "source_commit": "92eac12d5cd2868b469e8b9a10e68f5e20ee2cb0", + "base_commit": "e0b4d67e50bda6fe08cc02b43a475137133310b3", + "candidate_version": "0.26.1-dev+termforge.5", + "backend": { + "name": "Microsoft.Windows.Console.ConPTY", + "version": "1.25.260930003", + "url": "https://github.com/microsoft/terminal/releases/download/v1.25.2733.0/Microsoft.Windows.Console.ConPTY.1.25.260930003.nupkg", + "bytes": 1750533, + "sha256": "6d6f8b008c655d814c498d8205d4012dcd4d0b6dfc1096461d0a185d1e3caf03", + "files": { + "conpty.dll": "feeef341d891643c62d30b6b07800bc70f0bc148f44cb8c3bee84aa557ae805a", + "OpenConsole.exe": "3d66b23d0a71bb8eed2b77edc8b9df9bf54ce6c8fb74c863a30e760997f80586" + } + }, + "creation_flags": 24, + "windows": "10.0.26200", + "resize_policy": "native_viewport only for explicit reviewed-backend candidate ShellWindow", + "sources_sha256": { + "CMakeLists.txt": "3db27130f32f57eb798872410506eea4a638e0841dfd5474fdbeb216fb774b5b", + "experiments/candidates/alacritty/Cargo.lock": "f7b8906cc63b6a5d66943f15c50a78c51f773face60229fcddf24b2d02911736", + "experiments/candidates/alacritty/include/termforge_candidate.h": "e3accd6b57cce18784f9bd14bbb5042bec985464fe440bf54f652a0ebecfee92", + "experiments/candidates/alacritty/run.py": "2d83240506bf03b4da92f7b2dc85adf40446aff25cd0f0dbe08132b611c2627e", + "experiments/candidates/alacritty/src/ffi.rs": "09ea991999db3d1948cb5c4a46cd8d12e8e8f70d7648f07d09b1dd82eeab9c04", + "experiments/candidates/alacritty/vendor/alacritty_terminal/Cargo.toml": "2b5fc3bab139b7f141fbe39ae32364617cf526c4d6a5ec2ce12efc97379e42b7", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs": "eedd587edfd00917b3f8397cd5683263a33e63675ba332eba50e3648d008e157", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs": "05e489c6ac0c0bf6ea50182316985c4fa59e9a7c977e808edd0de4267d87c077", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/term/mod.rs": "0479445db15d75f006398ba7fbbebba763cbedb1c993d69b3e5fdc94746a62e2", + "experiments/conpty/backend.hpp": "5ee3ab95dc4b9aefce23ff7900cda2ecdd9cd09d1b1044074de2f5e572c31489", + "experiments/conpty/resize_native_probe.cpp": "ea0f26b3cfc322929193a89ea72d3e987a2ec9f3fda87241989f508cd431336a", + "experiments/conpty/resize_native_run.py": "a7fdc198a9fde7cff00ce1f74031b680e83d130e5034a612e576340e037fa1f2", + "experiments/conpty/session.hpp": "6945e6ffba1d61e66266f867d3cd124eb63c39da37e27662a5eaf544f60ed96b", + "experiments/core/candidate_resize_replay.cpp": "c3e6bcd7dc41622c8e407507c4cd9da39398197d76aa7d3ea042ae0bd4164a7b", + "experiments/core/candidate_terminal.cpp": "16eb43f2488e9621fc965d50be99a25ebf49b6d51ad69d2446fcc13bedda7bf5", + "experiments/core/candidate_terminal.hpp": "f4e51039bc5a10fd120ef8a5fc2b12fcb9ddb1c32a4ae36fc74f226ae22898a1", + "experiments/core/candidate_terminal_test.cpp": "097faf9c92cad3751ba3d0292739a041943c4f1cd7f4e4bc5fac72bc578aa3fa", + "experiments/core/test_candidate_resize_replay.py": "14a78930060f8c66c78a6e601312cb6997efe77330d3c226e9560d8c48ce0944", + "experiments/shell/resize_trace.py": "fd75a4a1a6d3d0ad8ea213da04a875f4bda210c6501e41b4b4004161b0037501", + "experiments/shell/run.py": "45976b828eecb316881d698dedaef09ad304f72d991dfa360f1c552bbcbe9f1d", + "experiments/shell/shell.cpp": "ab5f39acd6174e3a5a256dea6f59037758e3b521af91c183a67f7d021700a089", + "experiments/shell/size_client.cpp": "cb11661bec9551053eec0dbb474bce2ac899a06ee2e50830a370af4816df39f4", + "experiments/shell/test_launcher.cpp": "19cab4a960f47f252c02f92a36316d43eed2481da1f72b5a7e97003c4fa1c2ed", + "experiments/shell/text_client.cpp": "aea8ed527e6e79fbe61446afadbb25d4fa0b7ddef4966930583ac8fa04cadd15", + "experiments/shell/workflow_checks.py": "f186ff0f5335c26fc5c4f0286961981dfd6af8fbaafc789e2bc50058486efa44", + "experiments/shell/workflow_sessions_checks.py": "b976d3504837eef57ff13319daed254870727741f36f265412a6b14b4cb3faae", + "experiments/shell/workspace_workflow_scenario.hpp": "467571556440f4e89305bc1261d389bb8f24ba84f607b8cd36b8078a58314369" + }, + "binaries_sha256": { + "termforge_shell.exe": "15a149a3d7d47efaf866226559d4b60bbf83e0ff6004a85df41edcb5415ad206", + "candidate_core_test.exe": "4c73b58d7da111ab52b6edbbb59898cf660860fdedaa64c4f49b46fae856a02d", + "candidate_resize_replay.exe": "c4cf0d9f68bb5bd688e601b87025f3237c8659f4d80e96bd4f67fa463463a13f", + "conpty_resize_native_probe.exe": "a570b55f7ef0f65be01b827112905a223fb32f37a0f645ce121ead1ed6555f59", + "shell_test_launcher.exe": "7bd78cffd78ac66d651933c9614d6be937c99baa105ce04c1a533df5785c11e2", + "shell_size_client.exe": "eeb0801f69cc8add0a8178a185dcedb78a9b33234abfe5c600fd9b20b0f5aab4", + "shell_text_client.exe": "9350e53de74c9a6b78495a3b59788e3a31e0067a9c5ec1b8685a729ff18244d3", + "shell_query_client.exe": "691b440e9c500c76c1c09c448aafbe2316793bf341ffe69bbc2bf70e41be64cc" + }, + "source_hash_policy": "Raw worktree bytes used for builds; Git checkout line endings can differ.", + "baseline_gui": { + "source": "e0b4d67e50bda6fe08cc02b43a475137133310b3", + "diagnostic_only": true, + "source_patch_applied": false, + "patched_resize_sha256": "4907427c9488435f1e1ee624629bc8445c4dde5399f0349955d1bbc2042080c4", + "binary_sha256": "e4be16054f1c43528cdaca0f54b664f856843669a43000778b751b722026dff4", + "cases": [ + { + "case": "resize-0", + "outcome": "failure", + "exit_code": 2, + "timed_out": false, + "job_after": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "desktop_gone": true, + "sample_sha256": "dbf63d052fae858dca4ec36d8884f2b919515defad23bec749a994eab1b9e393", + "stage": 43 + } + ], + "started_utc": "2026-10-26T19:54:02.639884+00:00" + }, + "formal_core_red": { + "exit_code": 1, + "error": "native viewport text differs", + "identity": { + "experiments/core/candidate_terminal.hpp": "f4e51039bc5a10fd120ef8a5fc2b12fcb9ddb1c32a4ae36fc74f226ae22898a1", + "experiments/core/candidate_terminal.cpp": "16eb43f2488e9621fc965d50be99a25ebf49b6d51ad69d2446fcc13bedda7bf5", + "experiments/core/candidate_terminal_test.cpp": "310ef633d13b909d5ad0d659cffc4cbe81b2f3d84bbd87400b13e54051bfc772", + "experiments/candidates/alacritty/src/ffi.rs": "0824acd1e13f74254ee38976485c21d79e6ae4feb38c4634ce3aef163315e6d4", + "experiments/candidates/alacritty/include/termforge_candidate.h": "e3accd6b57cce18784f9bd14bbb5042bec985464fe440bf54f652a0ebecfee92", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs": "4907427c9488435f1e1ee624629bc8445c4dde5399f0349955d1bbc2042080c4", + "test_exe_sha256": "0f5bd0454c288eb300368ab0090f2b917b618f74f32bde7522658b0db03f0ee8" + }, + "log_sha256": "2fb33922305c15a0912d40fe556589f7092375ffa05a903aa16b99a1f0a40dc2" + }, + "original_native_capture": { + "source": "188228ebe1a4e6cd3779fc7c6b150624514886fb", + "shell_sha256": "e81d92a8c7d380d4c33639861180b04935e021bc7c1a5179197c8a9e3c83454a", + "note": "Temporary size helper snapshots perturb timing; native row10 exact, candidate row10 had OW.", + "files_sha256": { + "case-0/resize-trace.json": "b3d57213f4da6a94e2a672ddd5d2e5690ad4604636c16a0835d506ea6c3d76f8", + "case-0/resize-trace.transport.bin": "2b4deb1d932af810cef0892064a13b481c582ad44a4faa02cd08fe502d410feb", + "case-0/r51-native-size-100x28-after.json": "67d2217d0c03212a87b4ccaac822cf9cbd93380a337f30ac91bdb988e0644ba0", + "case-0/sample.json": "87624dc16ace15e803bea774aa7af253099f9588975ddabfab0c4ccc7d04cb81", + "replay-0/plan.txt": "bab0875e0f31908a1c5f1a8194ee43b47113a78af1da28f618a17677bf29f57b" + }, + "native_row10": "TF_SIZE_100x28", + "candidate_row10": "TF_SIZE_100x28OW" + }, + "final_original_replay": { + "replay_exe_sha256": "c4cf0d9f68bb5bd688e601b87025f3237c8659f4d80e96bd4f67fa463463a13f", + "options": [ + "--native-viewport" + ], + "same_native_rows_0_through_10": true, + "exact_marker": true, + "history_rows": 9, + "cursor": [ + 12, + 3 + ], + "diagnostic_only": true + }, + "final_gui": { + "source": "92eac12d5cd2868b469e8b9a10e68f5e20ee2cb0", + "diagnostic_only": true, + "source_patch_applied": false, + "patched_resize_sha256": "eedd587edfd00917b3f8397cd5683263a33e63675ba332eba50e3648d008e157", + "binary_sha256": "15a149a3d7d47efaf866226559d4b60bbf83e0ff6004a85df41edcb5415ad206", + "cases": [ + { + "case": "resize-0", + "outcome": "green", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 9, + "terminated": 0 + }, + "desktop_gone": true, + "sample_sha256": "b113686ea1536b96932401ec5caa6e678db2146d46ad8f43250729d5b0c037e0", + "result": { + "valid": true, + "exit_code": 37, + "active_children_after": 0, + "cleanup_complete": true, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": true, + "resize_verified": 3, + "rows": 18, + "columns": 72, + "input_rejected": 0, + "presented_done": null + } + }, + { + "case": "resize-1", + "outcome": "green", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 9, + "terminated": 0 + }, + "desktop_gone": true, + "sample_sha256": "95eedda4e52bb2c78bf445fc068d8a7cf2acbd6952b63c8bf41ad68136862f1c", + "result": { + "valid": true, + "exit_code": 37, + "active_children_after": 0, + "cleanup_complete": true, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": true, + "resize_verified": 3, + "rows": 18, + "columns": 72, + "input_rejected": 0, + "presented_done": null + } + }, + { + "case": "resize-close-2", + "outcome": "green", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "desktop_gone": true, + "sample_sha256": "4bfea5d8f916c27192b1d539d545e1df5ddc0fd04e338aae239fda902027a394", + "result": { + "valid": true, + "exit_code": 91, + "active_children_after": 0, + "cleanup_complete": true, + "command_verified": true, + "unicode_verified": true, + "backspace_verified": true, + "arrows_verified": true, + "ctrl_c_verified": false, + "resize_verified": 2, + "rows": 18, + "columns": 72, + "input_rejected": 0, + "presented_done": null + } + }, + { + "case": "candidate-unicode-3", + "outcome": "green", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "desktop_gone": true, + "sample_sha256": "a3c614c702d92ac849cf287923da9593deeeb0a0506545c27876945904c119c9", + "result": { + "valid": true, + "exit_code": 37, + "active_children_after": 0, + "cleanup_complete": true, + "command_verified": true, + "unicode_verified": false, + "backspace_verified": false, + "arrows_verified": false, + "ctrl_c_verified": false, + "resize_verified": 0, + "rows": 30, + "columns": 100, + "input_rejected": 0, + "presented_done": null + }, + "unicode_output": { + "scope": "fixed-four-streaming-graphemes-width-selection-glyph", + "source_sha256": "a3b951ab67ca5d079f7e91ee8f1cd25dfde82ae4b08ed40c406c911e0ccdf8c2", + "transport_sha256": "c115bf2fd28e7a602993c30985853b895409884c0fba65fd697fbd1f89173c0e", + "transport_bytes": 3353, + "scoped_validation_passed": true, + "complex_grapheme_rendering_verified": true, + "whole_cluster_width_policy_verified": true, + "resized_native_console_verified": false, + "windterm_runtime_equivalence_verified": false, + "complete_unicode_ime_verified": false, + "presented_done": null, + "resize": { + "scope": "fixed-four-streaming-graphemes-width-selection-glyph", + "source_sha256": "a3b951ab67ca5d079f7e91ee8f1cd25dfde82ae4b08ed40c406c911e0ccdf8c2", + "transport_sha256": "d3b472c45f7ed73f9d1c4b0fb190e974cc1f39c0522d495d30402639957859c5", + "transport_bytes": 3819, + "scoped_validation_passed": true, + "complex_grapheme_rendering_verified": true, + "whole_cluster_width_policy_verified": true, + "resized_native_console_verified": true, + "windterm_runtime_equivalence_verified": false, + "complete_unicode_ime_verified": false, + "presented_done": null + } + }, + "result_field_note": "Generic result.unicode_verified belongs to the basic shell scenario; candidate-unicode acceptance is unicode_output.scoped_validation_passed and its resize record." + }, + { + "case": "candidate-unicode-4", + "outcome": "green", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 8, + "terminated": 0 + }, + "desktop_gone": true, + "sample_sha256": "4fabd30042d7a6053663a2b3f084ea179926f211cb1acb634291877637349dd8", + "result": { + "valid": true, + "exit_code": 37, + "active_children_after": 0, + "cleanup_complete": true, + "command_verified": true, + "unicode_verified": false, + "backspace_verified": false, + "arrows_verified": false, + "ctrl_c_verified": false, + "resize_verified": 0, + "rows": 30, + "columns": 100, + "input_rejected": 0, + "presented_done": null + }, + "unicode_output": { + "scope": "fixed-four-streaming-graphemes-width-selection-glyph", + "source_sha256": "a3b951ab67ca5d079f7e91ee8f1cd25dfde82ae4b08ed40c406c911e0ccdf8c2", + "transport_sha256": "bff603002c1b35097b27bba74fd6fafa89eb44c3948b409c0c5feb9ffa40c32d", + "transport_bytes": 3353, + "scoped_validation_passed": true, + "complex_grapheme_rendering_verified": true, + "whole_cluster_width_policy_verified": true, + "resized_native_console_verified": false, + "windterm_runtime_equivalence_verified": false, + "complete_unicode_ime_verified": false, + "presented_done": null, + "resize": { + "scope": "fixed-four-streaming-graphemes-width-selection-glyph", + "source_sha256": "a3b951ab67ca5d079f7e91ee8f1cd25dfde82ae4b08ed40c406c911e0ccdf8c2", + "transport_sha256": "f50293197cfaeb077453e378f58c38c5c878e89d55f7833d2e0600e8ad2889cd", + "transport_bytes": 4001, + "scoped_validation_passed": true, + "complex_grapheme_rendering_verified": true, + "whole_cluster_width_policy_verified": true, + "resized_native_console_verified": true, + "windterm_runtime_equivalence_verified": false, + "complete_unicode_ime_verified": false, + "presented_done": null + } + }, + "result_field_note": "Generic result.unicode_verified belongs to the basic shell scenario; candidate-unicode acceptance is unicode_output.scoped_validation_passed and its resize record." + } + ], + "started_utc": "2026-10-26T20:46:03.844348+00:00" + }, + "native_cases_file": "native-cases.json", + "written_spaces_regression": { + "red": { + "source": "f40d347 plus written-space regression", + "files": { + "experiments/core/candidate_terminal_test.cpp": "097faf9c92cad3751ba3d0292739a041943c4f1cd7f4e4bc5fac72bc578aa3fa", + "experiments/candidates/alacritty/src/ffi.rs": "04f2b0a3cb66c3b588d083b861781eae6af83a2b03104d779f156a892bbd7517", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/row.rs": "2f963299e52525828d9d0bf1de758d793661c908eb7c126aa8556a1c9945a83d", + "experiments/candidates/alacritty/vendor/alacritty_terminal/src/grid/resize.rs": "a1f0d8bcd133208f12b2eb0c96edf04be6dd7bae9cb3a95db6a2086cf8833bc3", + "build/candidate/bin/Release/candidate_core_test.exe": "f574f415c509ac3c78a8b048dfab3ae789d4a6d63591f0256c4492dcb9195233" + }, + "exit_code": 1, + "error": "resize changed written spaces" + }, + "red_log_sha256": "6c6ef8d9801b3020b3da305423cfd81ae0fad3197f980cdb5d21330dcdc5a39d", + "green_log_sha256": "2d790a6cc2d35b2379ea0385b384c326a361db1e5cb550d0fb209a80901b0c26", + "input": "ABC D", + "widths": [ + 4, + 5, + 8, + 4, + 5 + ], + "policies": [ + "reflow_history", + "native_viewport" + ], + "passed": true + }, + "auxiliary_review_checks": { + "source": "92eac12d5cd2868b469e8b9a10e68f5e20ee2cb0", + "native_copy_assertions": 630, + "files_sha256": { + "native-compile.log": "dad173ee0c7a5afd1408be074bfdfa13e828e1c1c71865b477a9a9ef90e058d9", + "native-probe.exe": "0a6d3f643dde129526b506d356970b311ff46a40a449ac1515da3783a25b52f9", + "native-run.log": "564666081a6201b624a62f3bd3daebf776bb83b024e29a8e93408e1bd1beb020", + "probe.cpp": "b8926e08ec1b369ebcc35bc74435863fb5cf999c902dc4832ba02a290b7f505f" + }, + "scope": "Additional reviewer diagnostic; not a native-oracle or performance comparison." + }, + "native_probe_negatives": { + "source": "92eac12d5cd2868b469e8b9a10e68f5e20ee2cb0", + "runner_sha256": "a7fdc198a9fde7cff00ce1f74031b680e83d130e5034a612e576340e037fa1f2", + "cases": [ + { + "name": "no-go", + "case": 0, + "exit_code": 2, + "timed_out": false, + "forced_outer_cleanup": false, + "job_after": { + "active": 0, + "total": 2, + "terminated": 0 + }, + "job_after_forced_cleanup": null, + "process_reaped": true, + "streams_drained": true, + "cleanup_errors": [] + }, + { + "name": "watchdog", + "case": 0, + "exit_code": 99, + "timed_out": true, + "forced_outer_cleanup": true, + "job_after": { + "active": 0, + "total": 4, + "terminated": 0 + }, + "job_after_forced_cleanup": null, + "process_reaped": true, + "streams_drained": true, + "cleanup_errors": [] + }, + { + "name": "missing-executable-and-unwritable-evidence", + "original_FileNotFoundError_preserved": true, + "winerror": 2, + "existing_file_unchanged": true + } + ] + }, + "rust_tests": { + "passed_per_target": [ + 157, + 2, + 2, + 2, + 1 + ], + "log_sha256": "798884c07803a574adecf796b5da6b8279639922a908b52d969abe44de62a42b" + }, + "initial_full_ctest": { + "log_sha256": "a04c06b541b15e23716ce4b542138d5a6844acb548944ff5c1a3d359c1317d9f", + "tests": [ + { + "name": "shell_launch", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "candidate_title", + "result": "passed", + "seconds": 0.72 + }, + { + "name": "candidate_reply_close", + "result": "passed", + "seconds": 0.36 + }, + { + "name": "runtime_observation", + "result": "passed", + "seconds": 1.61 + }, + { + "name": "ime_input_expectation", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "sent_message_wait", + "result": "passed", + "seconds": 0.3 + }, + { + "name": "find_bar", + "result": "passed", + "seconds": 0.39 + }, + { + "name": "renderer_metrics", + "result": "passed", + "seconds": 0.96 + }, + { + "name": "ime_context", + "result": "passed", + "seconds": 0.41 + }, + { + "name": "layout_state", + "result": "passed", + "seconds": 0.15 + }, + { + "name": "workspace_state", + "result": "passed", + "seconds": 0.38 + }, + { + "name": "transport_completion", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "scenario_tick_gate", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "candidate_core", + "result": "passed", + "seconds": 1.33 + }, + { + "name": "resize_core", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "history_core", + "result": "passed", + "seconds": 0.03 + }, + { + "name": "selection_core", + "result": "passed", + "seconds": 0.11 + }, + { + "name": "search_core", + "result": "passed", + "seconds": 0.15 + }, + { + "name": "paste", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "sender_fence", + "result": "passed", + "seconds": 1.03 + }, + { + "name": "sender_fence_pipe", + "result": "passed", + "seconds": 0.39 + }, + { + "name": "clipboard_policy", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "keyboard", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "text_action_policy", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "candidate_resize_replay", + "result": "passed", + "seconds": 0.7 + }, + { + "name": "candidate_shell_tui", + "result": "failed", + "seconds": 31.03 + }, + { + "name": "core", + "result": "passed", + "seconds": 6.59 + }, + { + "name": "recovery", + "result": "passed", + "seconds": 13.14 + }, + { + "name": "measure", + "result": "passed", + "seconds": 5.17 + }, + { + "name": "binary_paths", + "result": "passed", + "seconds": 0.21 + }, + { + "name": "conpty_margin_contract", + "result": "passed", + "seconds": 0.34 + }, + { + "name": "benchmark_tools", + "result": "passed", + "seconds": 5.67 + }, + { + "name": "differential", + "result": "passed", + "seconds": 3.55 + }, + { + "name": "ime_inventory", + "result": "passed", + "seconds": 54.43 + }, + { + "name": "text_actions", + "result": "passed", + "seconds": 5.58 + }, + { + "name": "workflow_fixture", + "result": "passed", + "seconds": 22.86 + }, + { + "name": "workflow_sessions", + "result": "failed", + "seconds": 39.01 + }, + { + "name": "workflow_resources", + "result": "passed", + "seconds": 0.5 + }, + { + "name": "workflow_resource_phases", + "result": "passed", + "seconds": 4.37 + }, + { + "name": "workflow_system_content", + "result": "passed", + "seconds": 0.18 + }, + { + "name": "workflow_system_runner", + "result": "passed", + "seconds": 0.66 + }, + { + "name": "copy_error_frame", + "result": "passed", + "seconds": 0.15 + }, + { + "name": "paste_fence_evidence", + "result": "passed", + "seconds": 0.11 + }, + { + "name": "resize_trace", + "result": "passed", + "seconds": 0.13 + }, + { + "name": "research_guards", + "result": "passed", + "seconds": 5.07 + }, + { + "name": "receive", + "result": "passed", + "seconds": 0.21 + }, + { + "name": "conpty", + "result": "passed", + "seconds": 23.1 + }, + { + "name": "conpty_ctrl_c", + "result": "passed", + "seconds": 4.34 + }, + { + "name": "conpty_margin", + "result": "passed", + "seconds": 0.38 + }, + { + "name": "conpty_backend", + "result": "passed", + "seconds": 1.19 + }, + { + "name": "presentation", + "result": "passed", + "seconds": 4.19 + }, + { + "name": "shell", + "result": "passed", + "seconds": 96.68 + }, + { + "name": "selection_bytes", + "result": "passed", + "seconds": 3.7 + }, + { + "name": "selection_context", + "result": "passed", + "seconds": 3.78 + }, + { + "name": "raw_console_receiver", + "result": "passed", + "seconds": 0.41 + }, + { + "name": "paste_system", + "result": "passed", + "seconds": 8.59 + } + ], + "total": 56, + "passed": 54, + "failed": [ + "candidate_shell_tui", + "workflow_sessions" + ], + "seconds": 354.65, + "failure_stages": [ + 50 + ], + "full_acceptance": false, + "product_source_commit": "57f24f5a2204a38bcc6429cc448247a7893ef349", + "shell_sha256": "4e23e277ea7984d85856b713b3af5b0545d68042c2bbd42a8422f8a6b422c28d" + }, + "fixed_path_intermediate_ctest": { + "log_sha256": "068ef8ad11e19c697e95df4e9d8bd8a2210c8e075e02550bf0ed706913949000", + "tests": [ + { + "name": "shell_launch", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "candidate_title", + "result": "passed", + "seconds": 0.88 + }, + { + "name": "candidate_reply_close", + "result": "passed", + "seconds": 0.33 + }, + { + "name": "runtime_observation", + "result": "passed", + "seconds": 1.43 + }, + { + "name": "ime_input_expectation", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "sent_message_wait", + "result": "passed", + "seconds": 0.29 + }, + { + "name": "find_bar", + "result": "passed", + "seconds": 0.35 + }, + { + "name": "renderer_metrics", + "result": "passed", + "seconds": 0.94 + }, + { + "name": "ime_context", + "result": "passed", + "seconds": 0.39 + }, + { + "name": "layout_state", + "result": "passed", + "seconds": 0.14 + }, + { + "name": "workspace_state", + "result": "passed", + "seconds": 0.35 + }, + { + "name": "transport_completion", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "scenario_tick_gate", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "candidate_core", + "result": "passed", + "seconds": 1.22 + }, + { + "name": "resize_core", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "history_core", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "selection_core", + "result": "passed", + "seconds": 0.1 + }, + { + "name": "search_core", + "result": "passed", + "seconds": 0.12 + }, + { + "name": "paste", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "sender_fence", + "result": "passed", + "seconds": 1.02 + }, + { + "name": "sender_fence_pipe", + "result": "passed", + "seconds": 0.37 + }, + { + "name": "clipboard_policy", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "keyboard", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "text_action_policy", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "candidate_resize_replay", + "result": "passed", + "seconds": 0.61 + }, + { + "name": "candidate_shell_tui", + "result": "failed", + "seconds": 30.57 + }, + { + "name": "core", + "result": "passed", + "seconds": 6.28 + }, + { + "name": "recovery", + "result": "passed", + "seconds": 10.45 + }, + { + "name": "measure", + "result": "passed", + "seconds": 4.02 + }, + { + "name": "binary_paths", + "result": "passed", + "seconds": 0.18 + }, + { + "name": "conpty_margin_contract", + "result": "passed", + "seconds": 0.36 + }, + { + "name": "benchmark_tools", + "result": "passed", + "seconds": 5.54 + }, + { + "name": "differential", + "result": "passed", + "seconds": 3.38 + }, + { + "name": "ime_inventory", + "result": "passed", + "seconds": 50.46 + }, + { + "name": "text_actions", + "result": "passed", + "seconds": 5.62 + }, + { + "name": "workflow_fixture", + "result": "passed", + "seconds": 22.72 + }, + { + "name": "workflow_sessions", + "result": "failed", + "seconds": 80.03 + }, + { + "name": "workflow_resources", + "result": "passed", + "seconds": 0.54 + }, + { + "name": "workflow_resource_phases", + "result": "passed", + "seconds": 4.43 + }, + { + "name": "workflow_system_content", + "result": "passed", + "seconds": 0.18 + }, + { + "name": "workflow_system_runner", + "result": "passed", + "seconds": 0.66 + }, + { + "name": "copy_error_frame", + "result": "passed", + "seconds": 0.14 + }, + { + "name": "paste_fence_evidence", + "result": "passed", + "seconds": 0.11 + }, + { + "name": "resize_trace", + "result": "passed", + "seconds": 0.13 + }, + { + "name": "research_guards", + "result": "passed", + "seconds": 5.0 + }, + { + "name": "receive", + "result": "passed", + "seconds": 0.21 + }, + { + "name": "conpty", + "result": "passed", + "seconds": 23.06 + }, + { + "name": "conpty_ctrl_c", + "result": "passed", + "seconds": 4.32 + }, + { + "name": "conpty_margin", + "result": "passed", + "seconds": 0.42 + }, + { + "name": "conpty_backend", + "result": "passed", + "seconds": 1.19 + }, + { + "name": "presentation", + "result": "passed", + "seconds": 4.15 + }, + { + "name": "shell", + "result": "passed", + "seconds": 97.02 + }, + { + "name": "selection_bytes", + "result": "passed", + "seconds": 3.56 + }, + { + "name": "selection_context", + "result": "passed", + "seconds": 3.8 + }, + { + "name": "raw_console_receiver", + "result": "passed", + "seconds": 0.39 + }, + { + "name": "paste_system", + "result": "passed", + "seconds": 8.43 + } + ], + "total": 56, + "passed": 54, + "failed": [ + "candidate_shell_tui", + "workflow_sessions" + ], + "seconds": 386.09, + "failure_stages": [ + 59 + ], + "full_acceptance": false, + "product_source_commit": "57f24f5a2204a38bcc6429cc448247a7893ef349", + "shell_sha256": "4e23e277ea7984d85856b713b3af5b0545d68042c2bbd42a8422f8a6b422c28d" + }, + "final_full_ctest": { + "log_sha256": "e9a8ea36af3b9b72665ee29c6e87a7cac15fe5531fe7c23e72bb27a60f95c17f", + "tests": [ + { + "name": "shell_launch", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "candidate_title", + "result": "passed", + "seconds": 0.9 + }, + { + "name": "candidate_reply_close", + "result": "passed", + "seconds": 0.36 + }, + { + "name": "runtime_observation", + "result": "passed", + "seconds": 1.56 + }, + { + "name": "ime_input_expectation", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "sent_message_wait", + "result": "passed", + "seconds": 0.3 + }, + { + "name": "find_bar", + "result": "passed", + "seconds": 0.36 + }, + { + "name": "renderer_metrics", + "result": "passed", + "seconds": 0.89 + }, + { + "name": "ime_context", + "result": "passed", + "seconds": 0.33 + }, + { + "name": "layout_state", + "result": "passed", + "seconds": 0.14 + }, + { + "name": "workspace_state", + "result": "passed", + "seconds": 0.37 + }, + { + "name": "transport_completion", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "scenario_tick_gate", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "candidate_core", + "result": "passed", + "seconds": 1.34 + }, + { + "name": "resize_core", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "history_core", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "selection_core", + "result": "passed", + "seconds": 0.09 + }, + { + "name": "search_core", + "result": "passed", + "seconds": 0.15 + }, + { + "name": "paste", + "result": "passed", + "seconds": 0.02 + }, + { + "name": "sender_fence", + "result": "passed", + "seconds": 1.03 + }, + { + "name": "sender_fence_pipe", + "result": "passed", + "seconds": 0.38 + }, + { + "name": "clipboard_policy", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "keyboard", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "text_action_policy", + "result": "passed", + "seconds": 0.01 + }, + { + "name": "candidate_resize_replay", + "result": "passed", + "seconds": 0.72 + }, + { + "name": "candidate_shell_tui", + "result": "failed", + "seconds": 30.51 + }, + { + "name": "core", + "result": "passed", + "seconds": 6.93 + }, + { + "name": "recovery", + "result": "passed", + "seconds": 10.97 + }, + { + "name": "measure", + "result": "passed", + "seconds": 4.03 + }, + { + "name": "binary_paths", + "result": "passed", + "seconds": 0.18 + }, + { + "name": "conpty_margin_contract", + "result": "passed", + "seconds": 0.29 + }, + { + "name": "benchmark_tools", + "result": "passed", + "seconds": 5.4 + }, + { + "name": "differential", + "result": "passed", + "seconds": 3.32 + }, + { + "name": "ime_inventory", + "result": "passed", + "seconds": 50.32 + }, + { + "name": "text_actions", + "result": "passed", + "seconds": 5.59 + }, + { + "name": "workflow_fixture", + "result": "passed", + "seconds": 22.88 + }, + { + "name": "workflow_sessions", + "result": "passed", + "seconds": 81.41 + }, + { + "name": "workflow_resources", + "result": "passed", + "seconds": 0.48 + }, + { + "name": "workflow_resource_phases", + "result": "passed", + "seconds": 4.31 + }, + { + "name": "workflow_system_content", + "result": "passed", + "seconds": 0.16 + }, + { + "name": "workflow_system_runner", + "result": "passed", + "seconds": 0.67 + }, + { + "name": "copy_error_frame", + "result": "passed", + "seconds": 0.14 + }, + { + "name": "paste_fence_evidence", + "result": "passed", + "seconds": 0.11 + }, + { + "name": "resize_trace", + "result": "passed", + "seconds": 0.12 + }, + { + "name": "research_guards", + "result": "passed", + "seconds": 5.0 + }, + { + "name": "receive", + "result": "passed", + "seconds": 0.23 + }, + { + "name": "conpty", + "result": "passed", + "seconds": 22.44 + }, + { + "name": "conpty_ctrl_c", + "result": "passed", + "seconds": 4.47 + }, + { + "name": "conpty_margin", + "result": "passed", + "seconds": 0.57 + }, + { + "name": "conpty_backend", + "result": "passed", + "seconds": 1.34 + }, + { + "name": "presentation", + "result": "passed", + "seconds": 4.25 + }, + { + "name": "shell", + "result": "failed", + "seconds": 121.65 + }, + { + "name": "selection_bytes", + "result": "passed", + "seconds": 3.76 + }, + { + "name": "selection_context", + "result": "passed", + "seconds": 3.81 + }, + { + "name": "raw_console_receiver", + "result": "passed", + "seconds": 0.39 + }, + { + "name": "paste_system", + "result": "passed", + "seconds": 6.72 + } + ], + "total": 56, + "passed": 54, + "failed": [ + "candidate_shell_tui", + "shell" + ], + "seconds": 411.59, + "failure_stages": [ + 59 + ], + "full_acceptance": false, + "last_test_log_sha256": "cc3f6814ba85db77091830cf20f54afcd66c5da826e6ccda539533dce7bbfc28", + "internal_skips": [ + { + "test": "setUpClass (test_candidate_compare.CandidateComparisonTests)", + "reason": "optional Alacritty binary is not built; core-candidate workflow runs these checks" + }, + { + "test": "setUpClass (test_capability_matrix.CapabilityMatrixTests)", + "reason": "optional Alacritty binary is not built; core-candidate workflow runs these checks" + }, + { + "test": "setUpClass (test_fuzz_matrix.FuzzMatrixTests)", + "reason": "optional Alacritty binary is not built; core-candidate workflow runs these checks" + }, + { + "test": "setUpClass (test_real_app_matrix.RealApplicationMatrixTests)", + "reason": "optional real application capture dependencies are not built" + }, + { + "test": "test_actual_system_copy_paste_and_shortcuts_twice (experiments.shell.test_actions.ActionWindowTest.test_actual_system_copy_paste_and_shortcuts_twice)", + "reason": "actual system Copy/Paste requires a disposable hosted VM" + }, + { + "test": "test_suspended_target_is_reclaimed_if_job_assignment_fails (test_ui_r1.EvidenceBoundaryTests.test_suspended_target_is_reclaimed_if_job_assignment_fails)", + "reason": "opt-in pinned reference failure injection" + }, + { + "test": "test_mouse_selection_to_real_system_clipboard (experiments.shell.test_shell.ShellTests.test_mouse_selection_to_real_system_clipboard)", + "reason": "system copy UI gate requires a disposable hosted VM" + }, + { + "test": "test_system_clipboard_product_read_errors_cancel_and_close (experiments.shell.test_shell.ShellTests.test_system_clipboard_product_read_errors_cancel_and_close)", + "reason": "product clipboard read gate requires a disposable hosted VM" + }, + { + "test": "test_system_clipboard_shortcut_to_window_confirmation_and_receiver (experiments.shell.test_shell.ShellTests.test_system_clipboard_shortcut_to_window_confirmation_and_receiver)", + "reason": "real system clipboard UI gate requires a disposable hosted VM" + }, + { + "test": "test_real_system_copy_shortcuts_for_styled_fixture_twice (experiments.shell.test_selection_bytes.SelectionBytesWindowTest.test_real_system_copy_shortcuts_for_styled_fixture_twice)", + "reason": "system CF requires a disposable hosted VM" + }, + { + "test": "test_real_system_copy_and_edit_context_twice (experiments.shell.test_selection_context.SelectionContextWindowTest.test_real_system_copy_and_edit_context_twice)", + "reason": "real CF/EDIT Copy requires a disposable hosted VM" + }, + { + "test": "test_real_system_clipboard (experiments.shell.test_paste.PasteTests.test_real_system_clipboard)", + "reason": "Private window station OpenClipboard denied (5); system API gate remains unverified." + } + ], + "log_encoding_note": "CTest localized date header is not UTF-8; only ASCII skip fields are extracted. Raw log hash is preserved.", + "shell_unittest": { + "run": 40, + "passed": 36, + "errors": 1, + "skipped": 3, + "seconds": 121.382 + }, + "product_source_commit": "92eac12d5cd2868b469e8b9a10e68f5e20ee2cb0", + "shell_sha256": "15a149a3d7d47efaf866226559d4b60bbf83e0ff6004a85df41edcb5415ad206" + }, + "vim_environment": { + "vim_sha256": "2e0fda63a89171adfc46c9417d5105cc39ed778cc69a890b83a4eabc34e7646a", + "msys_sha256": "3096d2ba9be20e90be1399edd391bac65d84bc387f5a419b502c9b326081930f", + "initial_run": "Vim absent from process PATH; stage50 is not the original stage59.", + "isolated_intermediate": "Original two-round candidate_shell_tui passed once after process-only PATH fix, 6.95s.", + "full_intermediate": "Original stage59 reproduced in corrected full run; #52 remains open." + }, + "workflow_issue": 87, + "workflow_comparison": [ + { + "label": "baseline", + "shell_sha256": "e81d92a8c7d380d4c33639861180b04935e021bc7c1a5179197c8a9e3c83454a", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "error": null, + "sample_sha256": "213eccd11e89c1e4bbd0e2261ad43cf42772ac7c880ffe13d2ad4c17b52f8dd6" + }, + { + "label": "native_viewport", + "shell_sha256": "4e23e277ea7984d85856b713b3af5b0545d68042c2bbd42a8422f8a6b422c28d", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "error": null, + "sample_sha256": "9aa0079ca1b0ee43e186bd91d633490b80ee16ffc57bcf68f96b5c602994fc56" + } + ], + "workflow_mismatches": [ + { + "run": "run-89580b0cae404944b12e03e031dab1bd", + "bad": [ + { + "cycle": 1, + "name": "F5", + "source_bytes": 16384, + "actual": "TF_R108_F5_00127 \u4e2d\u6587 e\u0301............", + "expected": "TF_R108_F5_00127 \u4e2d\u6587 e\u0301...................................", + "exact_prefix": true, + "output": { + "queued_bytes": 8217, + "high_water_bytes": 8217, + "stopped": false + } + }, + { + "cycle": 1, + "name": "F5", + "source_bytes": 16384, + "actual": "TF_R108_F5_00127 \u4e2d\u6587 e\u0301............", + "expected": "TF_R108_F5_00127 \u4e2d\u6587 e\u0301...................................", + "exact_prefix": true, + "output": { + "queued_bytes": 8217, + "high_water_bytes": 8217, + "stopped": false + } + } + ] + }, + { + "run": "run-8abb195e8f824858adbc0149ce1bbca6", + "bad": [ + { + "cycle": 2, + "name": "F5", + "source_bytes": 16384, + "actual": "TF_R108_F5_00127 \u4e2d\u6587 e\u0301............", + "expected": "TF_R108_F5_00127 \u4e2d\u6587 e\u0301...................................", + "exact_prefix": true, + "output": { + "queued_bytes": 8217, + "high_water_bytes": 8217, + "stopped": false + } + }, + { + "cycle": 2, + "name": "F5", + "source_bytes": 16384, + "actual": "TF_R108_F5_00127 \u4e2d\u6587 e\u0301............", + "expected": "TF_R108_F5_00127 \u4e2d\u6587 e\u0301...................................", + "exact_prefix": true, + "output": { + "queued_bytes": 8217, + "high_water_bytes": 8217, + "stopped": false + } + } + ] + }, + { + "run": "run-9266bc270e244e05a0073d86f00ae8ae", + "bad": [] + } + ], + "workflow_runs": [ + { + "run": "run-3b821fd77c1c4e60870eeee8839d485d", + "validation_sha256": "b5bf6fe8e8dc71d8d9285378095ad9a542a74de68bf82ad4cbeafc1fb3ffae84", + "status": "passed", + "round": 1, + "sample_sha256": "915bfe48f290e082e1c461a167b1d5723217a0285de87d6597b0e2c830b99cf3", + "sessions_sha256": "0acec32f3af02cab9f89949382858ba19c121c1d1b6f8e0b1d06babc640bb39c", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "shell_sha256": "15a149a3d7d47efaf866226559d4b60bbf83e0ff6004a85df41edcb5415ad206" + }, + { + "run": "run-6515c0088c384c7c94542620ff12380b", + "validation_sha256": "450bd976cc67633db7b4a90aa28a3f818c9b188bc6281eb7fca0d3741cc553a1", + "status": "passed", + "round": 0, + "sample_sha256": "57c7f8bab95f8f98fa8b7dbae6110d18e68d4f73d1d689265c2b1fdc7e863e96", + "sessions_sha256": "e5d39eaf107c4dd112f52d1562ec6820033c356904975c4c72024100dac36b06", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "shell_sha256": "15a149a3d7d47efaf866226559d4b60bbf83e0ff6004a85df41edcb5415ad206" + }, + { + "run": "run-89580b0cae404944b12e03e031dab1bd", + "validation_sha256": "2de8f68846c3b76170acffe8bc3e717886d6f0f86b04af9405f9d58cf5df73f8", + "status": "failed", + "round": 0, + "sample_sha256": "3a5150e5e75ac3168080585af7865e1df631ec19d2b2a237f13be4a2e93e5e4f", + "sessions_sha256": "3d65a9c388ea43114b20bf3c6dc7da49825b54529e1eec25bdfc6107acfb2c97", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "shell_sha256": "4e23e277ea7984d85856b713b3af5b0545d68042c2bbd42a8422f8a6b422c28d" + }, + { + "run": "run-8abb195e8f824858adbc0149ce1bbca6", + "validation_sha256": "372adca843f92115d93a307240f58b30708e79c2700e29def8cceb179d508283", + "status": "failed", + "round": 1, + "sample_sha256": "74ceb06312732d0a38499149102f3f683f4a6f62a45961aeef8413ad6e0a20e9", + "sessions_sha256": "d4acc6bbc8f648732bf7e240d9f3613df82e1d4dbff4aa9a221363a122d6fcf2", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "shell_sha256": "4e23e277ea7984d85856b713b3af5b0545d68042c2bbd42a8422f8a6b422c28d" + }, + { + "run": "run-9266bc270e244e05a0073d86f00ae8ae", + "validation_sha256": "758bdc8ef0d865ad457bb898e14187751693d3c65b8240e15d2f3aa32a129083", + "status": "passed", + "round": 0, + "sample_sha256": "d669917b3065530eb1b65900b1ad86ef0b6a1f6bd186dc10cffa0bdddf8b1e0d", + "sessions_sha256": "8081ee0237b2d50a1450ff4c12c4a7824a955ccfde651d56eac659bd2935a2bc", + "exit_code": 0, + "timed_out": false, + "job_after": { + "active": 0, + "total": 77, + "terminated": 0 + }, + "desktop_gone": true, + "shell_sha256": "4e23e277ea7984d85856b713b3af5b0545d68042c2bbd42a8422f8a6b422c28d" + } + ], + "restore_stage1": { + "issue": 88, + "mode": "ui-workspace-restore", + "stage": 1, + "repeat": 1, + "files_sha256": { + "persistence-failure.json": "7ccb0a90b526462a929cbebce42debccfd97b54676e54e0935fbf292826b5e5b", + "restore-geometry.json": "5cc777f65d580599f8bda2d3e7e3d246b63aba19dba55a27e65d358709402638", + "sample.json": "5e546fb36ec65e2d2eb20e9d1047216be33dc8b17eaaa611d995686a1be7d620", + "workspace.layout": "660f3a4064006db91261100c6fb1e1d2fb82f73210575910a29880489dd0cdda" + }, + "result": { + "exit_code": 2, + "timed_out": false, + "job_after": { + "active": 0, + "total": 11, + "terminated": 0 + }, + "desktop_gone": true, + "job_after_cleanup": { + "active": 0, + "total": 11, + "terminated": 0 + } + }, + "tabs": [ + { + "group": "bottom", + "kind": "cmd", + "ready": 1, + "observation_enabled": 1, + "observed_bytes": 230, + "observed_sha256": "68fadc3fc45b9fbaab78b7e1a55987f5fdc11e9a2abdbe39fc65135556703017" + }, + { + "group": "main", + "kind": "cmd", + "ready": 1, + "observation_enabled": 1, + "observed_bytes": 262, + "observed_sha256": "8db2595d8d8546bbb7bb7a6facf79ab72e8f88ee4ce6ec9cf255dbac8528013e" + }, + { + "group": "main", + "kind": "powershell", + "ready": 1, + "observation_enabled": 1, + "observed_bytes": 4373, + "observed_sha256": "06d376c5709d01185a5696726e5ed2a412184b5e1df886ac948c1ac64b349f4c" + } + ], + "cause_confirmed": false, + "note": "Original transports contain output and prompts; bare CR in cmd echo is an unproved oracle hypothesis." + }, + "excluded": [ + { + "affected": [ + "gui-restored-baseline", + "restored-replay" + ], + "validation_status": "invalid_source_binary_pair", + "reason": "Copy-Item retained the old source timestamp; incremental build reused the diagnostic static library and executables. Binary hashes matched the probe, not the restored source. Excluded from baseline comparisons.", + "recorded_utc": "2026-10-26T19:53:23.674551+00:00" + }, + { + "calls": 174, + "elapsed_seconds": 2.6090000001713634, + "reduction_complete": true, + "bytes": 30, + "events": 2, + "transport_sha256": "060ed625d586f873039fb5c6f517b99a6b1ddcab9f854e568a0edcbec91b7da0", + "diagnostic_only": true, + "expected_symptom": "exact TF_SIZE_100x28OW", + "usable_causal_reduction": false, + "reason": "The reduction removed resize and constructed a legitimate overwrite. Symptom equality alone is insufficient; not a regression fixture." + } + ], + "limits": [ + "Original GUI transport and private sample files remain local; only reviewed synthetic native cases are published.", + "Native microprobe does not reply to CPR; snapshots and bounded host query wait perturb timing.", + "Default/system, candidate/system and candidate/official evidence remain separate.", + "Final full run failed #52 Vim stage59 and #88 restore stage1; #87 earlier workflow failures remain unresolved.", + "This is a limited experimental resize fix; #29, #34 and full R2 acceptance remain open." + ], + "presented_done": null, + "windterm_runtime_equivalence": false, + "performance_acceptance": false, + "product_backend_selected": false, + "full_r2_verified": false +} diff --git a/experiments/core/CANDIDATE.md b/experiments/core/CANDIDATE.md index a7f81954..fdab1346 100644 --- a/experiments/core/CANDIDATE.md +++ b/experiments/core/CANDIDATE.md @@ -2,7 +2,7 @@ 按 [ADR-0006](../../docs/adr/0006-confirmed-project-direction.md),用户确认保留本候选代码继续评估,尚未确认最终采用。下述实现、来源与回归结果继续保留;与 WindTerm 2.7.0 的重要差异、性能容差及正式选型分别提交用户确认。 -`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.4` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 +`CandidateTerminal` 是 #27 的拥有值适配层,不是自研核心的替代品声明。它把固定 `0.26.1-dev+termforge.5` 的 `Term/Processor` 接到现有 `Screen`/`Line`;默认 CMake 保留自研核心且不需要 Rust,候选产品由 `TERMFORGE_BUILD_CANDIDATE_ADAPTER=ON` 显式启用。 ## 构建与验证 @@ -26,6 +26,12 @@ Linux 使用相应 CMake 配置。候选 workflow 在 Windows/Ubuntu 构建 `can - 不走 JSON。Rust/C++ 文本由调用方拥有,未做增量 FFI 或性能验收;候选 damage 的产品化仍属后续。 - `screen.resize` 与候选 `Term::resize` 同步尺寸;默认自研回退继续采用 ADR-0005 的物理行裁剪,候选不把它称为完整 reflow。真实 ConPTY 重放可能先由上游把历史拉入可见区再覆盖,候选在完整重放后合并有界历史,按 ID 和完整行键去重,保留 Find/选区所需的稳定身份,且不让冲突产生重复 ID。连续同步缓存原始 FFI ID 与最终身份的映射,仅在需要重放的未知旧 ID 时回退到完整键匹配,不重复扫描全部历史。真实淘汰不恢复,显式清空不复活;普通空白行、未决 reflow、备用屏/滚动区全语义不在此保证内。合同与验证见 [resize/历史记录](results/2026-10-26-resize-replay/validation.json)。 +### 显式原生 viewport resize + +`CandidateResizePolicy::reflow_history` 是构造器默认值。显式官方 ConPTY 构建的候选 `ShellWindow` 选择 `native_viewport`:增高向底部加空行,增宽后可见行合并腾出的空间也保留在底部;历史继续有界保留,但不跨历史/可见区边界参与原生控制台的坐标重排。跨边界的软行关系继续用于文本复制;布局填充不变成正文空格。公开适配器检查六组原生行/光标、历史 ID、完整复制与重复 resize,并检查 0/2/100 行历史上限。 + +`candidate_resize_replay transport.bin plan.txt --native-viewport` 与 `resize_trace.py ... --native-viewport` 显式选择同一策略。缺省命令保留原历史重排行为,未知或额外参数拒绝;重放记录须标出策略,不能用默认策略解释官方候选窗口。原生观测、原因和完整场景边界见[resize 对照](../../docs/research/native-viewport-resize.md)。WindTerm 2.7.0 的对应行为仍需调查,本策略不替代 #29 或 #34 的确认。 + ## 真实产品场景 `candidate_shell_tui` 在同一 `ShellWindow` 和 ConPTY 路径上连续执行两轮真实 Vim:进入全屏编辑、插入文字、resize 到 60×12、退出写盘、确认主屏 shell 返回。本机 Vim 输出未发送 `1049/1047/47`,所以此场景不计作备用屏验收;1049 的固定核心用例与真实 Vim 证据分开。固定自动命令写入 `candidate-vim.vim` 并以短 `-S` 命令加载,避免交互式 cmd 拆分长启动命令。启动和插入由 Vim 写入的真实 `VimEnter`/`InsertEnter` 事件文件确认,避免 `echon` 消息被重绘覆盖;随后通过插入模式的 `Ctrl-O` 调用 Vim 命令,实际保存 `&columns/&lines` 并检查 60×12,再以 `Ctrl-O:wq` 写盘退出。不把 ConPTY 重绘或 `ResizePseudoConsole` 返回当作 Vim 已就绪。本路径不声称验证 Vim 的 Escape 行为,独立控制台 ESC 接收另测。`-W` 日志核对插入文本与 `:wq` 顺序,其中可能含 Vim 内部特殊键,不能冒充纯键盘字节。随后用独立 `shell_query_client.exe` 发出真实 `CSI 6n`,保存终端经 `InputQueue` 返回的光标报告,并用 `shell_size_client.exe` 核对子进程尺寸。另有 resize 后独立 ESC 接收检查。 diff --git a/experiments/core/candidate_resize_replay.cpp b/experiments/core/candidate_resize_replay.cpp index bd470308..a79218f3 100644 --- a/experiments/core/candidate_resize_replay.cpp +++ b/experiments/core/candidate_resize_replay.cpp @@ -29,11 +29,13 @@ size_t decimal(const std::string& token) { int main(int argc, char** argv) { try { - if (argc != 3) throw std::runtime_error("usage: candidate_resize_replay transport.bin plan.txt"); + if (argc != 3 && (argc != 4 || std::string(argv[3]) != "--native-viewport")) + throw std::runtime_error("usage: candidate_resize_replay transport.bin plan.txt [--native-viewport]"); const auto bytes = read_bounded(argv[1]); const auto commands = read_bounded(argv[2]); std::istringstream plan(commands); - termforge::CandidateTerminal terminal; + termforge::CandidateTerminal terminal(24,80,100, argc == 4 + ? termforge::CandidateResizePolicy::native_viewport : termforge::CandidateResizePolicy::reflow_history); size_t offset = 0, step = 0, traced_cells = 0; std::string line; while (std::getline(plan, line)) { diff --git a/experiments/core/candidate_terminal.cpp b/experiments/core/candidate_terminal.cpp index 9bb1fab8..50096b07 100644 --- a/experiments/core/candidate_terminal.cpp +++ b/experiments/core/candidate_terminal.cpp @@ -33,8 +33,9 @@ std::string cell_text(void* impl, size_t row, size_t column, size_t size) { } } // namespace -CandidateTerminal::CandidateTerminal(int rows, int columns, size_t history) +CandidateTerminal::CandidateTerminal(int rows, int columns, size_t history, CandidateResizePolicy resize_policy) : impl_(tf_terminal_new(static_cast(rows), static_cast(columns), history)), + resize_policy_(resize_policy), screen(rows, columns, history) { if (!impl_) throw std::invalid_argument("invalid candidate terminal dimensions"); synchronize(); @@ -258,8 +259,10 @@ void CandidateTerminal::finish() { void CandidateTerminal::resize(int rows, int columns) { std::array error{}; - check(tf_terminal_resize(impl_, static_cast(rows), static_cast(columns), - error.data(), error.size()), "candidate resize failed"); + const auto resize = resize_policy_ == CandidateResizePolicy::native_viewport + ? tf_terminal_resize_native_viewport : tf_terminal_resize; + check(resize(impl_, static_cast(rows), static_cast(columns), + error.data(), error.size()), "candidate resize failed"); begin_replay(); screen.resize(rows, columns); synchronize(); diff --git a/experiments/core/candidate_terminal.hpp b/experiments/core/candidate_terminal.hpp index 55de3176..5c3fef27 100644 --- a/experiments/core/candidate_terminal.hpp +++ b/experiments/core/candidate_terminal.hpp @@ -14,8 +14,11 @@ struct CandidateEvent { std::string bytes; }; +enum class CandidateResizePolicy { reflow_history, native_viewport }; + class CandidateTerminal { void* impl_ = nullptr; + const CandidateResizePolicy resize_policy_; uint64_t mode_bits_ = 0, grapheme_overflows_ = 0; std::unordered_map versions_; std::vector events_; @@ -37,7 +40,8 @@ class CandidateTerminal { public: Screen screen; - CandidateTerminal(int rows = 24, int columns = 80, size_t history = 100); + CandidateTerminal(int rows = 24, int columns = 80, size_t history = 100, + CandidateResizePolicy resize_policy = CandidateResizePolicy::reflow_history); ~CandidateTerminal(); CandidateTerminal(const CandidateTerminal&) = delete; CandidateTerminal& operator=(const CandidateTerminal&) = delete; diff --git a/experiments/core/candidate_terminal_test.cpp b/experiments/core/candidate_terminal_test.cpp index 8f1082c0..5e6ac3a0 100644 --- a/experiments/core/candidate_terminal_test.cpp +++ b/experiments/core/candidate_terminal_test.cpp @@ -510,6 +510,98 @@ void resize_and_errors() { try { terminal.feed(std::string(1024 * 1024 + 1, 'x')); } catch (const std::runtime_error&) { rejected = true; } require(rejected, "oversized feed was accepted"); } + +void resize_preserves_written_spaces() { + for (const auto policy : {termforge::CandidateResizePolicy::reflow_history, + termforge::CandidateResizePolicy::native_viewport}) { + termforge::CandidateTerminal terminal(3,4,100,policy); + terminal.feed("ABC D"); + for (const int width : {4,5,8,4,5}) { + terminal.resize(3,width); + termforge::TextDocument document(terminal.screen); + size_t last = document.size() - 1; + while (last && document.line(last).projection.text.empty()) --last; + termforge::TextSelection selection; + require(selection.set(document, {*document.position(0,0), + *document.position(last,document.line(last).projection.text.size())}), + "written-space selection failed"); + const auto copied = selection.copy(document,4096); + require(copied.status == termforge::SelectionCopyStatus::ok && copied.text == "ABC D", + "resize changed written spaces"); + } + } +} + +void native_viewport_resize() { + struct Case { + int rows, columns, new_rows, new_columns; + const char* source; + std::vector expected; + int row, column; + }; + // Fixed synthetic native CHAR_INFO observations: the native viewport has + // no access to rows already retained in the terminal's scrollback. + const Case cases[] = { + {3,8,6,8,"H0\r\nH1\r\nTOP\r\nMID\r\nBOT",{"TOP","MID","BOT","","",""},2,3}, + {3,8,3,16,"H0\r\nH1\r\n123456789\r\nP>",{"123456789","P>",""},1,2}, + {3,8,3,16,"H0\r\nH1\r\nH2\r\n123456789",{"H2","123456789",""},1,9}, + {3,8,3,5,"H0\r\n123456789012345678901234567890",{"90123","45678","90"},2,2}, + {4,8,4,10,"H0\r\nH1\r\nABCDEFGHIJKLMNO\r\nBOTTOM\x1b[3;2H",{"H1","ABCDEFGHIJ","KLMNO","BOTTOM"},1,9}, + {3,8,3,16,"H0\r\n123456789012345678901234567890",{"9012345678901234","567890",""},1,6}, + }; + for (const auto& test : cases) { + termforge::CandidateTerminal terminal(test.rows,test.columns,100, + termforge::CandidateResizePolicy::native_viewport); + terminal.feed(std::string("\x1b[2J\x1b[H") + test.source); + require(!terminal.screen.history.empty(), "native resize fixture lacks history"); + const auto retained = terminal.screen.history.front().id; + const auto copy_text = [&] { + termforge::TextDocument document(terminal.screen); + size_t last = document.size() - 1; + while (last && document.line(last).projection.text.empty()) --last; + const auto length = document.line(last).projection.text.size(); + termforge::TextSelection selection; + require(selection.set(document, {*document.position(0, 0), *document.position(last, length)}), + "native viewport whole-text selection failed"); + const auto copied = selection.copy(document, 4096); + require(copied.status == termforge::SelectionCopyStatus::ok, "native viewport whole-text copy failed"); + return copied.text; + }; + std::string expected_text = test.source; + if (const auto cursor_move = expected_text.find('\x1b'); cursor_move != std::string::npos) + expected_text.resize(cursor_move); + require(copy_text() == expected_text, "native viewport source text differs before resize"); + terminal.resize(test.new_rows,test.new_columns); + require(terminal.screen.lines.size() == test.expected.size(), "native viewport row count differs"); + for (size_t row=0; row