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
4 changes: 2 additions & 2 deletions captures/ghzinga-issue-88499/large/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml",
"requested_columns": 160,
"requested_rows": 50,
Expand Down
4 changes: 2 additions & 2 deletions captures/ghzinga-issue-88499/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml",
"offline_fixture": "fixtures/issue-88499.json",
"offline_resource_fixtures": [],
Expand Down
4 changes: 2 additions & 2 deletions captures/ghzinga-issue-88499/medium/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml",
"requested_columns": 120,
"requested_rows": 36,
Expand Down
4 changes: 2 additions & 2 deletions captures/ghzinga-issue-88499/mouse-smoke/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"fixtures/issue-66943.json"
],
"binary": "target/debug/gzg",
"git_commit": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"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",
Expand Down
4 changes: 2 additions & 2 deletions captures/ghzinga-issue-88499/narrow/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml",
"requested_columns": 80,
"requested_rows": 24,
Expand Down
4 changes: 2 additions & 2 deletions captures/ghzinga-pr-81834/large/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"title": "feat(senseaudio): add SenseAudio TTS provider",
"mode": "pr",
"binary": "target/debug/gzg",
"git_commit": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml",
"requested_columns": 160,
"requested_rows": 50,
Expand Down
4 changes: 2 additions & 2 deletions captures/ghzinga-pr-81834/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"title": "feat(senseaudio): add SenseAudio TTS provider",
"mode": "pr",
"binary": "target/debug/gzg",
"git_commit": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml",
"offline_fixture": null,
"offline_resource_fixtures": [],
Expand Down
4 changes: 2 additions & 2 deletions captures/ghzinga-pr-81834/medium/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"title": "feat(senseaudio): add SenseAudio TTS provider",
"mode": "pr",
"binary": "target/debug/gzg",
"git_commit": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml",
"requested_columns": 120,
"requested_rows": 36,
Expand Down
4 changes: 2 additions & 2 deletions captures/ghzinga-pr-81834/mouse-smoke/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"fixtures/issue-66943.json"
],
"binary": "target/debug/gzg",
"git_commit": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"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",
Expand Down
4 changes: 2 additions & 2 deletions captures/ghzinga-pr-81834/narrow/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"title": "feat(senseaudio): add SenseAudio TTS provider",
"mode": "pr",
"binary": "target/debug/gzg",
"git_commit": "7ddbf47fc6616b1f3bc2e631780d1d565faf1110",
"app_tree_hash": "dddf26dded2c4e38b74fe4c9490b980332198cbe3637c2e4928fcb933dc9e4b2",
"git_commit": "8361604b1b68330910b8e12386e7e6c877259b98",
"app_tree_hash": "dddd804277bbb61e09c06e504ab7c6480c622c0fe0345a6875687ed2c58b491e",
"config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml",
"requested_columns": 80,
"requested_rows": 24,
Expand Down
104 changes: 95 additions & 9 deletions src/render/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ use self::header::{
struct ContentRow {
line: Line<'static>,
target: Option<HitTarget>,
/// Clickable width of the target. None means the whole row is the hit
/// area (list rows); buttons set their rendered label width so clicks
/// beside them do nothing.
target_width: Option<u16>,
comfortable_gap_after: bool,
activity_focus: Option<String>,
}
Expand Down Expand Up @@ -121,6 +125,7 @@ struct ChromeSpacingPlan {
impl ContentRow {
fn plain(text: impl Into<String>) -> Self {
Self {
target_width: None,
line: Line::from(text.into()),
target: None,
comfortable_gap_after: false,
Expand All @@ -130,6 +135,7 @@ impl ContentRow {

fn styled(text: impl Into<String>, style: Style) -> Self {
Self {
target_width: None,
line: Line::from(Span::styled(text.into(), style)),
target: None,
comfortable_gap_after: false,
Expand All @@ -139,6 +145,7 @@ impl ContentRow {

fn target(text: impl Into<String>, target: HitTarget) -> Self {
Self {
target_width: None,
line: Line::from(text.into()),
target: Some(target),
comfortable_gap_after: false,
Expand All @@ -148,6 +155,7 @@ impl ContentRow {

fn target_styled(text: impl Into<String>, target: HitTarget, style: Style) -> Self {
Self {
target_width: None,
line: Line::from(Span::styled(text.into(), style)),
target: Some(target),
comfortable_gap_after: false,
Expand All @@ -157,13 +165,19 @@ impl ContentRow {

fn target_line(line: Line<'static>, target: HitTarget) -> Self {
Self {
target_width: None,
line,
target: Some(target),
comfortable_gap_after: false,
activity_focus: None,
}
}

fn with_target_width(mut self, width: u16) -> Self {
self.target_width = Some(width);
self
}

fn with_comfortable_gap_after(mut self) -> Self {
self.comfortable_gap_after = true;
self
Expand Down Expand Up @@ -1803,11 +1817,14 @@ fn render_content(frame: &mut Frame<'_>, area: Rect, state: &mut AppState, palet
let mut visible = Vec::new();
for (visible_index, (_row_index, row)) in visible_rows.into_iter().enumerate() {
if let Some(target) = row.target {
let width = row
.target_width
.map_or(content_area.width, |width| width.min(content_area.width));
state.hit_areas.push(HitArea::new(
Rect::new(
content_area.x,
content_area.y.saturating_add(visible_index as u16),
content_area.width,
width,
1,
),
target,
Expand Down Expand Up @@ -2110,6 +2127,9 @@ fn wrap_content_row(row: ContentRow, width: usize, spacing: SpacingMode) -> Vec<
None if style != Style::default() => ContentRow::styled(line, style),
None => ContentRow::plain(line),
};
// Button-like rows keep their precise hit width on every
// wrapped fragment; registration clamps it to the viewport.
wrapped.target_width = row.target_width;
wrapped.activity_focus = focus.clone();
wrapped
})
Expand Down Expand Up @@ -2599,10 +2619,10 @@ fn push_body_timeline_rows(
heading_style(palette),
));
if crate::domain::body_edit_target(resource).is_some() {
rows.push(ContentRow::target_styled(
"[edit]",
rows.push(edit_button_row(
&symbols,
palette,
HitTarget::EditResourceBody,
link_style(palette),
));
}
if resource.body.trim().is_empty() {
Expand Down Expand Up @@ -2702,7 +2722,7 @@ fn push_activity_timeline_rows(
let block = BlockId::Activity(entry.id.clone());
let can_expand = text_is_truncated(&entry.body, width, ACTIVITY_COLLAPSED_LINES);
let expanded = can_expand && state.block_expanded(&block);
if let Some(edit_row) = activity_edit_row(entry, palette) {
if let Some(edit_row) = activity_edit_row(entry, &symbols, palette) {
rows.push(edit_row);
}
if expanded {
Expand Down Expand Up @@ -2738,18 +2758,30 @@ fn activity_icon(entry: &ActivityEntry, symbols: &Symbols) -> &'static str {
}

/// A standalone clickable row offered on entries the viewer may edit.
fn activity_edit_row(entry: &ActivityEntry, palette: &Palette) -> Option<ContentRow> {
fn activity_edit_row(
entry: &ActivityEntry,
symbols: &Symbols,
palette: &Palette,
) -> Option<ContentRow> {
entry.edit.as_ref().map(|target| {
ContentRow::target_styled(
"[edit]",
edit_button_row(
symbols,
palette,
HitTarget::EditActivityEntry {
node_id: target.node_id.clone(),
},
link_style(palette),
)
})
}

/// The edit affordance renders like every other button and is clickable
/// only on its own label, not the rest of the row.
fn edit_button_row(symbols: &Symbols, palette: &Palette, target: HitTarget) -> ContentRow {
let label = symbols.edit_button;
ContentRow::target_styled(label, target, button_style(palette))
.with_target_width(UnicodeWidthStr::width(label) as u16)
}

fn activity_heading_style(entry: &ActivityEntry, palette: &Palette) -> Style {
let color = match entry.kind {
crate::domain::ActivityKind::Comment => palette.teal,
Expand Down Expand Up @@ -7746,4 +7778,58 @@ mod tests {
assert!(content.contains("Edit comment on openclaw/openclaw#81834"));
assert!(!content.contains("Edit description of"));
}

#[test]
fn edit_button_is_styled_like_a_button_and_sized_to_its_label() {
let palette = ThemeName::Default.palette();
let mut state = editable_pr_state();
draw_actionable(&mut state);
let rect = rendered_target_rect(&state, |target| *target == HitTarget::EditResourceBody)
.expect("edit button hit area");
let label = state.symbols.symbols().edit_button;
assert_eq!(
usize::from(rect.width),
UnicodeWidthStr::width(label),
"the hit area covers exactly the label"
);
assert_eq!(
draw_cell_bg_for_text(&mut state, 120, 36, label, 0),
Some(palette.accent),
"edit renders with the shared button style"
);
}

#[test]
fn clicking_beside_the_edit_button_does_nothing() {
let mut state = editable_pr_state();
draw_actionable(&mut state);
let rect = rendered_target_rect(&state, |target| *target == HitTarget::EditResourceBody)
.expect("edit button hit area");
apply_event(
&mut state,
AppEvent::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: rect.x + rect.width + 4,
row: rect.y,
modifiers: KeyModifiers::empty(),
}),
);
assert!(
state.comment_composer.is_none(),
"the row beside the button must not open the composer"
);
apply_event(
&mut state,
AppEvent::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: rect.x + 1,
row: rect.y,
modifiers: KeyModifiers::empty(),
}),
);
assert!(
state.comment_composer.is_some(),
"the button itself still opens the composer"
);
}
}
7 changes: 6 additions & 1 deletion src/render/symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ pub struct Symbols {
pub more_patch: &'static str,
pub less_patch: &'static str,
pub footer_actions: &'static str,
pub edit_button: &'static str,
pub footer_refresh: &'static str,
pub footer_copy: &'static str,
pub footer_open: &'static str,
Expand Down Expand Up @@ -132,6 +133,7 @@ impl Symbols {
more_patch: "[+ more patch]",
less_patch: "[- less patch]",
footer_actions: "[actions]",
edit_button: "[edit]",
footer_refresh: "[refresh]",
footer_copy: "[copy]",
footer_open: "[open]",
Expand Down Expand Up @@ -187,6 +189,7 @@ impl Symbols {
more_patch: "[➕ more patch]",
less_patch: "[➖ less patch]",
footer_actions: "[⚡ actions]",
edit_button: "[✏ edit]",
footer_refresh: "[🔄 refresh]",
footer_copy: "[📋 copy]",
footer_open: "[🌐 open]",
Expand All @@ -207,7 +210,7 @@ impl Symbols {
mod tests {
use super::*;

fn symbol_values(symbols: Symbols) -> [&'static str; 49] {
fn symbol_values(symbols: Symbols) -> [&'static str; 50] {
[
symbols.state_open,
symbols.state_merged,
Expand Down Expand Up @@ -246,6 +249,7 @@ mod tests {
symbols.collapse_all,
symbols.more_patch,
symbols.less_patch,
symbols.edit_button,
symbols.footer_refresh,
symbols.footer_copy,
symbols.footer_open,
Expand Down Expand Up @@ -291,6 +295,7 @@ mod tests {
symbols.collapse_all,
symbols.more_patch,
symbols.less_patch,
symbols.edit_button,
symbols.footer_refresh,
symbols.footer_copy,
symbols.footer_open,
Expand Down