diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 428a973d3a..b8f76e6d06 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -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) diff --git a/docs/next/website/src/content/docs/integrations.mdx b/docs/next/website/src/content/docs/integrations.mdx index 67ea8c3ea6..b727865bbb 100644 --- a/docs/next/website/src/content/docs/integrations.mdx +++ b/docs/next/website/src/content/docs/integrations.mdx @@ -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 diff --git a/docs/next/website/src/content/docs/ja/integrations.mdx b/docs/next/website/src/content/docs/ja/integrations.mdx index 084c9b356b..cb0979c608 100644 --- a/docs/next/website/src/content/docs/ja/integrations.mdx +++ b/docs/next/website/src/content/docs/ja/integrations.mdx @@ -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 diff --git a/docs/next/website/src/content/docs/session-state.mdx b/docs/next/website/src/content/docs/session-state.mdx index 548cf120a2..51096db264 100644 --- a/docs/next/website/src/content/docs/session-state.mdx +++ b/docs/next/website/src/content/docs/session-state.mdx @@ -70,7 +70,7 @@ Native session restore requires these Herdr integration versions or newer: | Claude Code | `6` | `claude --resume ` | | Codex | `5` | `codex resume ` | | Cursor Agent CLI | `1` | `cursor-agent --resume ` | -| Grok CLI | `1` | `grok --resume ` | +| Grok CLI | `2` | `grok --resume ` | | GitHub Copilot CLI | `2` | `copilot --resume=` | | Devin CLI | `2` | `devin --resume ` | | Droid | `2` | `droid --resume ` | diff --git a/docs/next/website/src/content/docs/zh-cn/integrations.mdx b/docs/next/website/src/content/docs/zh-cn/integrations.mdx index c9a39ba8a5..8b27806410 100644 --- a/docs/next/website/src/content/docs/zh-cn/integrations.mdx +++ b/docs/next/website/src/content/docs/zh-cn/integrations.mdx @@ -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 diff --git a/src/integration/assets/grok/herdr-agent-state.ps1 b/src/integration/assets/grok/herdr-agent-state.ps1 index 69c4f51512..1b183a3b90 100644 --- a/src/integration/assets/grok/herdr-agent-state.ps1 +++ b/src/integration/assets/grok/herdr-agent-state.ps1 @@ -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 = "") @@ -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 } @@ -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 { } diff --git a/src/integration/assets/grok/herdr-agent-state.sh b/src/integration/assets/grok/herdr-agent-state.sh index bdf0f6715b..3eaa3431fd 100644 --- a/src/integration/assets/grok/herdr-agent-state.sh +++ b/src/integration/assets/grok/herdr-agent-state.sh @@ -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 @@ -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. @@ -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: diff --git a/src/integration/mod.rs b/src/integration/mod.rs index 9630b578a7..0af63b33e9 100644 --- a/src/integration/mod.rs +++ b/src/integration/mod.rs @@ -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:"; diff --git a/src/integration/tests.rs b/src/integration/tests.rs index 6eaf7a3931..51d6deb845 100644 --- a/src/integration/tests.rs +++ b/src/integration/tests.rs @@ -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), @@ -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")); @@ -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"); diff --git a/src/terminal/state.rs b/src/terminal/state.rs index 4b1fdaef4a..6f3e33f35f 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -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", @@ -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) } @@ -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(); @@ -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"] { diff --git a/tests/cli/hooks.rs b/tests/cli/hooks.rs index 21e7036b96..ff655e7a0f 100644 --- a/tests/cli/hooks.rs +++ b/tests/cli/hooks.rs @@ -37,6 +37,15 @@ fn run_devin_hook( ) } +fn run_grok_hook(hook_input: &str, envs: &[(&str, &str)]) -> Option { + 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 { run_shell_hook_with_env(asset_path, args, hook_input, &[]) } @@ -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 [