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
1 change: 1 addition & 0 deletions .github/workflows/windows-full-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ on:
- "scripts/test-http-server.py"
- "scripts/test-windows-full-manifest.py"
- "windows-full/**"
- "links/**"
- ".github/workflows/windows-full-ci.yml"
merge_group:

Expand Down
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ Full install. Works with Claude Code, Codex, pi, Copilot CLI, Droid, Oh My Pi, H
| `Ctrl+B O` | this folder, with a file tree |
| `Ctrl+B Shift+O` | the agent's recent replies |
| `Ctrl+B Ctrl+O` | the agent's newest reply, with no picker |
| Ctrl-click a `file://…md` link | that file |
| Ctrl-click a `file://…md` link | that file, only with the opt-in Links add-on below |

`annotate.terminal` opens the focused pane's recent terminal output (the last 200 lines) for
review in the same way. It has no default key; to use it, bind one yourself, for example:
Expand All @@ -225,6 +225,31 @@ command = "annotate.terminal"
description = "review this pane's recent output"
```

#### Ctrl-click Markdown links (opt-in)

Annotate does not capture Ctrl-click on links by default, so `file://…md` links open the way
Herdr normally opens them. To send Ctrl-clicked `file://` Markdown links (`.md`, `.markdown`,
`.mdx`) to Annotate instead, install the Links add-on next to the Full install (on Windows, next
to Windows Full):

```sh
herdr plugin install plannotator/herdr-annotate/links
```

To turn it off again:

```sh
herdr plugin uninstall annotate-links
```

The add-on has no binary of its own; it opens the file with the Full (or Windows Full) plugin's
plannotator-tui. If neither is installed, Ctrl-click shows a notification saying which one to install.

> **Upgrading from 0.8.0 or earlier:** up to plugin 0.8.0, the Full install captured these clicks
> itself. From 0.9.0 it does not
> ([#69](https://github.com/plannotator/herdr-annotate/issues/69)). Install the Links add-on to keep
> the old behavior.

**Send** (or `E`) makes the review the agent's next message. `q` closes. On an agent reply, `S`
sends and closes in one key; `E` and `q` are unchanged.

Expand Down Expand Up @@ -288,6 +313,7 @@ herdr plugin link "$PWD" # or "$PWD/lite" for the Lite variant

`herdr plugin link` does not run manifest build hooks, so stage the binary first. It also replaces
any existing `annotate` link; link the other directory to switch back.
The Links add-on has no build step: `herdr plugin link "$PWD/links"` links it as `annotate-links`.

`bash scripts/lite-regression.sh` checks the runtime against goldens recorded from the retired Bun
runtime. [docs/lite-testing.md](docs/lite-testing.md) covers what it compares and everything else
Expand Down
15 changes: 8 additions & 7 deletions docs/windows-full-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ requirements, not completed verification. Every box starts unchecked.
| `send-archive` | Send annotations to the agent and archive them | `pane` |
| `manage` | Manage annotations | `global` |
| `open` | Annotate: open here | `workspace`, `pane` |
| `open-link` | Annotate this file | `pane` |
| `last` | Annotate: agent's last message | `pane` |
| `last-newest` | Annotate: agent's newest message | `pane` |
| `terminal` | Annotate: terminal output | `pane` |
Expand All @@ -43,13 +42,16 @@ requirements, not completed verification. Every box starts unchecked.
command must be exactly `["./bin/plannotator-tui.exe", "herdr", "pane"]`.
No shell, PowerShell, command-string interpolation, or fallback launcher may sit
between Herdr's pane process and the TUI.
- [ ] Keep `open` and `open-link` as direct argv ending in `herdr open`,
- [ ] Keep `open` as direct argv ending in `herdr open`,
`last` as direct argv ending in `herdr last`, `last-newest` as direct argv
ending in `herdr last --newest`, and `terminal` as direct argv ending in
`herdr terminal`, all using
`./bin/plannotator-tui.exe`. Preserve the `markdown-file` link handler's title,
pattern, and `open-link` action. All actions, panes, the handler, and the build
must be effective on Windows; no inherited Unix gate may disable them.
`./bin/plannotator-tui.exe`. All actions, panes, and the build must be
effective on Windows; no inherited Unix gate may disable them. Since plugin
0.9.0 ([#69](https://github.com/plannotator/herdr-annotate/issues/69)) the
variant declares no `[[link_handlers]]` and no `open-link` action: Ctrl-click
capture is opt-in through the `links/` add-on, whose `open-link-windows` action
runs `links/open-link.ps1` against this variant's staged plannotator-tui.
- [ ] Reuse the shared native runtime with paths valid from `windows-full/`
(as `lite/` does with `../bin/`). Verify those paths from an installed checkout,
not just the repository root. Preserve shared annotation state and archive
Expand Down Expand Up @@ -136,8 +138,7 @@ does not establish that a real Herdr pane works.
- [ ] Pass a relative `PLANNOTATOR_TUI_FILE` that exists only in the review cwd,
then an absolute path with spaces and Unicode. Assert each intended document
renders. Verify delivery target variables supplied by the launcher survive
unchanged. Exercise `open`, `open-link` (including a percent-encoded `file://`
path), `last`, and `last-newest` with controlled context/transcript fixtures. Record fixture
unchanged. Exercise `open`, `last`, and `last-newest` with controlled context/transcript fixtures. Record fixture
evidence separately from real-agent integration.
- [ ] Open Full with inert review-folder `.env` fixtures that would change a
launcher process. Full must render the intended document without loading those
Expand Down
25 changes: 5 additions & 20 deletions herdr-plugin.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id = "annotate"
name = "Annotate"
version = "0.8.0"
version = "0.9.0"
min_herdr_version = "0.8.0"
description = "Comment on terminal selections and copy annotations as agent context."
platforms = ["linux", "macos", "windows"]
Expand Down Expand Up @@ -96,9 +96,10 @@ placement = "overlay"
platforms = ["macos", "linux"]
command = ["sh", "-c", "exec bash \"$HERDR_PLUGIN_ROOT/scripts/plannotator-tui.sh\" herdr pane"]

# Both actions run the launcher, which reads Herdr's invocation context: the focused pane's
# folder (open) or the clicked file:// link (open-link), and the focused pane's agent as
# the target the review is sent back to.
# The review actions run the launcher, which reads Herdr's invocation context (the focused
# pane's folder and agent) and sends the review back to that agent. Ctrl-click on a
# file:// Markdown link is not captured here; the opt-in links/ add-on declares that handler and
# runs this plugin's launcher, so a plain install leaves the click to Herdr's default opener.
[[actions]]
id = "open"
title = "Annotate: open here"
Expand All @@ -107,14 +108,6 @@ contexts = ["workspace", "pane"]
platforms = ["macos", "linux"]
command = ["./bin/plannotator-tui.exe", "herdr", "open"]

[[actions]]
id = "open-link"
title = "Annotate this file"
description = "Open a Ctrl-clicked Markdown file in plannotator-tui."
contexts = ["pane"]
platforms = ["macos", "linux"]
command = ["./bin/plannotator-tui.exe", "herdr", "open"]

[[actions]]
id = "last"
title = "Annotate: agent's last message"
Expand All @@ -139,11 +132,3 @@ description = "Review the focused pane's recent terminal output in plannotator-t
contexts = ["pane"]
platforms = ["macos", "linux"]
command = ["./bin/plannotator-tui.exe", "herdr", "terminal"]

# Ctrl-click on a file:// Markdown link. Anchored on the scheme so web links never match.
[[link_handlers]]
id = "markdown-file"
title = "Annotate this file"
pattern = "^file://.*\\.(md|markdown|mdx)$"
action = "open-link"
platforms = ["macos", "linux"]
54 changes: 54 additions & 0 deletions links/herdr-plugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Herdr Annotate, Links: opt-in Ctrl-click capture for file:// Markdown links.
# Install with: herdr plugin install plannotator/herdr-annotate/links
# Remove with: herdr plugin uninstall annotate-links
#
# The Annotate plugin no longer claims these clicks itself, because Herdr matches link handlers
# from the manifest before any plugin code runs and offers no way to turn one off. Keeping the
# handler in its own plugin makes capture a choice: install this to send Markdown links to
# Annotate, uninstall it to give them back to Herdr's default opener.
#
# This add-on ships no binary. Its action finds the installed `annotate` plugin and runs that
# plugin's plannotator-tui exactly as the old built-in open-link action did, so it needs a Full
# install: plannotator/herdr-annotate on macOS or Linux, plannotator/herdr-annotate/windows-full
# on Windows. Herdr rejects duplicate ids, so each platform family has its own action and
# handler; Herdr skips a handler whose platforms exclude the current one.

id = "annotate-links"
name = "Annotate Links"
version = "0.9.0"
min_herdr_version = "0.8.0"
description = "Open Ctrl-clicked file:// Markdown links in Annotate (needs the Annotate Full plugin)."
platforms = ["linux", "macos", "windows"]

[[actions]]
id = "open-link"
title = "Annotate this file"
description = "Open a Ctrl-clicked Markdown file in plannotator-tui."
contexts = ["pane"]
platforms = ["macos", "linux"]
command = ["bash", "open-link.sh"]

# The same invocation idiom as the Windows build hooks: powershell.exe comes from PATH, never
# from the plugin root, and the script path is relative to the root Herdr runs actions from.
[[actions]]
id = "open-link-windows"
title = "Annotate this file"
description = "Open a Ctrl-clicked Markdown file in plannotator-tui."
contexts = ["pane"]
platforms = ["windows"]
command = ["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", "open-link.ps1"]

# Anchored on the scheme so web links never match.
[[link_handlers]]
id = "markdown-file"
title = "Annotate this file"
pattern = "^file://.*\\.(md|markdown|mdx)$"
action = "open-link"
platforms = ["macos", "linux"]

[[link_handlers]]
id = "markdown-file-windows"
title = "Annotate this file"
pattern = "^file://.*\\.(md|markdown|mdx)$"
action = "open-link-windows"
platforms = ["windows"]
56 changes: 56 additions & 0 deletions links/open-link.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Open a Ctrl-clicked file:// Markdown link in the installed Annotate plugin, on Windows.
#
# The Windows counterpart of open-link.sh. Herdr runs this with the clicked URL and the focused
# pane in HERDR_PLUGIN_CONTEXT_JSON. It finds the `annotate` plugin's root, then runs that
# plugin's plannotator-tui with the same arguments, working directory and plugin id the old
# built-in open-link action of Windows Full used. The review pane therefore opens inside
# `annotate`, and feedback goes to the focused agent.

# Continue, not Stop: Windows PowerShell turns any stderr from a native command into a
# terminating error under Stop. Every outcome below is checked explicitly instead.
$ErrorActionPreference = "Continue"

$herdr = if ([string]::IsNullOrEmpty($env:HERDR_BIN_PATH)) { "herdr" } else { $env:HERDR_BIN_PATH }
$installHint = "herdr plugin install plannotator/herdr-annotate/windows-full"

function Fail {
param([string]$Title, [string]$Body)
[Console]::Error.WriteLine($Body)
try { & $herdr notification show $Title --body $Body *> $null } catch { }
exit 1
}

# Herdr writes UTF-8; Windows PowerShell would otherwise decode it with the OEM code page.
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

$plugin = $null
try {
$listing = (& $herdr plugin list --plugin annotate --json 2> $null) -join "`n"
$plugin = @(($listing | ConvertFrom-Json).result.plugins) |
Where-Object { $null -ne $_ -and $_.plugin_id -ceq "annotate" } |
Select-Object -First 1
} catch {
$plugin = $null
}

if ($null -eq $plugin -or [string]::IsNullOrEmpty($plugin.plugin_root)) {
Fail "Annotate: plugin not installed" "Ctrl-click needs the Annotate plugin. Install it with: $installHint"
}
if (-not $plugin.enabled) {
Fail "Annotate: plugin disabled" "Ctrl-click needs the Annotate plugin enabled. Enable it with: herdr plugin enable annotate"
}

# .NET rather than Join-Path/Test-Path: the root may be an extended-length \\?\ path.
$root = [string]$plugin.plugin_root
$tui = [System.IO.Path]::Combine($root, "bin", "plannotator-tui.exe")
if (-not [System.IO.File]::Exists($tui)) {
Fail "Annotate: document review unavailable" "Ctrl-click needs Annotate Windows Full, which includes plannotator-tui. Install it with: $installHint"
}

$env:HERDR_PLUGIN_ID = "annotate"
$env:HERDR_PLUGIN_ROOT = $root
# Herdr ran the old action from the plugin root; do the same, best effort.
try { [System.IO.Directory]::SetCurrentDirectory($root) } catch { }
try { Set-Location -LiteralPath $root } catch { }
& $tui herdr open
exit $LASTEXITCODE
56 changes: 56 additions & 0 deletions links/open-link.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# Open a Ctrl-clicked file:// Markdown link in the installed Annotate plugin.
#
# Herdr runs this with the clicked URL and the focused pane in HERDR_PLUGIN_CONTEXT_JSON. It
# finds the `annotate` plugin's root, then runs that plugin's plannotator-tui with the same
# arguments, working directory and plugin id the old built-in open-link action used. The
# review pane therefore opens inside `annotate`, and feedback goes to the focused agent.
set -euo pipefail

herdr_bin="${HERDR_BIN_PATH:-herdr}"
install_hint="herdr plugin install plannotator/herdr-annotate"

fail() {
echo "$2" >&2
"$herdr_bin" notification show "$1" --body "$2" >/dev/null 2>&1 || true
exit 1
}

# Print the decoded value of a top-level JSON string field. `plugin list --plugin` returns one
# plugin, and these keys occur only at the plugin level, so the first match is the right one.
json_string() {
printf '%s' "$2" | sed -nE "s/.*\"$1\":\"(([^\"\\\\]|\\\\.)*)\".*/\\1/p" | awk '
{
out = ""
while ((i = index($0, "\\")) > 0) {
c = substr($0, i + 1, 1)
if (c == "n") c = "\n"; else if (c == "t") c = "\t"; else if (c == "u") exit 1
out = out substr($0, 1, i - 1) c
$0 = substr($0, i + 2)
}
print out $0
}'
}

listing="$("$herdr_bin" plugin list --plugin annotate --json 2>/dev/null | tr -d '\n' || true)"
root="$(json_string plugin_root "$listing" || true)"

if [ -z "$root" ]; then
fail "Annotate: plugin not installed" \
"Ctrl-click needs the Annotate plugin. Install it with: $install_hint"
fi
case "$listing" in
*'"enabled":true'*) ;;
*) fail "Annotate: plugin disabled" \
"Ctrl-click needs the Annotate plugin enabled. Enable it with: herdr plugin enable annotate" ;;
esac

tui="$root/bin/plannotator-tui.exe"
if [ ! -x "$tui" ]; then
fail "Annotate: document review unavailable" \
"Ctrl-click needs the Full Annotate plugin, which includes plannotator-tui. Install it with: $install_hint"
fi

cd "$root"
export HERDR_PLUGIN_ID=annotate HERDR_PLUGIN_ROOT="$root"
exec "$tui" herdr open
2 changes: 1 addition & 1 deletion lite/herdr-plugin.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

id = "annotate"
name = "Annotate"
version = "0.8.0"
version = "0.9.0"
min_herdr_version = "0.8.0"
description = "Comment on terminal selections and copy annotations as agent context."
platforms = ["linux", "macos", "windows"]
Expand Down
4 changes: 2 additions & 2 deletions rust/src/clipboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ mod tests {

#[test]
fn every_platform_has_read_and_write_candidates() {
assert!(!read_commands().is_empty());
assert!(!write_commands().is_empty());
assert_ne!(read_commands().len(), 0);
assert_ne!(write_commands().len(), 0);
}
}
2 changes: 1 addition & 1 deletion rust/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -892,7 +892,7 @@ mod tests {
app.handle_key(KeyEvent::from(KeyCode::Char('D')));
assert_eq!(load_annotations(&dir).expect("still active").len(), 1);
app.handle_key(KeyEvent::from(KeyCode::Char('D')));
assert!(load_annotations(&dir).expect("cleared").is_empty());
assert_eq!(load_annotations(&dir).expect("cleared").len(), 0);
let _ = fs::remove_dir_all(dir);
}
}
2 changes: 1 addition & 1 deletion rust/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ mod tests {
fs::create_dir_all(&parent).expect("temporary parent");
fs::create_dir(&reference).expect("reference directory");

assert!(load_annotations(&dir).expect("load").is_empty());
assert_eq!(load_annotations(&dir).expect("load").len(), 0);
assert_eq!(
fs::metadata(&dir)
.expect("state metadata")
Expand Down
25 changes: 21 additions & 4 deletions scripts/smoke.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Smoke-test the plugin the way users get it: fresh install, upgrade from an old commit, lite,
# and the lite -> full swap, exercising both fetched binaries and both panes in a disposable
# Herdr session. Restores whatever `annotate` install was present before it ran.
# the lite -> full swap, and the opt-in links add-on, exercising both fetched binaries and both
# panes in a disposable Herdr session. Restores whatever `annotate` install was present before it ran.
#
# HERDR_SESSION=<disposable named session> bash scripts/smoke.sh [old-ref]
#
Expand Down Expand Up @@ -97,7 +97,7 @@ trap restore EXIT

echo "== fresh install: full"
install "$spec"
check "actions" "$(actions)" "capture,copy-archive,copy-context,last,last-newest,manage,open,open-link,paste-archive,send-archive,terminal"
check "actions" "$(actions)" "capture,copy-archive,copy-context,last,last-newest,manage,open,paste-archive,send-archive,terminal"
check "action programs" "$(programs)" "./bin/herdr-annotate.exe,./bin/plannotator-tui.exe"
check "review binary matches pin" "$(tui_version)" "$(tui_pin)"
check "native binary matches pin" "$(native_version)" "$(native_pin)"
Expand Down Expand Up @@ -134,9 +134,26 @@ check "manager pane" "$(manager_renders)" "ok"

echo "== swap: lite -> full"
install "$spec"
check "actions" "$(actions)" "capture,copy-archive,copy-context,last,last-newest,manage,open,open-link,paste-archive,send-archive,terminal"
check "actions" "$(actions)" "capture,copy-archive,copy-context,last,last-newest,manage,open,paste-archive,send-archive,terminal"
check "review binary" "$(tui_version)" "$(tui_pin)"
check "native binary" "$(native_version)" "$(native_pin)"

echo "== opt-in links add-on (#69)"
link_handlers() { herdr plugin list --plugin "$1" --json | python3 -c "
import json,sys
plugins=json.load(sys.stdin)['result']['plugins']
print(','.join(sorted(h['id'] for p in plugins for h in p.get('link_handlers',[]))) or 'none')"; }
check "full declares no link handler" "$(link_handlers annotate)" "none"
if herdr plugin list --plugin annotate-links --json | grep -q '"plugin_id":"annotate-links"'; then
echo " note annotate-links is already installed here; skipping so it is left untouched"
else
herdr plugin install "$spec/links" --yes >/dev/null
check "add-on link handlers" "$(link_handlers annotate-links)" "markdown-file,markdown-file-windows"
check "add-on Unix action" "$(herdr plugin action list --plugin annotate-links | python3 -c "
import json,sys; print('open-link' if any(a['action_id']=='open-link' for a in json.load(sys.stdin)['result']['actions']) else 'missing')")" "open-link"
herdr plugin uninstall annotate-links >/dev/null
check "add-on removed" "$(link_handlers annotate-links)" "none"
fi

echo "== result: $failures failure(s)"
[ "$failures" -eq 0 ]
Loading
Loading