Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion experiments/shell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ R1 的 [#12–#17 限定合同](../../docs/milestones/R1-local-text.md)和 [#18

## 选区丢弃诊断

几何/DPI/缩放变化后若选区被 `TextSelection::reconcile` 丢弃,可用环境变量 `TERMFORGE_SELECTION_DIAG=<jsonl 路径>` 打开一次性诊断:每次丢弃写入一行 `{"schema":"termforge-selection-discard-v1","reason":"...","row":N}`(`row` 是发生不一致的选区行序号,`0` 表示与具体行无关的分支),`reason` 取 `epoch-mismatch`、`anchor-missing-or-shrunk`、`row-identity-replaced`、`row-text-changed`、`endpoint-unavailable` 之一。未设置该变量时不写任何文件、行为不变;`experiments/shell/run.py` 的 `execute()` 会自动把该变量指向本次场景产物目录下的 `selection-discard.jsonl`,因此失败时该文件随 `build/test-artifacts/` 一起保留。`selection_core` 里新增的分组只验证观察器能收到分支名。
几何/DPI/缩放变化后若选区被 `TextSelection::reconcile` 丢弃,可用环境变量 `TERMFORGE_SELECTION_DIAG=<jsonl 路径>` 打开一次性诊断:每次丢弃写入一行 `{"schema":"termforge-selection-discard-v1","reason":"...","row":N}`(`row` 是发生不一致的选区行序号,`0` 表示与具体行无关的分支),`reason` 取 `epoch-mismatch`、`anchor-missing-or-shrunk`、`row-identity-replaced`、`row-text-changed`、`endpoint-unavailable` 之一。未设置该变量时不写任何文件、行为不变;另有 `TERMFORGE_CAPTURE_OUTPUT=<路径>` 会把该窗口消费的**全部输出字节**追加写入文件(同样 opt-in、默认不写),供失败时逐字节回放;`experiments/shell/run.py` 的 `execute()` 会自动把该变量指向本次场景产物目录下的 `selection-discard.jsonl`,因此失败时该文件随 `build/test-artifacts/` 一起保留。`selection_core` 里新增的分组只验证观察器能收到分支名。

## 所有权与调度

Expand Down
1 change: 1 addition & 0 deletions experiments/shell/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ def execute(exe, mode, out, timeout=40, settings_source=None, *, resources=False
# the variable is set, and the path stays inside the preserved artifacts.
environment = dict(os.environ)
environment.setdefault("TERMFORGE_SELECTION_DIAG", str((out / "selection-discard.jsonl").resolve()))
environment.setdefault("TERMFORGE_CAPTURE_OUTPUT", str((out / "output.bin").resolve()))
process = subprocess.Popen([str(exe.with_name("shell_test_launcher.exe").resolve()), desktop.full_name,
mode, str((out / "frame.bmp").resolve())],
cwd=out, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
Expand Down
18 changes: 15 additions & 3 deletions experiments/shell/shell.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,17 @@ namespace {
// Opt-in diagnostic for the selection-discard path, used when a hosted geometry
// change drops a live selection. Gated by TERMFORGE_SELECTION_DIAG=<jsonl path>;
// without it nothing is written and no behaviour changes.
std::string selection_diag_path() {
std::string env_path(const char* name) {
#ifdef _MSC_VER
char* buffer = nullptr; size_t size = 0;
if (_dupenv_s(&buffer, &size, "TERMFORGE_SELECTION_DIAG") != 0 || !buffer) return {};
if (_dupenv_s(&buffer, &size, name) != 0 || !buffer) return {};
std::string value(buffer); std::free(buffer); return value;
#else
const char* value = std::getenv("TERMFORGE_SELECTION_DIAG");
const char* value = std::getenv(name);
return value ? std::string(value) : std::string{};
#endif
}
std::string selection_diag_path() { return env_path("TERMFORGE_SELECTION_DIAG"); }
void selection_discard_diagnostic(const termforge::TextSelection::DiscardReport& report) {
static const std::string path = selection_diag_path();
if (path.empty()) return;
Expand Down Expand Up @@ -104,6 +105,7 @@ class ShellWindow {
tf_shell_test::HostedImeObservation ime_observation_;
inline static uint64_t next_identity_ = 0;
const uint64_t identity_ = ++next_identity_;
std::ofstream capture_output_;
std::unique_ptr<tf_shell::ClipboardReadJob> clipboard_read_;
std::optional<tf_shell::PreparedPaste> pending_paste_;
std::unique_ptr<tf_shell::PastePrompt> paste_prompt_;
Expand Down Expand Up @@ -809,6 +811,13 @@ class ShellWindow {
observed_output_.append(bytes);
if(observed_output_.size()>8192) observed_output_.erase(0,observed_output_.size()-8192);
}
// Opt-in full capture: TERMFORGE_CAPTURE_OUTPUT=<path> appends every
// consumed chunk so a hosted failure can be replayed byte for byte.
// Nothing is written unless the variable is set.
if (capture_output_) {
capture_output_.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
capture_output_.flush();
}
parser_.feed(bytes); batch += bytes.size();
#ifdef TERMFORGE_CANDIDATE_CORE
route_terminal_events();
Expand Down Expand Up @@ -992,6 +1001,9 @@ class ShellWindow {
DispatchMessageW(&message);
}
ShellWindow(std::string mode, std::string image) : mode_(std::move(mode)), image_path_(std::move(image)) {
// Opt-in full output capture for replayable failure evidence.
const std::string capture = env_path("TERMFORGE_CAPTURE_OUTPUT");
if (!capture.empty()) capture_output_.open(capture, std::ios::binary | std::ios::app);
snapshot_ = std::make_shared<const Snapshot>(viewport_.snapshot(parser_.screen), 0);
}
~ShellWindow() {
Expand Down
Loading