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: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ on:
- slophammer.yml
- fixtures/**
- captures/**
- plugins/**
- scripts/**
- src/**
- tests/**
Expand All @@ -23,6 +24,7 @@ on:
- slophammer.yml
- fixtures/**
- captures/**
- plugins/**
- scripts/**
- src/**
- tests/**
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ghzinga"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
rust-version = "1.88"
description = "Terminal UI for monitoring GitHub pull requests and issues."
Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,29 @@ that session, including while another resource is still loading. If the target
session is not running, `gzg open` updates the saved session so the resources
appear on the next restore.

## Herdr Plugin

`ghzinga` includes a Herdr plugin for opening GitHub issue and pull request links
from Herdr panes.

Install it with:

```sh
cargo install ghzinga --locked --force
herdr plugin install dutifuldev/ghzinga/plugins/herdr --yes
```

Then Ctrl-click a GitHub issue or pull request URL inside Herdr. The plugin opens
or reuses a right-side ghzinga pane next to the clicked pane. Links followed from
inside that ghzinga pane update the same pane.

The Herdr plugin requires `ghzinga` 0.4.0 or newer on `PATH`.

The plugin is intentionally thin: Herdr calls `gzg herdr-plugin open/viewer`,
and ghzinga owns the URL parsing, pane reuse, and session control in Rust. See
[`plugins/herdr/README.md`](plugins/herdr/README.md) for the install and
architecture details.

## Refresh

`ghzinga` refreshes automatically every 300 seconds by default. Use
Expand Down
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,7 +4,7 @@
"title": "openai-responses provider: 404 on previous_response_id when store=false (default)",
"mode": "issue",
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml",
"requested_columns": 160,
"requested_rows": 50,
Expand Down Expand Up @@ -113,5 +113,5 @@
}
],
"actual_tmux_size": "160x50",
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
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,7 +3,7 @@
"title": "openai-responses provider: 404 on previous_response_id when store=false (default)",
"mode": "issue",
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml",
"offline_fixture": "fixtures/issue-88499.json",
"offline_resource_fixtures": [],
Expand All @@ -24,5 +24,5 @@
"rows": 50
}
],
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
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,7 +4,7 @@
"title": "openai-responses provider: 404 on previous_response_id when store=false (default)",
"mode": "issue",
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml",
"requested_columns": 120,
"requested_rows": 36,
Expand Down Expand Up @@ -113,5 +113,5 @@
}
],
"actual_tmux_size": "120x36",
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
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,7 +5,7 @@
"fixtures/issue-66943.json"
],
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"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 Expand Up @@ -120,5 +120,5 @@
"ansi": "70_mouse_quit_confirm.ansi"
}
],
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
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,7 +4,7 @@
"title": "openai-responses provider: 404 on previous_response_id when store=false (default)",
"mode": "issue",
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"config_path": "captures/ghzinga-issue-88499/capture-empty-config.toml",
"requested_columns": 80,
"requested_rows": 24,
Expand Down Expand Up @@ -113,5 +113,5 @@
}
],
"actual_tmux_size": "80x24",
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
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,7 +4,7 @@
"title": "feat(senseaudio): add SenseAudio TTS provider",
"mode": "pr",
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml",
"requested_columns": 160,
"requested_rows": 50,
Expand Down Expand Up @@ -183,5 +183,5 @@
}
],
"actual_tmux_size": "160x50",
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
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,7 +3,7 @@
"title": "feat(senseaudio): add SenseAudio TTS provider",
"mode": "pr",
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml",
"offline_fixture": null,
"offline_resource_fixtures": [],
Expand All @@ -24,5 +24,5 @@
"rows": 50
}
],
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
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,7 +4,7 @@
"title": "feat(senseaudio): add SenseAudio TTS provider",
"mode": "pr",
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml",
"requested_columns": 120,
"requested_rows": 36,
Expand Down Expand Up @@ -183,5 +183,5 @@
}
],
"actual_tmux_size": "120x36",
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
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,7 +5,7 @@
"fixtures/issue-66943.json"
],
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"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 Expand Up @@ -240,5 +240,5 @@
],
"load_full_fixture": "captures/ghzinga-pr-81834/mouse-smoke/load-full-fixture.json",
"load_full_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 ./target/debug/gzg 'openclaw/openclaw#81834' --offline-fixture ./captures/ghzinga-pr-81834/mouse-smoke/load-full-fixture.json --no-restore --refresh-seconds 0",
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
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,7 +4,7 @@
"title": "feat(senseaudio): add SenseAudio TTS provider",
"mode": "pr",
"binary": "target/debug/gzg",
"git_commit": "b673d451bc0e78e5f0f6e5e5ace46288288c47e3",
"git_commit": "50adb66554e3eb8b6a53e751631532d968f2588b",
"config_path": "captures/ghzinga-pr-81834/capture-empty-config.toml",
"requested_columns": 80,
"requested_rows": 24,
Expand Down Expand Up @@ -183,5 +183,5 @@
}
],
"actual_tmux_size": "80x24",
"app_tree_hash": "4d92003b824c94729ccd42745e5bac8e9f73559f666d40d96b7c4149575de0b6"
"app_tree_hash": "085282e9659a0f7b7da99bfc219f12ca4ccc7ab59b379dea63ce601cda4239e3"
}
124 changes: 124 additions & 0 deletions plugins/herdr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Ghzinga for Herdr

This Herdr plugin opens GitHub issue and pull request links in a ghzinga side
pane.

## Install

Install `ghzinga` 0.4.0 or newer first:

```sh
cargo install ghzinga --locked --force
```

Then install the Herdr plugin:

```sh
herdr plugin install dutifuldev/ghzinga/plugins/herdr --yes
```

For local development:

```sh
herdr plugin link /path/to/ghzinga/plugins/herdr
```

## Usage

Inside Herdr, Ctrl-click a GitHub issue or pull request URL:

```text
https://github.com/dutifuldev/ghzinga/pull/29
https://github.com/dutifuldev/ghzinga/issues/32
```

The plugin opens a right-side ghzinga pane next to the pane that contained the
link. Later Ctrl-clicks from the same source pane in the same Herdr session
reuse that side pane by running `gzg open --session ...`.

Ctrl-clicks from inside that ghzinga side pane also stay in the same ghzinga
pane, so following related issue/PR links does not create nested ghzinga panes.
When a new ghzinga side pane is created, it starts from the clicked resource
rather than restoring tabs from a previous closed plugin pane.

Herdr currently routes plugin link handlers through Ctrl-click. Plain left-click
link handling would need Herdr itself to expose that behavior to plugins.

## How it works

The plugin manifest is only wiring. It registers a GitHub issue/PR link handler
and points Herdr at two ghzinga-owned entrypoints:

```text
herdr-plugin.toml -> gzg herdr-plugin open
herdr-plugin.toml -> gzg herdr-plugin viewer
```

`gzg herdr-plugin open` runs as the Herdr link action. It reads Herdr's clicked
URL and source pane environment, normalizes the GitHub issue or PR URL with
ghzinga's Rust parser, and opens a right-side Herdr plugin pane beside the source
pane.

`gzg herdr-plugin viewer` runs inside that side pane. It starts the normal
ghzinga TUI for the selected issue or PR with `--new --session`, so the pane has
a fresh initial resource while still exposing a stable session for later
`gzg open --session ...` updates.

The Rust entrypoint also keeps pane reuse state scoped by Herdr session/socket
and source pane. On later Ctrl-clicks from the same source pane, it focuses the
existing ghzinga pane if it is still alive and still belongs to this plugin, then
sends `gzg open --session ... <url>` into that ghzinga session. If the stored
pane is gone or belongs to something else, it opens a fresh side pane.

When the source pane is already a ghzinga plugin viewer, the entrypoint uses the
viewer-pane-to-session state written at pane creation time and opens the link in
that same ghzinga session. If that reverse state is missing, for example for a
pane opened by an older plugin build, it falls back to ghzinga's Herdr pane
context resolution and still updates the current viewer.

## Requirements

- Herdr 0.7.0 or newer.
- `ghzinga` 0.4.0 or newer, with `gzg` installed on `PATH`. The Herdr entrypoints call
`gzg herdr-plugin open` and `gzg herdr-plugin viewer`.
- GitHub credentials through `gh auth token`, `GH_TOKEN`, or `GITHUB_TOKEN` for
private repositories.

Set `GHZINGA_BIN` before launching Herdr if you need to use a non-default
ghzinga binary path for the viewer process. Normal installs do not need this.

## Troubleshooting

Check that Herdr can find a new enough ghzinga binary:

```sh
gzg --version
```

Check that Herdr has the plugin installed and enabled:

```sh
herdr plugin list --json
```

Inspect plugin command failures:

```sh
herdr plugin log list --plugin dutifuldev.ghzinga
```

If Herdr still runs an older `gzg`, reinstall ghzinga and restart Herdr so its
plugin environment sees the updated `PATH`.

## Verification

The production path has three layers of coverage:

- Rust unit tests for URL normalization, pane identity validation, session
scoping, self-pane reuse, and the `gzg herdr-plugin open` entrypoint.
- Fake Herdr shell tests for open, reuse, self-pane reuse, stale-pane, and viewer
launch behavior.
- `scripts/herdr-plugin-live-smoke.sh`, which starts a disposable Herdr session,
links this plugin, opens a real right-side pane, verifies ghzinga rendered
fixture content there, and verifies links from the ghzinga pane reuse that
pane.
24 changes: 24 additions & 0 deletions plugins/herdr/herdr-plugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
id = "dutifuldev.ghzinga"
name = "Ghzinga for Herdr"
version = "0.1.0"
min_herdr_version = "0.7.0"
description = "Open GitHub issue and pull request links in a ghzinga side pane from Herdr."
platforms = ["linux", "macos"]

[[actions]]
id = "open"
title = "Open in ghzinga"
contexts = ["pane"]
command = ["gzg", "herdr-plugin", "open"]

[[panes]]
id = "viewer"
title = "ghzinga"
placement = "split"
command = ["gzg", "herdr-plugin", "viewer"]

[[link_handlers]]
id = "github-issue-pr"
title = "Open GitHub issue or PR in ghzinga"
pattern = "^https://github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/(issues|pull)/[0-9]+([/?#].*)?$"
action = "open"
6 changes: 6 additions & 0 deletions plugins/herdr/test/fake-gzg.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -eu

log=${GZG_FAKE_LOG:?GZG_FAKE_LOG is required}
printf '%s\n' "$*" >>"$log"
printf 'fake gzg: %s\n' "$*"
Loading