Skip to content
Open
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 docs/next/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- Experimental pane graphics now support bounded named layers, acknowledged full-RGBA primary-layer direct file frames on audited local terminals, owned BGRA fallback, exact pixel mouse input, and placement-only resize replay.

### Fixed
- Grok CLI panes now update their native session identity after `/new`, so API results and later restores use the current conversation. (#2681)
- Prefix keybindings now disambiguate layout-aware shifted punctuation, so a shifted `\` no longer triggers `prefix+|` on keyboard layouts where the same key produces both characters. (#2674)
- Remote clients now continue redrawing at very large terminal sizes instead of freezing when a full ANSI frame exceeds the transport limit. (#2670)
- OpenCode panes now track the root conversation selected in their own TUI for native restore without adopting activity from attached clients. (#2450)
Expand Down
2 changes: 1 addition & 1 deletion docs/next/website/src/content/docs/integrations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Use `HERDR_BIN_PATH` and the CLI wrappers for portable integrations. Code that n

Some integrations report native agent session references. Herdr uses official session references to resume Claude Code, Codex, Devin CLI, Droid, Kimi Code CLI, Qoder CLI, Cursor Agent CLI, Grok CLI, GitHub Copilot CLI, Pi, OMP, Hermes Agent, OpenCode, Kilo Code CLI, MastraCode, and Antigravity CLI panes after a Herdr server restart unless `[session] resume_agents_on_restore = false` disables it.

Native session restore requires current Herdr integrations: Pi integration version `2`, OMP version `3`, Claude Code version `6`, Codex version `5`, GitHub Copilot CLI version `2`, Devin CLI version `2`, Droid version `2`, Kimi Code CLI version `3`, Qoder CLI version `2`, Cursor Agent CLI version `1`, Grok CLI version `1`, OpenCode version `5`, Kilo Code CLI version `1`, Hermes Agent version `5`, MastraCode version `1`, or Antigravity CLI version `1`. Check installed versions with `herdr integration status`.
Native session restore requires current Herdr integrations: Pi integration version `2`, OMP version `3`, Claude Code version `6`, Codex version `5`, GitHub Copilot CLI version `2`, Devin CLI version `2`, Droid version `2`, Kimi Code CLI version `3`, Qoder CLI version `2`, Cursor Agent CLI version `1`, Grok CLI version `2`, OpenCode version `5`, Kilo Code CLI version `1`, Hermes Agent version `5`, MastraCode version `1`, or Antigravity CLI version `1`. Check installed versions with `herdr integration status`.

## Pi

Expand Down
2 changes: 1 addition & 1 deletion docs/next/website/src/content/docs/ja/integrations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Herdr の外では何もしないように、`HERDR_ENV=1` で必要な変数が

一部のインテグレーションは、エージェントのネイティブセッション参照を報告します。Herdr は公式のセッション参照を使って、`[session] resume_agents_on_restore = false` で無効化されていない限り、Herdr サーバーの再起動後に Claude Code、Codex、Devin CLI、Droid、Kimi Code CLI、Qoder CLI、Cursor Agent CLI、Grok CLI、GitHub Copilot CLI、Pi、OMP、Hermes Agent、OpenCode、Kilo Code CLI、MastraCode、Antigravity CLI のペインを resume します。

エージェントネイティブのセッション復元には最新の Herdr インテグレーションが必要です: Pi インテグレーションはバージョン `2`、OMP は `3`、Claude Code は `6`、Codex は `5`、GitHub Copilot CLI は `2`、Devin CLI は `2`、Droid は `2`、Kimi Code CLI は `3`、Qoder CLI は `2`、Cursor Agent CLI は `1`、Grok CLI は `1`、OpenCode は `5`、Kilo Code CLI は `1`、Hermes Agent は `5`、MastraCode は `1`、Antigravity CLI は `1` です。インストール済みバージョンは `herdr integration status` で確認してください。
エージェントネイティブのセッション復元には最新の Herdr インテグレーションが必要です: Pi インテグレーションはバージョン `2`、OMP は `3`、Claude Code は `6`、Codex は `5`、GitHub Copilot CLI は `2`、Devin CLI は `2`、Droid は `2`、Kimi Code CLI は `3`、Qoder CLI は `2`、Cursor Agent CLI は `1`、Grok CLI は `2`、OpenCode は `5`、Kilo Code CLI は `1`、Hermes Agent は `5`、MastraCode は `1`、Antigravity CLI は `1` です。インストール済みバージョンは `herdr integration status` で確認してください。

## Pi

Expand Down
2 changes: 1 addition & 1 deletion docs/next/website/src/content/docs/session-state.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Native session restore requires these Herdr integration versions or newer:
| Claude Code | `6` | `claude --resume <id>` |
| Codex | `5` | `codex resume <id>` |
| Cursor Agent CLI | `1` | `cursor-agent --resume <id>` |
| Grok CLI | `1` | `grok --resume <id>` |
| Grok CLI | `2` | `grok --resume <id>` |
| GitHub Copilot CLI | `2` | `copilot --resume=<id>` |
| Devin CLI | `2` | `devin --resume <id>` |
| Droid | `2` | `droid --resume <id>` |
Expand Down
2 changes: 1 addition & 1 deletion docs/next/website/src/content/docs/zh-cn/integrations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Herdr 以两种不同方式使用集成:

一些集成会上报智能体的原生会话引用。除非被 `[session] resume_agents_on_restore = false` 禁用,Herdr 会在服务器重启后使用官方会话引用恢复 Claude Code、Codex、Devin CLI、Droid、Kimi Code CLI、Qoder CLI、Cursor Agent CLI、Grok CLI、GitHub Copilot CLI、Pi、OMP、Hermes Agent、OpenCode、Kilo Code CLI、MastraCode 和 Antigravity CLI 的窗格。

原生会话恢复需要最新的 Herdr 集成: Pi 集成版本 `2`、OMP 版本 `3`、Claude Code 版本 `6`、Codex 版本 `5`、GitHub Copilot CLI 版本 `2`、Devin CLI 版本 `2`、Droid 版本 `2`、Kimi Code CLI 版本 `3`、Qoder CLI 版本 `2`、Cursor Agent CLI 版本 `1`、Grok CLI 版本 `1`、OpenCode 版本 `5`、Kilo Code CLI 版本 `1`、Hermes Agent 版本 `5`、MastraCode 版本 `1`、Antigravity CLI 版本 `1`。用 `herdr integration status` 查看已安装版本。
原生会话恢复需要最新的 Herdr 集成: Pi 集成版本 `2`、OMP 版本 `3`、Claude Code 版本 `6`、Codex 版本 `5`、GitHub Copilot CLI 版本 `2`、Devin CLI 版本 `2`、Droid 版本 `2`、Kimi Code CLI 版本 `3`、Qoder CLI 版本 `2`、Cursor Agent CLI 版本 `1`、Grok CLI 版本 `2`、OpenCode 版本 `5`、Kilo Code CLI 版本 `1`、Hermes Agent 版本 `5`、MastraCode 版本 `1`、Antigravity CLI 版本 `1`。用 `herdr integration status` 查看已安装版本。

## Pi

Expand Down
20 changes: 18 additions & 2 deletions src/integration/assets/grok/herdr-agent-state.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=grok
# HERDR_INTEGRATION_VERSION=1
# HERDR_INTEGRATION_VERSION=2

param([string]$Action = "")

Expand All @@ -26,6 +26,12 @@ $event = if ($null -ne $payload -and $payload.hook_event_name -is [string]) {
}
if ($null -ne $event -and $event -notin @("session_start", "SessionStart", "sessionStart")) { exit 0 }

$sessionStartSource = if ($null -ne $payload -and $payload.source -is [string]) {
$payload.source
} else {
$null
}

$sessionId = $env:GROK_SESSION_ID
if ([string]::IsNullOrWhiteSpace($sessionId) -and $null -ne $payload) {
if ($payload.session_id -is [string]) { $sessionId = $payload.session_id }
Expand All @@ -35,7 +41,17 @@ if ([string]::IsNullOrWhiteSpace($sessionId)) { exit 0 }

$seq = [DateTime]::UtcNow.Ticks
$herdr = if ([string]::IsNullOrWhiteSpace($env:HERDR_BIN_PATH)) { "herdr" } else { $env:HERDR_BIN_PATH }
$herdrArgs = @(
"pane", "report-agent-session", $env:HERDR_PANE_ID,
"--source", "herdr:grok",
"--agent", "grok",
"--seq", "$seq",
"--agent-session-id", "$sessionId"
)
if (-not [string]::IsNullOrWhiteSpace($sessionStartSource)) {
$herdrArgs += @("--session-start-source", "$sessionStartSource")
}
try {
& $herdr pane report-agent-session $env:HERDR_PANE_ID --source herdr:grok --agent grok --seq $seq --agent-session-id $sessionId 2>$null | Out-Null
& $herdr @herdrArgs 2>$null | Out-Null
} catch {
}
20 changes: 12 additions & 8 deletions src/integration/assets/grok/herdr-agent-state.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# managed by herdr; reinstalling or updating the integration overwrites this file.
# add custom hooks beside this file instead of editing it.
# HERDR_INTEGRATION_ID=grok
# HERDR_INTEGRATION_VERSION=1
# HERDR_INTEGRATION_VERSION=2

set -eu

Expand Down Expand Up @@ -62,6 +62,7 @@ def first_text(*keys):
hook_event_name = first_text("hook_event_name", "hookEventName")
if hook_event_name not in (None, "session_start", "SessionStart", "sessionStart"):
raise SystemExit(0)
session_start_source = first_text("source")

# Grok injects GROK_SESSION_ID into every hook process; prefer it and fall
# back to the event payload's session id fields.
Expand All @@ -72,16 +73,19 @@ if not agent_session_id:

request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}"
report_seq = time.time_ns()
params = {
"pane_id": pane_id,
"source": source,
"agent": "grok",
"seq": report_seq,
"agent_session_id": agent_session_id,
}
if session_start_source:
params["session_start_source"] = session_start_source
request = {
"id": request_id,
"method": "pane.report_agent_session",
"params": {
"pane_id": pane_id,
"source": source,
"agent": "grok",
"seq": report_seq,
"agent_session_id": agent_session_id,
},
"params": params,
}

try:
Expand Down
2 changes: 1 addition & 1 deletion src/integration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const GROK_HOOK_ASSET: &str = if cfg!(windows) {
} else {
include_str!("assets/grok/herdr-agent-state.sh")
};
const GROK_INTEGRATION_VERSION: u32 = 1;
const GROK_INTEGRATION_VERSION: u32 = 2;

pub(crate) const INSTALL_WARNING_PREFIX: &str = "warning:";

Expand Down
39 changes: 38 additions & 1 deletion src/integration/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2729,6 +2729,7 @@ fn bundled_integration_asset_versions_match_expected_versions() {
MASTRACODE_HOOK_ASSET,
MASTRACODE_INTEGRATION_VERSION,
),
("grok", GROK_HOOK_ASSET, GROK_INTEGRATION_VERSION),
] {
assert_eq!(
parse_integration_version(asset),
Expand Down Expand Up @@ -2855,6 +2856,10 @@ fn bundled_integration_assets_report_session_refs() {
assert!(GROK_HOOK_ASSET.contains("sessionId"));
assert!(GROK_HOOK_ASSET.contains("agent_session_id"));
assert!(GROK_HOOK_ASSET.contains("pane.report_agent_session"));
assert!(
GROK_HOOK_ASSET.contains("session_start_source")
|| GROK_HOOK_ASSET.contains("--session-start-source")
);
assert!(GROK_HOOK_ASSET.contains("herdr:grok"));
assert!(!GROK_HOOK_ASSET.contains("\"state\":"));
assert!(!GROK_HOOK_ASSET.contains("pane.release_agent"));
Expand Down Expand Up @@ -3873,7 +3878,39 @@ fn install_antigravity_cli_errors_when_config_dir_missing() {
}

#[test]
fn grok_v1_integration_status_is_current() {
fn grok_v1_integration_status_is_outdated() {
let _lock = integration_env_lock();
let base = unique_base();
let grok_dir = base.join(".grok");
let hooks_dir = grok_dir.join("hooks");
fs::create_dir_all(&hooks_dir).unwrap();
std::env::set_var(GROK_CONFIG_DIR_ENV_VAR, &grok_dir);
let hook_path = hooks_dir.join(GROK_HOOK_INSTALL_NAME);
fs::write(
&hook_path,
"#!/bin/sh\n# HERDR_INTEGRATION_ID=grok\n# HERDR_INTEGRATION_VERSION=1\n",
)
.unwrap();
fs::write(
hooks_dir.join(GROK_HOOK_CONFIG_INSTALL_NAME),
serde_json::to_string(&grok_hook_config(&hook_path)).unwrap(),
)
.unwrap();

let grok = installed_integration_statuses()
.into_iter()
.find(|status| status.target == crate::api::schema::IntegrationTarget::Grok)
.expect("grok integration status");
assert_eq!(grok.installed_version, Some(1));
assert_eq!(grok.expected_version, GROK_INTEGRATION_VERSION);
assert_eq!(grok.state, IntegrationStatusKind::Outdated);

clear_integration_path_env();
let _ = fs::remove_dir_all(base);
}

#[test]
fn grok_v2_integration_status_is_current() {
let _lock = integration_env_lock();
let base = unique_base();
let grok_dir = base.join(".grok");
Expand Down
65 changes: 64 additions & 1 deletion src/terminal/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,7 @@ impl TerminalState {
| ("herdr:hermes", "hermes", Some("startup" | "new" | "resume"))
| ("herdr:opencode", "opencode", Some("select"))
| ("herdr:pi", "pi", Some("new" | "resume" | "fork"))
| ("herdr:grok", "grok", Some("new"))
| (
"herdr:omp",
"omp",
Expand Down Expand Up @@ -1616,7 +1617,8 @@ impl TerminalState {
session_ref: &crate::agent_resume::AgentSessionRef,
session_start_source: Option<&str>,
) -> bool {
Self::session_start_source_is_recognized(session_start_source)
(source, agent_label) != ("herdr:grok", "grok")
&& Self::session_start_source_is_recognized(session_start_source)
&& self.foreground_agent_confirms_session_owner(source, agent_label, session_ref)
}

Expand Down Expand Up @@ -4551,6 +4553,38 @@ mod tests {
}
}

#[test]
fn grok_new_session_ref_replaces_existing_session_ref() {
let mut terminal = test_terminal();
terminal
.set_agent_session_ref(
"herdr:grok".into(),
"grok".into(),
crate::agent_resume::AgentSessionRef::id("grok-old"),
Some(20),
)
.expect("initial session should be accepted");

let mutation = terminal
.set_agent_session_ref_for_session_start(
"herdr:grok".into(),
"grok".into(),
crate::agent_resume::AgentSessionRef::id("grok-new"),
Some(21),
Some("new".into()),
)
.expect("new should replace the grok session");

assert!(mutation.session_ref_changed);
assert_eq!(
terminal
.persisted_agent_session
.as_ref()
.map(|session| session.session_ref.value.as_str()),
Some("grok-new")
);
}

#[test]
fn opencode_server_new_does_not_replace_existing_session_ref() {
let mut terminal = test_terminal();
Expand Down Expand Up @@ -4950,6 +4984,35 @@ mod tests {
);
}

#[test]
fn grok_new_session_does_not_replace_a_different_owner() {
let mut terminal = test_terminal();
terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession {
source: "herdr:claude".into(),
agent: "claude".into(),
session_ref: crate::agent_resume::AgentSessionRef::id("claude-session").unwrap(),
});
terminal.set_detected_state(Some(Agent::Grok), AgentState::Idle);

let mutation = terminal.set_agent_session_ref_for_session_start(
"herdr:grok".into(),
"grok".into(),
crate::agent_resume::AgentSessionRef::id("grok-session"),
Some(21),
Some("new".into()),
);

assert!(mutation.is_none());
assert_eq!(
terminal.persisted_agent_session.as_ref().map(|session| (
session.source.as_str(),
session.agent.as_str(),
session.session_ref.value.as_str()
)),
Some(("herdr:claude", "claude", "claude-session"))
);
}

#[test]
fn foreground_agent_session_replaces_stale_different_owner_session_ref() {
for session_start_source in ["resume", "startup"] {
Expand Down
22 changes: 22 additions & 0 deletions tests/cli/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ fn run_devin_hook(
)
}

fn run_grok_hook(hook_input: &str, envs: &[(&str, &str)]) -> Option<serde_json::Value> {
run_shell_hook_with_env(
"src/integration/assets/grok/herdr-agent-state.sh",
&["session"],
hook_input,
envs,
)
}

fn run_shell_hook(asset_path: &str, args: &[&str], hook_input: &str) -> Option<serde_json::Value> {
run_shell_hook_with_env(asset_path, args, hook_input, &[])
}
Expand Down Expand Up @@ -211,6 +220,19 @@ fn copilot_hook_reports_session_id_from_stdin() {
assert!(camel["params"].get("state").is_none());
}

#[test]
fn grok_hook_reports_new_session_source() {
let request = run_grok_hook(
r#"{"hook_event_name":"session_start","source":"new","session_id":"new-session"}"#,
&[("GROK_SESSION_ID", "new-session")],
)
.expect("grok session start should report session identity");

assert_eq!(request["method"], "pane.report_agent_session");
assert_eq!(request["params"]["agent_session_id"], "new-session");
assert_eq!(request["params"]["session_start_source"], "new");
}

#[test]
fn copilot_hook_does_not_report_lifecycle_state() {
for payload in [
Expand Down
Loading