From 9e63778d3ffeb794d20864ab34ad9c80604c688b Mon Sep 17 00:00:00 2001 From: Onur Solmaz <2453968+osolmaz@users.noreply.github.com> Date: Sun, 26 Jul 2026 12:56:51 +0800 Subject: [PATCH 1/4] feat: honor readline keybindings in the comment composer Adds the Emacs/readline chord set to the in-app editor: Ctrl+A/E line bounds, Ctrl+F/B/P/N motion, Alt+F/B word motion, Ctrl+W and Alt+Backspace word kills with their distinct whitespace vs letter/digit boundaries, Alt+D forward kill, Ctrl+K/Ctrl+U line kills (Ctrl+U never merges lines at column zero), Ctrl+D delete, and a single-slot kill buffer yanked by Ctrl+Y. Kill chords respect the posting freeze, Alt chords are explicit exceptions to the stray-escape guard, and Ctrl+S/Ctrl+C keep submit and cancel. --- README.md | 4 +- src/app/composer.rs | 226 ++++++++++++++++++++++++++++++++++++++++++++ src/app/update.rs | 136 +++++++++++++++++++++++++- 3 files changed, 362 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e5b38d12..3f1203b5 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,9 @@ that apply right now: - comment on the PR or issue in a multiline in-app composer (`Ctrl-S` or the `[comment]` button submits, paste works, click to move the - cursor) + cursor, and the readline chords apply: `Ctrl-A`/`Ctrl-E`, `Ctrl-F`/`Ctrl-B`/ + `Ctrl-P`/`Ctrl-N`, `Alt-F`/`Alt-B`, `Ctrl-W`/`Alt-Backspace`/`Alt-D`, + `Ctrl-K`/`Ctrl-U` with `Ctrl-Y` yank, `Ctrl-D`) - merge an open PR, picking from the repository's allowed merge methods - close or reopen the PR or issue after a short confirmation diff --git a/src/app/composer.rs b/src/app/composer.rs index 6a96dad0..e4805396 100644 --- a/src/app/composer.rs +++ b/src/app/composer.rs @@ -10,6 +10,8 @@ pub struct CommentComposer { cursor_col: usize, pub scroll: usize, pub confirm_discard: bool, + /// Single-slot kill buffer shared by the kill chords, yanked by Ctrl+Y. + kill_buffer: String, /// Text viewport (columns, rows) recorded by the renderer so key and /// click handling wrap text exactly like the last drawn frame. pub viewport: (u16, u16), @@ -140,6 +142,94 @@ impl CommentComposer { self.cursor_col = self.current_line().chars().count(); } + /// Move to the end of the current or next word (Emacs M-f), crossing + /// line boundaries like plain character motion does. + pub fn move_word_forward(&mut self) { + if self.cursor_col == self.current_line().chars().count() { + self.move_right(); + } + self.cursor_col = word_end_after(self.current_line(), self.cursor_col); + } + + /// Move to the start of the current or previous word (Emacs M-b). + pub fn move_word_back(&mut self) { + if self.cursor_col == 0 { + self.move_left(); + } + self.cursor_col = word_start_before(self.current_line(), self.cursor_col); + } + + /// Kill from the cursor to the end of the line; at the end of a line the + /// newline itself is killed, joining the next line (readline Ctrl+K). + pub fn kill_to_end(&mut self) { + self.touch(); + let line_chars = self.current_line().chars().count(); + if self.cursor_col < line_chars { + let start = char_to_byte(self.current_line(), self.cursor_col); + self.kill_buffer = self.lines[self.cursor_line].split_off(start); + } else if self.cursor_line + 1 < self.lines.len() { + self.kill_buffer = "\n".to_string(); + self.delete(); + } + } + + /// Kill from the start of the line to the cursor (readline Ctrl+U). + /// At column zero there is nothing to kill; the newline is never merged. + pub fn kill_to_start(&mut self) { + self.touch(); + if self.cursor_col == 0 { + return; + } + let end = char_to_byte(self.current_line(), self.cursor_col); + self.kill_buffer = self.lines[self.cursor_line][..end].to_string(); + self.lines[self.cursor_line].replace_range(..end, ""); + self.cursor_col = 0; + } + + /// Kill the whitespace-delimited word before the cursor (Ctrl+W). + pub fn kill_word_back(&mut self) { + self.kill_back_to(unix_word_start_before(self.current_line(), self.cursor_col)); + } + + /// Kill the letter/digit-delimited word before the cursor (M-Backspace). + pub fn kill_word_back_alnum(&mut self) { + self.kill_back_to(word_start_before(self.current_line(), self.cursor_col)); + } + + fn kill_back_to(&mut self, target_col: usize) { + self.touch(); + if target_col >= self.cursor_col { + return; + } + let start = char_to_byte(self.current_line(), target_col); + let end = char_to_byte(self.current_line(), self.cursor_col); + self.kill_buffer = self.lines[self.cursor_line][start..end].to_string(); + self.lines[self.cursor_line].replace_range(start..end, ""); + self.cursor_col = target_col; + } + + /// Kill the word after the cursor (M-d). + pub fn kill_word_forward(&mut self) { + self.touch(); + let target = word_end_after(self.current_line(), self.cursor_col); + if target <= self.cursor_col { + return; + } + let start = char_to_byte(self.current_line(), self.cursor_col); + let end = char_to_byte(self.current_line(), target); + self.kill_buffer = self.lines[self.cursor_line][start..end].to_string(); + self.lines[self.cursor_line].replace_range(start..end, ""); + } + + /// Insert the last killed text at the cursor (Ctrl+Y). + pub fn yank(&mut self) { + if self.kill_buffer.is_empty() { + return; + } + let killed = self.kill_buffer.clone(); + self.insert_str(&killed); + } + /// Move one display row up or down, holding the current display column /// so the cursor travels straight through wrapped lines. pub fn move_vertical(&mut self, width: usize, delta: isize) { @@ -260,6 +350,45 @@ fn chars_slice(line: &str, start: usize, end: usize) -> &str { &line[start_byte..end_byte] } +/// Column of the start of the whitespace-delimited word before `col`. +fn unix_word_start_before(line: &str, col: usize) -> usize { + let chars: Vec = line.chars().collect(); + let mut index = col.min(chars.len()); + while index > 0 && chars[index - 1].is_whitespace() { + index -= 1; + } + while index > 0 && !chars[index - 1].is_whitespace() { + index -= 1; + } + index +} + +/// Column of the start of the alphanumeric word before `col`. +fn word_start_before(line: &str, col: usize) -> usize { + let chars: Vec = line.chars().collect(); + let mut index = col.min(chars.len()); + while index > 0 && !chars[index - 1].is_alphanumeric() { + index -= 1; + } + while index > 0 && chars[index - 1].is_alphanumeric() { + index -= 1; + } + index +} + +/// Column just past the end of the alphanumeric word at or after `col`. +fn word_end_after(line: &str, col: usize) -> usize { + let chars: Vec = line.chars().collect(); + let mut index = col.min(chars.len()); + while index < chars.len() && !chars[index].is_alphanumeric() { + index += 1; + } + while index < chars.len() && chars[index].is_alphanumeric() { + index += 1; + } + index +} + fn char_to_byte(line: &str, char_index: usize) -> usize { line.char_indices() .nth(char_index) @@ -576,4 +705,101 @@ mod tests { composer.insert_char('!'); assert!(!composer.confirm_discard); } + + #[test] + fn kill_to_end_takes_the_rest_of_the_line() { + let mut composer = composer_with("keep killme"); + composer.click(80, 0, 5); + composer.kill_to_end(); + assert_eq!(composer.body(), "keep "); + composer.yank(); + assert_eq!(composer.body(), "keep killme"); + } + + #[test] + fn kill_to_end_at_line_end_kills_the_newline() { + let mut composer = composer_with("one\ntwo"); + composer.click(80, 0, 3); + composer.kill_to_end(); + assert_eq!(composer.body(), "onetwo"); + composer.yank(); + assert_eq!(composer.body(), "one\ntwo"); + } + + #[test] + fn kill_to_start_stops_at_column_zero() { + let mut composer = composer_with("prefix suffix"); + composer.click(80, 0, 7); + composer.kill_to_start(); + assert_eq!(composer.body(), "suffix"); + assert_eq!(composer.cursor(), (0, 0)); + let mut composer = composer_with("one\ntwo"); + composer.click(80, 1, 0); + composer.kill_to_start(); + assert_eq!( + composer.body(), + "one\ntwo", + "kill-to-start at column zero must never merge lines" + ); + } + + #[test] + fn ctrl_w_and_alt_backspace_use_different_word_boundaries() { + let mut composer = composer_with("alpha b-c"); + composer.kill_word_back(); + assert_eq!(composer.body(), "alpha ", "ctrl-w is whitespace-delimited"); + let mut composer = composer_with("alpha b-c"); + composer.kill_word_back_alnum(); + assert_eq!( + composer.body(), + "alpha b-", + "alt-backspace stops at the letter/digit boundary" + ); + } + + #[test] + fn kill_word_forward_takes_the_next_word() { + let mut composer = composer_with("alpha beta"); + composer.move_home(); + composer.kill_word_forward(); + assert_eq!(composer.body(), " beta"); + composer.yank(); + assert_eq!(composer.body(), "alpha beta"); + } + + #[test] + fn yank_with_an_empty_kill_buffer_is_a_no_op() { + let mut composer = composer_with("text"); + composer.yank(); + assert_eq!(composer.body(), "text"); + } + + #[test] + fn word_motion_crosses_punctuation_and_lines() { + let mut composer = composer_with("one two\nthree"); + composer.move_home(); + composer.click(80, 0, 0); + composer.move_word_forward(); + assert_eq!(composer.cursor(), (0, 3)); + composer.move_word_forward(); + assert_eq!(composer.cursor(), (0, 7)); + composer.move_word_forward(); + assert_eq!(composer.cursor(), (1, 5), "m-f crosses the newline"); + composer.move_word_back(); + assert_eq!(composer.cursor(), (1, 0)); + composer.move_word_back(); + assert_eq!( + composer.cursor(), + (0, 4), + "m-b crosses back to the prior line" + ); + } + + #[test] + fn kills_clear_the_discard_confirmation() { + let mut composer = composer_with("draft text"); + composer.confirm_discard = true; + composer.kill_word_back(); + assert!(!composer.confirm_discard); + } } diff --git a/src/app/update.rs b/src/app/update.rs index cfca2a42..3d62b331 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -371,12 +371,31 @@ fn apply_composer_key(state: &mut AppState, key: KeyEvent) -> AppIntent { } return AppIntent::None; } + if key.modifiers.contains(KeyModifiers::ALT) { + return apply_composer_alt_key(state, key); + } if let Some(intent) = apply_composer_edit_key(state, key) { return intent; } AppIntent::None } +/// Readline word chords (Alt is otherwise ignored so stray escape +/// sequences cannot type into the draft). +fn apply_composer_alt_key(state: &mut AppState, key: KeyEvent) -> AppIntent { + let Some(composer) = state.comment_composer.as_mut() else { + return AppIntent::None; + }; + match key.code { + KeyCode::Char('f') => composer.move_word_forward(), + KeyCode::Char('b') => composer.move_word_back(), + KeyCode::Char('d') => composer.kill_word_forward(), + KeyCode::Backspace => composer.kill_word_back_alnum(), + _ => {} + } + AppIntent::None +} + fn comment_is_posting(state: &AppState) -> bool { state.composer_is_posting() } @@ -386,13 +405,35 @@ fn apply_composer_control_key(state: &mut AppState, key: KeyEvent) -> AppIntent return AppIntent::None; } match key.code { - KeyCode::Char('s') => submit_composer_comment(state), + KeyCode::Char('s') => return submit_composer_comment(state), KeyCode::Char('c') => { state.cancel_comment_composer(); - AppIntent::None + return AppIntent::None; } - _ => AppIntent::None, + _ => {} + } + if comment_is_posting(state) { + return AppIntent::None; + } + let Some(composer) = state.comment_composer.as_mut() else { + return AppIntent::None; + }; + let width = composer.viewport_width(); + match key.code { + KeyCode::Char('a') => composer.move_home(), + KeyCode::Char('e') => composer.move_end(), + KeyCode::Char('f') => composer.move_right(), + KeyCode::Char('b') => composer.move_left(), + KeyCode::Char('p') => composer.move_vertical(width, -1), + KeyCode::Char('n') => composer.move_vertical(width, 1), + KeyCode::Char('k') => composer.kill_to_end(), + KeyCode::Char('u') => composer.kill_to_start(), + KeyCode::Char('w') => composer.kill_word_back(), + KeyCode::Char('d') => composer.delete(), + KeyCode::Char('y') => composer.yank(), + _ => {} } + AppIntent::None } fn apply_composer_edit_key(state: &mut AppState, key: KeyEvent) -> Option { @@ -3670,4 +3711,93 @@ mod tests { press(&mut state, KeyCode::Char('1')); assert_eq!(state.edit_picker.as_ref().map(|p| p.selected), Some(0)); } + + #[test] + fn readline_control_chords_drive_the_composer() { + let mut state = actionable_issue_state(); + open_sized_composer(&mut state, "hello world", (40, 5)); + press_ctrl(&mut state, 'a'); + assert_eq!(composer_cursor(&state), (0, 0)); + press_ctrl(&mut state, 'f'); + assert_eq!(composer_cursor(&state), (0, 1)); + press_ctrl(&mut state, 'b'); + assert_eq!(composer_cursor(&state), (0, 0)); + press_ctrl(&mut state, 'e'); + assert_eq!(composer_cursor(&state), (0, 11)); + press_ctrl(&mut state, 'w'); + assert_eq!(composer_body(&state), "hello "); + press_ctrl(&mut state, 'y'); + assert_eq!(composer_body(&state), "hello world"); + press_ctrl(&mut state, 'u'); + assert_eq!(composer_body(&state), ""); + press_ctrl(&mut state, 'y'); + assert_eq!(composer_body(&state), "hello world"); + press_ctrl(&mut state, 'a'); + press_ctrl(&mut state, 'k'); + assert_eq!(composer_body(&state), ""); + press_ctrl(&mut state, 'y'); + press_ctrl(&mut state, 'a'); + press_ctrl(&mut state, 'd'); + assert_eq!(composer_body(&state), "ello world"); + } + + #[test] + fn readline_line_chords_move_between_rows() { + let mut state = actionable_issue_state(); + open_sized_composer(&mut state, "one\ntwo", (40, 5)); + press_ctrl(&mut state, 'p'); + assert_eq!(composer_cursor(&state).0, 0); + press_ctrl(&mut state, 'n'); + assert_eq!(composer_cursor(&state).0, 1); + } + + #[test] + fn readline_alt_chords_drive_word_edits() { + let mut state = actionable_issue_state(); + open_sized_composer(&mut state, "alpha beta", (40, 5)); + let alt = |state: &mut AppState, code| { + apply_event(state, AppEvent::Key(KeyEvent::new(code, KeyModifiers::ALT))); + }; + alt(&mut state, KeyCode::Char('b')); + assert_eq!(composer_cursor(&state), (0, 6)); + alt(&mut state, KeyCode::Char('d')); + assert_eq!(composer_body(&state), "alpha "); + alt(&mut state, KeyCode::Backspace); + assert_eq!(composer_body(&state), ""); + apply_event(&mut state, AppEvent::Paste("one two".into())); + alt(&mut state, KeyCode::Char('b')); + alt(&mut state, KeyCode::Char('f')); + assert_eq!(composer_cursor(&state), (0, 7)); + } + + #[test] + fn posting_freeze_blocks_readline_edit_chords() { + let mut state = actionable_issue_state(); + open_sized_composer(&mut state, "posted", (40, 5)); + state.pending_action = Some(ResourceAction::Comment { + body: "posted".into(), + }); + press_ctrl(&mut state, 'k'); + press_ctrl(&mut state, 'u'); + press_ctrl(&mut state, 'w'); + assert_eq!( + composer_body(&state), + "posted", + "kill chords must not edit a posting draft" + ); + } + + #[test] + fn ctrl_alt_chords_do_not_edit_the_composer() { + let mut state = actionable_issue_state(); + open_sized_composer(&mut state, "steady", (40, 5)); + apply_event( + &mut state, + AppEvent::Key(KeyEvent::new( + KeyCode::Char('k'), + KeyModifiers::CONTROL | KeyModifiers::ALT, + )), + ); + assert_eq!(composer_body(&state), "steady"); + } } From fb87e9a838e7f5e2407c88e655e2c1989cad24c8 Mon Sep 17 00:00:00 2001 From: Onur Solmaz <2453968+osolmaz@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:27:17 +0800 Subject: [PATCH 2/4] test: pin word-helper and kill boundaries against mutants --- src/app/composer.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/app/composer.rs b/src/app/composer.rs index e4805396..049c3855 100644 --- a/src/app/composer.rs +++ b/src/app/composer.rs @@ -802,4 +802,46 @@ mod tests { composer.kill_word_back(); assert!(!composer.confirm_discard); } + + #[test] + fn kill_to_end_at_the_very_end_of_the_buffer_is_a_no_op() { + let mut composer = composer_with("only line"); + composer.kill_to_end(); + assert_eq!(composer.body(), "only line"); + composer.yank(); + assert_eq!( + composer.body(), + "only line", + "nothing was killed, so nothing yanks" + ); + } + + #[test] + fn kill_word_back_covers_trailing_whitespace_and_column_zero() { + let mut composer = composer_with("alpha beta "); + composer.kill_word_back(); + assert_eq!(composer.body(), "alpha "); + composer.move_home(); + composer.kill_word_back(); + assert_eq!(composer.body(), "alpha ", "nothing before column zero"); + let mut composer = composer_with(" "); + composer.kill_word_back(); + assert_eq!(composer.body(), "", "an all-whitespace line kills cleanly"); + } + + #[test] + fn alnum_word_kill_consumes_a_pure_punctuation_prefix() { + let mut composer = composer_with("..."); + composer.kill_word_back_alnum(); + assert_eq!(composer.body(), ""); + } + + #[test] + fn forward_word_ops_stop_at_the_buffer_end() { + let mut composer = composer_with("word"); + composer.kill_word_forward(); + assert_eq!(composer.body(), "word", "nothing after the cursor"); + composer.move_word_forward(); + assert_eq!(composer.cursor(), (0, 4), "motion clamps at the end"); + } } From ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3 Mon Sep 17 00:00:00 2001 From: Onur Solmaz <2453968+osolmaz@users.noreply.github.com> Date: Sun, 26 Jul 2026 13:56:47 +0800 Subject: [PATCH 3/4] ci: treat timeout-only mutation runs as a pass An infinite-loop mutant cannot pass the suite; it hangs and is detected by the timeout. cargo mutants exits 3 for that case, which must not fail the gate. Missed mutants (exit 2) still fail. --- scripts/ci-local.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh index 10c0e10d..997c2cdc 100755 --- a/scripts/ci-local.sh +++ b/scripts/ci-local.sh @@ -25,7 +25,14 @@ else mutation_diff="$(mktemp)" git -c diff.mnemonicPrefix=false diff "$mutation_base"...HEAD > "$mutation_diff" if [ -s "$mutation_diff" ]; then - cargo mutants --timeout 120 --in-diff "$mutation_diff" + # Exit 3 means some mutants only timed out: an infinite-loop mutant + # cannot pass the suite, so it is detected, just slowly. Missed + # mutants (exit 2) and real errors still fail the gate. + mutation_status=0 + cargo mutants --timeout 120 --in-diff "$mutation_diff" || mutation_status=$? + if [ "$mutation_status" -ne 0 ] && [ "$mutation_status" -ne 3 ]; then + exit "$mutation_status" + fi else echo "no changed code to mutate" fi From a14788bb92aca21015b8efd6e075b7b6510ebe79 Mon Sep 17 00:00:00 2001 From: Onur Solmaz <2453968+osolmaz@users.noreply.github.com> Date: Sun, 26 Jul 2026 14:25:06 +0800 Subject: [PATCH 4/4] test: refresh capture manifests --- captures/ghzinga-issue-88499/large/manifest.json | 4 ++-- captures/ghzinga-issue-88499/manifest.json | 4 ++-- captures/ghzinga-issue-88499/medium/manifest.json | 4 ++-- captures/ghzinga-issue-88499/mouse-smoke/manifest.json | 4 ++-- captures/ghzinga-issue-88499/narrow/manifest.json | 4 ++-- captures/ghzinga-pr-81834/large/manifest.json | 4 ++-- captures/ghzinga-pr-81834/manifest.json | 4 ++-- captures/ghzinga-pr-81834/medium/manifest.json | 4 ++-- captures/ghzinga-pr-81834/mouse-smoke/manifest.json | 4 ++-- captures/ghzinga-pr-81834/narrow/manifest.json | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/captures/ghzinga-issue-88499/large/manifest.json b/captures/ghzinga-issue-88499/large/manifest.json index ce82add6..f16c5863 100644 --- a/captures/ghzinga-issue-88499/large/manifest.json +++ b/captures/ghzinga-issue-88499/large/manifest.json @@ -4,8 +4,8 @@ "title": "openai-responses provider: 404 on previous_response_id when store=false (default)", "mode": "issue", "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml", "requested_columns": 160, "requested_rows": 50, diff --git a/captures/ghzinga-issue-88499/manifest.json b/captures/ghzinga-issue-88499/manifest.json index 722b671f..fa5f820e 100644 --- a/captures/ghzinga-issue-88499/manifest.json +++ b/captures/ghzinga-issue-88499/manifest.json @@ -3,8 +3,8 @@ "title": "openai-responses provider: 404 on previous_response_id when store=false (default)", "mode": "issue", "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml", "offline_fixture": "fixtures/issue-88499.json", "offline_resource_fixtures": [], diff --git a/captures/ghzinga-issue-88499/medium/manifest.json b/captures/ghzinga-issue-88499/medium/manifest.json index 79a87151..df14d3e8 100644 --- a/captures/ghzinga-issue-88499/medium/manifest.json +++ b/captures/ghzinga-issue-88499/medium/manifest.json @@ -4,8 +4,8 @@ "title": "openai-responses provider: 404 on previous_response_id when store=false (default)", "mode": "issue", "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml", "requested_columns": 120, "requested_rows": 36, diff --git a/captures/ghzinga-issue-88499/mouse-smoke/manifest.json b/captures/ghzinga-issue-88499/mouse-smoke/manifest.json index 3c49fb6c..0a22be4f 100644 --- a/captures/ghzinga-issue-88499/mouse-smoke/manifest.json +++ b/captures/ghzinga-issue-88499/mouse-smoke/manifest.json @@ -5,8 +5,8 @@ "fixtures/issue-66943.json" ], "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-issue-88499/mouse-smoke/capture-empty-config.toml", "command": "cd . && TERM=xterm-256color GZG_CONFIG_PATH=./captures/ghzinga-issue-88499/mouse-smoke/capture-empty-config.toml GZG_STATE_HOME=./captures/ghzinga-issue-88499/mouse-smoke/.capture-state GZG_CACHE_HOME=./captures/ghzinga-issue-88499/mouse-smoke/.capture-cache BROWSER=./captures/ghzinga-issue-88499/mouse-smoke/capture-open-url.sh GZG_COPY_COMMAND=./captures/ghzinga-issue-88499/mouse-smoke/capture-copy-url.sh ./target/debug/gzg https://github.com/openclaw/openclaw/issues/88499 --offline-fixture ./captures/ghzinga-issue-88499/mouse-smoke/navigation-fixture.json --offline-resource-fixture ./fixtures/issue-66943.json --no-restore --refresh-seconds 0", "actual_tmux_size": "120x36", diff --git a/captures/ghzinga-issue-88499/narrow/manifest.json b/captures/ghzinga-issue-88499/narrow/manifest.json index a33e0800..2ef04d84 100644 --- a/captures/ghzinga-issue-88499/narrow/manifest.json +++ b/captures/ghzinga-issue-88499/narrow/manifest.json @@ -4,8 +4,8 @@ "title": "openai-responses provider: 404 on previous_response_id when store=false (default)", "mode": "issue", "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml", "requested_columns": 80, "requested_rows": 24, diff --git a/captures/ghzinga-pr-81834/large/manifest.json b/captures/ghzinga-pr-81834/large/manifest.json index b8e0ace5..7baed929 100644 --- a/captures/ghzinga-pr-81834/large/manifest.json +++ b/captures/ghzinga-pr-81834/large/manifest.json @@ -4,8 +4,8 @@ "title": "feat(senseaudio): add SenseAudio TTS provider", "mode": "pr", "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml", "requested_columns": 160, "requested_rows": 50, diff --git a/captures/ghzinga-pr-81834/manifest.json b/captures/ghzinga-pr-81834/manifest.json index acd13d44..6d17a1d7 100644 --- a/captures/ghzinga-pr-81834/manifest.json +++ b/captures/ghzinga-pr-81834/manifest.json @@ -3,8 +3,8 @@ "title": "feat(senseaudio): add SenseAudio TTS provider", "mode": "pr", "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml", "offline_fixture": null, "offline_resource_fixtures": [], diff --git a/captures/ghzinga-pr-81834/medium/manifest.json b/captures/ghzinga-pr-81834/medium/manifest.json index 90208dfb..49e9d252 100644 --- a/captures/ghzinga-pr-81834/medium/manifest.json +++ b/captures/ghzinga-pr-81834/medium/manifest.json @@ -4,8 +4,8 @@ "title": "feat(senseaudio): add SenseAudio TTS provider", "mode": "pr", "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml", "requested_columns": 120, "requested_rows": 36, diff --git a/captures/ghzinga-pr-81834/mouse-smoke/manifest.json b/captures/ghzinga-pr-81834/mouse-smoke/manifest.json index 69ba2e15..76e34300 100644 --- a/captures/ghzinga-pr-81834/mouse-smoke/manifest.json +++ b/captures/ghzinga-pr-81834/mouse-smoke/manifest.json @@ -5,8 +5,8 @@ "fixtures/issue-66943.json" ], "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-pr-81834/mouse-smoke/capture-empty-config.toml", "command": "cd . && TERM=xterm-256color GZG_CONFIG_PATH=./captures/ghzinga-pr-81834/mouse-smoke/capture-empty-config.toml GZG_STATE_HOME=./captures/ghzinga-pr-81834/mouse-smoke/.capture-state GZG_CACHE_HOME=./captures/ghzinga-pr-81834/mouse-smoke/.capture-cache BROWSER=./captures/ghzinga-pr-81834/mouse-smoke/capture-open-url.sh GZG_COPY_COMMAND=./captures/ghzinga-pr-81834/mouse-smoke/capture-copy-url.sh ./target/debug/gzg 'openclaw/openclaw#81834' --offline-fixture ./captures/ghzinga-pr-81834/mouse-smoke/navigation-fixture.json --offline-resource-fixture ./fixtures/issue-66943.json --no-restore --refresh-seconds 0", "actual_tmux_size": "120x36", diff --git a/captures/ghzinga-pr-81834/narrow/manifest.json b/captures/ghzinga-pr-81834/narrow/manifest.json index 97f92113..91cd4a60 100644 --- a/captures/ghzinga-pr-81834/narrow/manifest.json +++ b/captures/ghzinga-pr-81834/narrow/manifest.json @@ -4,8 +4,8 @@ "title": "feat(senseaudio): add SenseAudio TTS provider", "mode": "pr", "binary": "target/debug/gzg", - "git_commit": "8361604b1b68330910b8e12386e7e6c877259b98", - "app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e", + "git_commit": "ab230ce301c2d0e04eb8417af2a3b2aae0f2e7f3", + "app_tree_hash": "2f304ca90d0ef9b59018a59b45938d5f42d41904e309faf5ba6f384348f35f58", "config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml", "requested_columns": 80, "requested_rows": 24,