diff --git a/docs/next/CHANGELOG.md b/docs/next/CHANGELOG.md index 428a973d3a..7fd6135c8a 100644 --- a/docs/next/CHANGELOG.md +++ b/docs/next/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Added +- Added Qwen Code detection for idle, working, and user-confirmation states, plus optional native session restore. - Herdr now keeps the outer terminal window title in sync with the session through `ui.window_title`, so window managers and terminal tab bars show the active workspace and the host the panes actually run on. - The desktop tab bar now has configurable right-aligned status entries for zoom state, hostname, date/time, literal text, and asynchronously refreshed command output. - Optional `keys.move_tab_previous` and `keys.move_tab_next` bindings now reorder the active tab in place, wrapping at either end. diff --git a/docs/next/api/herdr-api.schema.json b/docs/next/api/herdr-api.schema.json index 8a552da010..674dac8da5 100644 --- a/docs/next/api/herdr-api.schema.json +++ b/docs/next/api/herdr-api.schema.json @@ -2117,6 +2117,7 @@ "kilo", "hermes", "qodercli", + "qwen", "cursor", "mastracode", "antigravity_cli", @@ -7172,6 +7173,7 @@ "kilo", "hermes", "qodercli", + "qwen", "cursor", "mastracode", "antigravity_cli", diff --git a/docs/next/website/src/content/docs/agent-automation.mdx b/docs/next/website/src/content/docs/agent-automation.mdx index 33bb14a502..f1ca77b25c 100644 --- a/docs/next/website/src/content/docs/agent-automation.mdx +++ b/docs/next/website/src/content/docs/agent-automation.mdx @@ -41,7 +41,7 @@ Agent commands accept either a unique live name or the pane ID that currently ho An available shell pane is at its interactive shell prompt: the shell itself owns the foreground, with no foreground command, editor, or agent running. Return the pane to its prompt before calling `agent start`. -`--kind` selects a supported agent and its canonical executable. Supported kinds are `pi`, `claude`, `codex`, `gemini`, `cursor`, `devin`, `agy`, `cline`, `omp`, `mastracode`, `opencode`, `copilot`, `kimi`, `kiro`, `droid`, `amp`, `grok`, `hermes`, `kilo`, `qodercli`, and `maki`. Arguments after `--` are passed unchanged to that executable. +`--kind` selects a supported agent and its canonical executable. Supported kinds are `pi`, `claude`, `codex`, `gemini`, `cursor`, `devin`, `agy`, `cline`, `omp`, `mastracode`, `opencode`, `copilot`, `kimi`, `kiro`, `droid`, `amp`, `grok`, `hermes`, `kilo`, `qodercli`, `qwen`, and `maki`. Arguments after `--` are passed unchanged to that executable. Successful `agent start` returns only after Herdr detects the expected agent in the same terminal and marks it ready for interactive input. If detection reports `blocked` during startup, the command returns `agent_not_ready` immediately. The name remains available for `agent read` and `agent send-keys`, and becomes ready for prompts after detection reports `idle`. Startup waits for 30 seconds by default; `--timeout` must be greater than 3000 and no more than 300000 milliseconds. diff --git a/docs/next/website/src/content/docs/agents.mdx b/docs/next/website/src/content/docs/agents.mdx index 58939ed4ba..c43097e213 100644 --- a/docs/next/website/src/content/docs/agents.mdx +++ b/docs/next/website/src/content/docs/agents.mdx @@ -20,6 +20,7 @@ Automatic detection works out of the box for common coding agents. The table sho | Kimi Code CLI | lifecycle hooks when installed; otherwise screen manifest | state and session | | Hermes Agent | screen manifest | session | | Qoder CLI | screen manifest | session | +| Qwen Code | screen manifest | session | | Droid | screen manifest | session | | OpenCode | lifecycle plugin when installed; otherwise screen manifest | state and session | | Kilo Code CLI | lifecycle plugin when installed; otherwise screen manifest | state and session | diff --git a/docs/next/website/src/content/docs/cli-reference.mdx b/docs/next/website/src/content/docs/cli-reference.mdx index 1a362ca911..e3c8a934e6 100644 --- a/docs/next/website/src/content/docs/cli-reference.mdx +++ b/docs/next/website/src/content/docs/cli-reference.mdx @@ -305,7 +305,7 @@ herdr agent explain --file PATH --agent LABEL [--json|--verbose] Agent targets are either a unique live agent name or the pane ID that currently hosts the agent. Terminal IDs and bare agent-kind labels are not agent targets. Agents started through `agent start` require a name; manually launched agents remain unnamed and use their pane ID. -`agent start` activates an existing available shell pane: the pane's interactive shell must own the foreground, with no foreground command, editor, or agent running. Topology must be created separately. Names are unique among live agents and must match `[a-z][a-z0-9_-]{0,31}`. The kind selects Herdr's canonical interactive executable, while arguments after `--` are passed to that executable. Supported kinds are `pi`, `claude`, `codex`, `gemini`, `cursor`, `devin`, `agy`, `cline`, `omp`, `mastracode`, `opencode`, `copilot`, `kimi`, `kiro`, `droid`, `amp`, `grok`, `hermes`, `kilo`, `qodercli`, and `maki`. A name follows the current pane occupant and is cleared when that agent exits, is released, or is replaced. Temporary detection uncertainty does not clear it. +`agent start` activates an existing available shell pane: the pane's interactive shell must own the foreground, with no foreground command, editor, or agent running. Topology must be created separately. Names are unique among live agents and must match `[a-z][a-z0-9_-]{0,31}`. The kind selects Herdr's canonical interactive executable, while arguments after `--` are passed to that executable. Supported kinds are `pi`, `claude`, `codex`, `gemini`, `cursor`, `devin`, `agy`, `cline`, `omp`, `mastracode`, `opencode`, `copilot`, `kimi`, `kiro`, `droid`, `amp`, `grok`, `hermes`, `kilo`, `qodercli`, `qwen`, and `maki`. A name follows the current pane occupant and is cleared when that agent exits, is released, or is replaced. Temporary detection uncertainty does not clear it. A successful start returns only after the expected agent owns the same terminal and is ready for interactive input. If detection reports `blocked` during startup, the command returns `agent_not_ready` immediately. The name remains available for `agent read` and `agent send-keys`, and becomes ready for prompts after detection reports `idle`. The default startup timeout is 30000 milliseconds; explicit values must be greater than 3000 and no more than 300000. @@ -372,6 +372,7 @@ herdr integration install opencode herdr integration install kilo herdr integration install hermes herdr integration install qodercli +herdr integration install qwen herdr integration install cursor herdr integration install mastracode herdr integration install grok @@ -387,6 +388,7 @@ herdr integration uninstall opencode herdr integration uninstall kilo herdr integration uninstall hermes herdr integration uninstall qodercli +herdr integration uninstall qwen herdr integration uninstall cursor herdr integration uninstall mastracode herdr integration uninstall grok diff --git a/docs/next/website/src/content/docs/integrations.mdx b/docs/next/website/src/content/docs/integrations.mdx index 67ea8c3ea6..398fdc5e3f 100644 --- a/docs/next/website/src/content/docs/integrations.mdx +++ b/docs/next/website/src/content/docs/integrations.mdx @@ -1,6 +1,6 @@ --- title: Integrations -description: Install Herdr integrations for Pi, OMP, Claude Code, Codex, GitHub Copilot CLI, Devin CLI, Droid, Kimi Code CLI, OpenCode, Kilo Code CLI, Hermes Agent, Qoder CLI, Cursor Agent CLI, MastraCode, Antigravity CLI, and Grok CLI. +description: Install Herdr integrations for Pi, OMP, Claude Code, Codex, GitHub Copilot CLI, Devin CLI, Droid, Kimi Code CLI, OpenCode, Kilo Code CLI, Hermes Agent, Qoder CLI, Qwen Code, Cursor Agent CLI, MastraCode, Antigravity CLI, and Grok CLI. --- Herdr detects supported agents automatically. Install official integrations when you want native agent session restore, direct lifecycle reports, or both. See [Agents](/docs/agents/) for the full status authority model. @@ -22,6 +22,7 @@ herdr integration install opencode herdr integration install kilo herdr integration install hermes herdr integration install qodercli +herdr integration install qwen herdr integration install cursor herdr integration install mastracode herdr integration install antigravity-cli @@ -43,6 +44,7 @@ herdr integration uninstall opencode herdr integration uninstall kilo herdr integration uninstall hermes herdr integration uninstall qodercli +herdr integration uninstall qwen herdr integration uninstall cursor herdr integration uninstall mastracode herdr integration uninstall antigravity-cli @@ -56,7 +58,7 @@ Herdr uses integrations in two ways: | Integration type | Agents | Effect | | --- | --- | --- | | Lifecycle authority | Pi, OMP, Kimi Code CLI, OpenCode, Kilo Code CLI, MastraCode | When installed and actively reporting for the pane, hook or plugin events author `idle`, `working`, and `blocked`. Herdr does not also use screen manifest fallback for that same lifecycle authority. | -| Session identity | Claude Code, Codex, GitHub Copilot CLI, Devin CLI, Droid, Qoder CLI, Cursor Agent CLI, Hermes Agent, Antigravity CLI, Grok CLI | The integration reports native session references for restore. State still comes from Herdr's screen manifest detection. | +| Session identity | Claude Code, Codex, GitHub Copilot CLI, Devin CLI, Droid, Qoder CLI, Qwen Code, Cursor Agent CLI, Hermes Agent, Antigravity CLI, Grok CLI | The integration reports native session references for restore. State still comes from Herdr's screen manifest detection. | Custom integrations can also report state that is not visible in the native terminal UI. They do not need to be built into Herdr or use a recognized agent executable. @@ -87,9 +89,9 @@ Use `HERDR_BIN_PATH` and the CLI wrappers for portable integrations. Code that n [Prime Agent's built-in Herdr reporter](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/core/extensions/builtin/herdr-agent-state.ts) is a real-world example. It activates only inside Herdr, maps agent events to `working`, `idle`, and `blocked`, preserves report ordering across sessions, and releases authority on exit. -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. +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, Qwen Code, 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`, Qwen Code version `1`, 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`. ## Pi @@ -257,6 +259,20 @@ Herdr resumes stored Qoder CLI sessions with `qodercli --resume `. Native screen manifest detection remains available when the hook is not installed. +## Qwen Code + +Install the Qwen Code hook: + +```bash +herdr integration install qwen +``` + +The `SessionStart` hook reports only Qwen Code's session identity for native restore. Lifecycle state remains under Herdr's screen manifest detection. + +Herdr uses `~/.qwen` by default, or `QWEN_HOME` when set. The Qwen config directory must already exist. Install writes `hooks/herdr-agent-session.sh` (`hooks/herdr-agent-session.ps1` on Windows) and adds a Herdr entry to `settings.json`. Uninstall removes only the matching entry and managed script. + +Herdr resumes stored Qwen Code sessions with `qwen --resume `. + ## Cursor Agent CLI Install the Cursor Agent CLI hook: diff --git a/docs/next/website/src/content/docs/ja/agent-automation.mdx b/docs/next/website/src/content/docs/ja/agent-automation.mdx index 74584574bb..1f857cd311 100644 --- a/docs/next/website/src/content/docs/ja/agent-automation.mdx +++ b/docs/next/website/src/content/docs/ja/agent-automation.mdx @@ -41,7 +41,7 @@ review_pane=$(printf '%s\n' "$split" | jq -r '.result.pane.pane_id') 利用可能なシェルペインとは、対話シェルのプロンプトに戻っており、フォアグラウンドをシェル自身が所有し、フォアグラウンドのコマンド、エディタ、エージェントが動いていないペインです。`agent start` の前にプロンプトへ戻してください。 -`--kind` は対応済みエージェントとその標準実行ファイルを選びます。対応する kind は `pi`、`claude`、`codex`、`gemini`、`cursor`、`devin`、`agy`、`cline`、`omp`、`mastracode`、`opencode`、`copilot`、`kimi`、`kiro`、`droid`、`amp`、`grok`、`hermes`、`kilo`、`qodercli`、`maki` です。`--` より後の引数は、その実行ファイルへそのまま渡されます。 +`--kind` は対応済みエージェントとその標準実行ファイルを選びます。対応する kind は `pi`、`claude`、`codex`、`gemini`、`cursor`、`devin`、`agy`、`cline`、`omp`、`mastracode`、`opencode`、`copilot`、`kimi`、`kiro`、`droid`、`amp`、`grok`、`hermes`、`kilo`、`qodercli`、`qwen`、`maki` です。`--` より後の引数は、その実行ファイルへそのまま渡されます。 成功した `agent start` は、同じターミナルで期待したエージェントを検出し、対話入力の準備ができたと Herdr が判断してから返ります。起動中の検出状態が `blocked` の場合、コマンドは直ちに `agent_not_ready` を返します。名前は `agent read` と `agent send-keys` で引き続き使用でき、検出状態が `idle` になるとプロンプトを送信できるようになります。起動待機はデフォルトで 30 秒です。`--timeout` は 3000 より大きく 300000 以下のミリ秒で指定します。 diff --git a/docs/next/website/src/content/docs/ja/agents.mdx b/docs/next/website/src/content/docs/ja/agents.mdx index eff614e7b2..0cc00a16c9 100644 --- a/docs/next/website/src/content/docs/ja/agents.mdx +++ b/docs/next/website/src/content/docs/ja/agents.mdx @@ -20,6 +20,7 @@ Herdr は複数のコーディングエージェントを同時に動かすた | Kimi Code CLI | インストール時はライフサイクルフック。それ以外はスクリーンマニフェスト | 状態とセッション | | Hermes Agent | スクリーンマニフェスト | セッション | | Qoder CLI | スクリーンマニフェスト | セッション | +| Qwen Code | スクリーンマニフェスト | セッション | | Droid | スクリーンマニフェスト | セッション | | OpenCode | インストール時はライフサイクルプラグイン。それ以外はスクリーンマニフェスト | 状態とセッション | | Kilo Code CLI | インストール時はライフサイクルプラグイン。それ以外はスクリーンマニフェスト | 状態とセッション | diff --git a/docs/next/website/src/content/docs/ja/cli-reference.mdx b/docs/next/website/src/content/docs/ja/cli-reference.mdx index 76be2b433c..774dd86fef 100644 --- a/docs/next/website/src/content/docs/ja/cli-reference.mdx +++ b/docs/next/website/src/content/docs/ja/cli-reference.mdx @@ -285,7 +285,7 @@ herdr agent explain --file PATH --agent LABEL [--json|--verbose] エージェントターゲットは、一意なライブエージェント名、または現在そのエージェントをホストしているペイン ID です。ターミナル ID とエージェント kind のラベルだけでは指定できません。`agent start` で起動するエージェントには名前が必須で、手動で起動したエージェントは名前なしのままペイン ID で指定します。 -`agent start` は既存の利用可能なシェルペインを起動対象にします。対話シェル自身がフォアグラウンドを所有し、フォアグラウンドのコマンド、エディタ、エージェントが動いていない必要があります。トポロジーは別に作成します。名前はライブエージェント間で一意で、`[a-z][a-z0-9_-]{0,31}` に一致する必要があります。対応する kind は `pi`、`claude`、`codex`、`gemini`、`cursor`、`devin`、`agy`、`cline`、`omp`、`mastracode`、`opencode`、`copilot`、`kimi`、`kiro`、`droid`、`amp`、`grok`、`hermes`、`kilo`、`qodercli`、`maki` です。名前は現在のペイン占有者に属し、そのエージェントの終了、release、置換で消えます。一時的に検出できないだけでは消えません。 +`agent start` は既存の利用可能なシェルペインを起動対象にします。対話シェル自身がフォアグラウンドを所有し、フォアグラウンドのコマンド、エディタ、エージェントが動いていない必要があります。トポロジーは別に作成します。名前はライブエージェント間で一意で、`[a-z][a-z0-9_-]{0,31}` に一致する必要があります。対応する kind は `pi`、`claude`、`codex`、`gemini`、`cursor`、`devin`、`agy`、`cline`、`omp`、`mastracode`、`opencode`、`copilot`、`kimi`、`kiro`、`droid`、`amp`、`grok`、`hermes`、`kilo`、`qodercli`、`qwen`、`maki` です。名前は現在のペイン占有者に属し、そのエージェントの終了、release、置換で消えます。一時的に検出できないだけでは消えません。 成功した start は、期待したエージェントが同じターミナルを所有し、対話入力の準備ができてから返ります。起動中の検出状態が `blocked` の場合、コマンドは直ちに `agent_not_ready` を返します。名前は `agent read` と `agent send-keys` で引き続き使用でき、検出状態が `idle` になるとプロンプトを送信できるようになります。デフォルトの起動タイムアウトは 30000 ミリ秒で、明示する値は 3000 より大きく 300000 以下でなければなりません。 @@ -340,6 +340,7 @@ herdr integration install kilo herdr integration install hermes herdr integration install mastracode herdr integration install qodercli +herdr integration install qwen herdr integration install cursor herdr integration uninstall pi herdr integration uninstall omp @@ -354,6 +355,7 @@ herdr integration uninstall kilo herdr integration uninstall hermes herdr integration uninstall mastracode herdr integration uninstall qodercli +herdr integration uninstall qwen herdr integration uninstall cursor herdr integration status [--outdated-only] ``` diff --git a/docs/next/website/src/content/docs/ja/integrations.mdx b/docs/next/website/src/content/docs/ja/integrations.mdx index 084c9b356b..3f647bec90 100644 --- a/docs/next/website/src/content/docs/ja/integrations.mdx +++ b/docs/next/website/src/content/docs/ja/integrations.mdx @@ -1,6 +1,6 @@ --- title: インテグレーション -description: Pi、OMP、Claude Code、Codex、GitHub Copilot CLI、Devin CLI、Droid、Kimi Code CLI、OpenCode、Kilo Code CLI、Hermes Agent、Qoder CLI、Cursor Agent CLI、MastraCode、Antigravity CLI、Grok CLI 向けの Herdr インテグレーションをインストールします。 +description: Pi、OMP、Claude Code、Codex、GitHub Copilot CLI、Devin CLI、Droid、Kimi Code CLI、OpenCode、Kilo Code CLI、Hermes Agent、Qoder CLI、Qwen Code、Cursor Agent CLI、MastraCode、Antigravity CLI、Grok CLI 向けの Herdr インテグレーションをインストールします。 --- Herdr は対応エージェントを自動的に検出します。公式インテグレーションは、復元のためのネイティブセッション識別、ライフサイクル状態の報告、またはその両方を追加できます。 @@ -25,6 +25,7 @@ herdr integration install kilo herdr integration install hermes herdr integration install mastracode herdr integration install qodercli +herdr integration install qwen herdr integration install cursor herdr integration install antigravity-cli herdr integration install grok @@ -46,6 +47,7 @@ herdr integration uninstall kilo herdr integration uninstall hermes herdr integration uninstall mastracode herdr integration uninstall qodercli +herdr integration uninstall qwen herdr integration uninstall cursor herdr integration uninstall antigravity-cli herdr integration uninstall grok @@ -58,7 +60,7 @@ Herdr はインテグレーションを 2 つの異なる方法で使います: | インテグレーションの種類 | エージェント | 効果 | | --- | --- | --- | | ライフサイクル権威 | Pi、OMP、Kimi Code CLI、OpenCode、Kilo Code CLI、MastraCode | インストールされ、そのペインについて能動的に報告している間は、フックまたはプラグインのイベントが `idle`、`working`、`blocked` を決定します。同じライフサイクル権威に対して、Herdr はスクリーンマニフェストのフォールバックを併用しません。 | -| セッション識別 | Claude Code、Codex、GitHub Copilot CLI、Devin CLI、Droid、Qoder CLI、Cursor Agent CLI、Hermes Agent、Antigravity CLI、Grok CLI | インテグレーションは復元用のネイティブセッション参照を報告します。状態は引き続き Herdr のスクリーンマニフェスト検出から得られます。 | +| セッション識別 | Claude Code、Codex、GitHub Copilot CLI、Devin CLI、Droid、Qoder CLI、Qwen Code、Cursor Agent CLI、Hermes Agent、Antigravity CLI、Grok CLI | インテグレーションは復元用のネイティブセッション参照を報告します。状態は引き続き Herdr のスクリーンマニフェスト検出から得られます。 | カスタムインテグレーションも、ネイティブのターミナル UI では見えない状態を定義する場合に状態を報告できます。Herdr への組み込みや、認識済みのエージェント実行ファイルは必要ありません。 @@ -89,9 +91,9 @@ Herdr の外では何もしないように、`HERDR_ENV=1` で必要な変数が [Prime Agent の組み込み Herdr レポーター](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/core/extensions/builtin/herdr-agent-state.ts)は実際の実装例です。Herdr 内でのみ有効になり、エージェントイベントを `working`、`idle`、`blocked` に対応付け、セッションをまたいで報告順序を維持し、終了時に権威を解放します。 -一部のインテグレーションは、エージェントのネイティブセッション参照を報告します。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 は公式のセッション参照を使って、`[session] resume_agents_on_restore = false` で無効化されていない限り、Herdr サーバーの再起動後に Claude Code、Codex、Devin CLI、Droid、Kimi Code CLI、Qoder CLI、Qwen Code、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`、Qwen Code は `1`、Cursor Agent CLI は `1`、Grok CLI は `1`、OpenCode は `5`、Kilo Code CLI は `1`、Hermes Agent は `5`、MastraCode は `1`、Antigravity CLI は `1` です。インストール済みバージョンは `herdr integration status` で確認してください。 ## Pi @@ -259,6 +261,20 @@ Herdr は保存された Qoder CLI セッションを `qodercli --resume ` フックがインストールされていないときは、スクリーンマニフェスト検出が引き続き利用できます。 +## Qwen Code + +Qwen Code フックをインストールします: + +```bash +herdr integration install qwen +``` + +`SessionStart` フックはネイティブ復元用のセッション識別だけを報告します。ライフサイクル状態は引き続き Herdr のスクリーンマニフェスト検出が決定します。 + +Herdr はデフォルトで `~/.qwen` を使い、`QWEN_HOME` が設定されていればそちらを使います。Qwen の設定ディレクトリはあらかじめ存在している必要があります。インストールは `hooks/herdr-agent-session.sh`(Windows では `hooks/herdr-agent-session.ps1`)を書き込み、`settings.json` に Herdr エントリを追加します。アンインストールは一致するエントリと管理対象スクリプトだけを削除します。 + +Herdr は保存された Qwen Code セッションを `qwen --resume ` で resume します。 + ## Cursor Agent CLI Cursor Agent CLI フックをインストールします: diff --git a/docs/next/website/src/content/docs/ja/session-state.mdx b/docs/next/website/src/content/docs/ja/session-state.mdx index ed3e73a689..6f3753f0a8 100644 --- a/docs/next/website/src/content/docs/ja/session-state.mdx +++ b/docs/next/website/src/content/docs/ja/session-state.mdx @@ -76,6 +76,7 @@ Herdr が resume するのは、現行の公式 Herdr インテグレーショ | Droid | `2` | `droid --resume ` | | Kimi Code CLI | `3` | `kimi --session ` | | Qoder CLI | `2` | `qodercli --resume ` | +| Qwen Code | `1` | `qwen --resume ` | | OpenCode | `5` | `opencode --session ` | | Kilo Code CLI | `1` | `kilo --session ` | | Hermes Agent | `2` | `hermes --resume ` | diff --git a/docs/next/website/src/content/docs/session-state.mdx b/docs/next/website/src/content/docs/session-state.mdx index 548cf120a2..f6d1b95ef6 100644 --- a/docs/next/website/src/content/docs/session-state.mdx +++ b/docs/next/website/src/content/docs/session-state.mdx @@ -76,6 +76,7 @@ Native session restore requires these Herdr integration versions or newer: | Droid | `2` | `droid --resume ` | | Kimi Code CLI | `3` | `kimi --session ` | | Qoder CLI | `2` | `qodercli --resume ` | +| Qwen Code | `1` | `qwen --resume ` | | OpenCode | `5` | `opencode --session ` | | Kilo Code CLI | `1` | `kilo --session ` | | Hermes Agent | `2` | `hermes --resume ` | diff --git a/docs/next/website/src/content/docs/zh-cn/agent-automation.mdx b/docs/next/website/src/content/docs/zh-cn/agent-automation.mdx index 35b98b371c..e48e9dc918 100644 --- a/docs/next/website/src/content/docs/zh-cn/agent-automation.mdx +++ b/docs/next/website/src/content/docs/zh-cn/agent-automation.mdx @@ -41,7 +41,7 @@ review_pane=$(printf '%s\n' "$split" | jq -r '.result.pane.pane_id') 可用的 shell 窗格必须停在交互式 shell 提示符,由 shell 自身占用前台,没有正在前台运行的命令、编辑器或智能体。调用 `agent start` 前先让窗格回到提示符。 -`--kind` 选择受支持的智能体及其标准可执行文件。支持的 kind 是 `pi`、`claude`、`codex`、`gemini`、`cursor`、`devin`、`agy`、`cline`、`omp`、`mastracode`、`opencode`、`copilot`、`kimi`、`kiro`、`droid`、`amp`、`grok`、`hermes`、`kilo`、`qodercli` 和 `maki`。`--` 后的参数会原样传给该可执行文件。 +`--kind` 选择受支持的智能体及其标准可执行文件。支持的 kind 是 `pi`、`claude`、`codex`、`gemini`、`cursor`、`devin`、`agy`、`cline`、`omp`、`mastracode`、`opencode`、`copilot`、`kimi`、`kiro`、`droid`、`amp`、`grok`、`hermes`、`kilo`、`qodercli`、`qwen` 和 `maki`。`--` 后的参数会原样传给该可执行文件。 成功的 `agent start` 只有在 Herdr 于同一终端检测到预期智能体,并确认它可接受交互输入后才返回。如果启动期间检测到 `blocked`,命令会立即返回 `agent_not_ready`。该名称仍可用于 `agent read` 和 `agent send-keys`,检测变为 `idle` 后即可用于发送提示。默认等待启动 30 秒;`--timeout` 必须大于 3000 且不超过 300000 毫秒。 diff --git a/docs/next/website/src/content/docs/zh-cn/agents.mdx b/docs/next/website/src/content/docs/zh-cn/agents.mdx index 08dec2c4fb..4e4f6c06b3 100644 --- a/docs/next/website/src/content/docs/zh-cn/agents.mdx +++ b/docs/next/website/src/content/docs/zh-cn/agents.mdx @@ -20,6 +20,7 @@ Herdr 为同时运行多个编程智能体而生。每个智能体都待在一 | Kimi Code CLI | 安装后为生命周期钩子;否则为屏幕清单 | 状态与会话 | | Hermes Agent | 屏幕清单 | 会话 | | Qoder CLI | 屏幕清单 | 会话 | +| Qwen Code | 屏幕清单 | 会话 | | Droid | 屏幕清单 | 会话 | | OpenCode | 安装后为生命周期插件;否则为屏幕清单 | 状态与会话 | | Kilo Code CLI | 安装后为生命周期插件;否则为屏幕清单 | 状态与会话 | diff --git a/docs/next/website/src/content/docs/zh-cn/cli-reference.mdx b/docs/next/website/src/content/docs/zh-cn/cli-reference.mdx index 2de976b145..9706daafa5 100644 --- a/docs/next/website/src/content/docs/zh-cn/cli-reference.mdx +++ b/docs/next/website/src/content/docs/zh-cn/cli-reference.mdx @@ -285,7 +285,7 @@ herdr agent explain --file PATH --agent LABEL [--json|--verbose] 智能体目标只能是唯一的实时智能体名称,或当前承载该智能体的窗格 ID。终端 ID 和单独的智能体 kind 标签不能作为目标。通过 `agent start` 启动的智能体必须有名称;手动启动的智能体保持未命名,通过窗格 ID 寻址。 -`agent start` 会在现有可用 shell 窗格中启动智能体:交互式 shell 必须占用前台,不能有正在前台运行的命令、编辑器或智能体。拓扑必须单独创建。名称在实时智能体中必须唯一,并匹配 `[a-z][a-z0-9_-]{0,31}`。支持的 kind 是 `pi`、`claude`、`codex`、`gemini`、`cursor`、`devin`、`agy`、`cline`、`omp`、`mastracode`、`opencode`、`copilot`、`kimi`、`kiro`、`droid`、`amp`、`grok`、`hermes`、`kilo`、`qodercli` 和 `maki`。名称属于当前窗格占用者,在该智能体退出、release 或被替换时清除;短暂的检测不确定不会清除它。 +`agent start` 会在现有可用 shell 窗格中启动智能体:交互式 shell 必须占用前台,不能有正在前台运行的命令、编辑器或智能体。拓扑必须单独创建。名称在实时智能体中必须唯一,并匹配 `[a-z][a-z0-9_-]{0,31}`。支持的 kind 是 `pi`、`claude`、`codex`、`gemini`、`cursor`、`devin`、`agy`、`cline`、`omp`、`mastracode`、`opencode`、`copilot`、`kimi`、`kiro`、`droid`、`amp`、`grok`、`hermes`、`kilo`、`qodercli`、`qwen` 和 `maki`。名称属于当前窗格占用者,在该智能体退出、release 或被替换时清除;短暂的检测不确定不会清除它。 成功的 start 只有在预期智能体占用同一终端并可接受交互输入后才返回。如果启动期间检测到 `blocked`,命令会立即返回 `agent_not_ready`。该名称仍可用于 `agent read` 和 `agent send-keys`,检测变为 `idle` 后即可用于发送提示。默认启动超时是 30000 毫秒;显式值必须大于 3000 且不超过 300000。 @@ -340,6 +340,7 @@ herdr integration install kilo herdr integration install hermes herdr integration install mastracode herdr integration install qodercli +herdr integration install qwen herdr integration install cursor herdr integration uninstall pi herdr integration uninstall omp @@ -354,6 +355,7 @@ herdr integration uninstall kilo herdr integration uninstall hermes herdr integration uninstall mastracode herdr integration uninstall qodercli +herdr integration uninstall qwen herdr integration uninstall cursor herdr integration status [--outdated-only] ``` 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..10a02f5373 100644 --- a/docs/next/website/src/content/docs/zh-cn/integrations.mdx +++ b/docs/next/website/src/content/docs/zh-cn/integrations.mdx @@ -1,6 +1,6 @@ --- title: 集成 -description: 为 Pi、OMP、Claude Code、Codex、GitHub Copilot CLI、Devin CLI、Droid、Kimi Code CLI、OpenCode、Kilo Code CLI、Hermes Agent、Qoder CLI、Cursor Agent CLI、MastraCode、Antigravity CLI 和 Grok CLI 安装 Herdr 集成。 +description: 为 Pi、OMP、Claude Code、Codex、GitHub Copilot CLI、Devin CLI、Droid、Kimi Code CLI、OpenCode、Kilo Code CLI、Hermes Agent、Qoder CLI、Qwen Code、Cursor Agent CLI、MastraCode、Antigravity CLI 和 Grok CLI 安装 Herdr 集成。 --- Herdr 自动检测受支持的智能体。官方集成可以额外提供用于恢复的原生会话身份、生命周期状态上报,或两者兼有。 @@ -25,6 +25,7 @@ herdr integration install kilo herdr integration install hermes herdr integration install mastracode herdr integration install qodercli +herdr integration install qwen herdr integration install cursor herdr integration install antigravity-cli herdr integration install grok @@ -46,6 +47,7 @@ herdr integration uninstall kilo herdr integration uninstall hermes herdr integration uninstall mastracode herdr integration uninstall qodercli +herdr integration uninstall qwen herdr integration uninstall cursor herdr integration uninstall antigravity-cli herdr integration uninstall grok @@ -58,7 +60,7 @@ Herdr 以两种不同方式使用集成: | 集成类型 | 智能体 | 效果 | | --- | --- | --- | | 生命周期权威 | Pi、OMP、Kimi Code CLI、OpenCode、Kilo Code CLI、MastraCode | 已安装且在为该窗格主动上报时,由钩子或插件事件决定 `idle`、`working` 和 `blocked`。对同一个生命周期权威,Herdr 不再使用屏幕清单兜底。 | -| 会话身份 | Claude Code、Codex、GitHub Copilot CLI、Devin CLI、Droid、Qoder CLI、Cursor Agent CLI、Hermes Agent、Antigravity CLI、Grok CLI | 集成上报用于恢复的原生会话引用。状态仍来自 Herdr 的屏幕清单检测。 | +| 会话身份 | Claude Code、Codex、GitHub Copilot CLI、Devin CLI、Droid、Qoder CLI、Qwen Code、Cursor Agent CLI、Hermes Agent、Antigravity CLI、Grok CLI | 集成上报用于恢复的原生会话引用。状态仍来自 Herdr 的屏幕清单检测。 | 自定义集成在定义了原生终端 UI 中不可见的状态时,也可以上报状态。它们不需要内置到 Herdr 中,也不要求 Herdr 识别智能体的可执行文件。 @@ -89,9 +91,9 @@ Herdr 以两种不同方式使用集成: [Prime Agent 内置的 Herdr 上报器](https://github.com/PrimeIntellect-ai/prime-agent/blob/main/packages/coding-agent/src/core/extensions/builtin/herdr-agent-state.ts)是一个真实实现示例。它只在 Herdr 中启用,将智能体事件映射为 `working`、`idle` 和 `blocked`,跨会话保持上报顺序,并在退出时释放权威。 -一些集成会上报智能体的原生会话引用。除非被 `[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 的窗格。 +一些集成会上报智能体的原生会话引用。除非被 `[session] resume_agents_on_restore = false` 禁用,Herdr 会在服务器重启后使用官方会话引用恢复 Claude Code、Codex、Devin CLI、Droid、Kimi Code CLI、Qoder CLI、Qwen Code、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`、Qwen Code 版本 `1`、Cursor Agent CLI 版本 `1`、Grok CLI 版本 `1`、OpenCode 版本 `5`、Kilo Code CLI 版本 `1`、Hermes Agent 版本 `5`、MastraCode 版本 `1`、Antigravity CLI 版本 `1`。用 `herdr integration status` 查看已安装版本。 ## Pi @@ -259,6 +261,20 @@ Herdr 用 `qodercli --resume ` 恢复保存的 Qoder CLI 会话。 钩子未安装时,屏幕清单检测仍然可用。 +## Qwen Code + +安装 Qwen Code 钩子: + +```bash +herdr integration install qwen +``` + +`SessionStart` 钩子只上报用于原生恢复的会话身份。生命周期状态仍由 Herdr 的屏幕清单检测决定。 + +Herdr 默认使用 `~/.qwen`,设置了 `QWEN_HOME` 时使用后者。Qwen 配置目录必须已经存在。安装会写入 `hooks/herdr-agent-session.sh`(Windows 上为 `hooks/herdr-agent-session.ps1`),并向 `settings.json` 添加 Herdr 条目。卸载只移除匹配条目和托管脚本。 + +Herdr 用 `qwen --resume ` 恢复保存的 Qwen Code 会话。 + ## Cursor Agent CLI 安装 Cursor Agent CLI 钩子: diff --git a/docs/next/website/src/content/docs/zh-cn/session-state.mdx b/docs/next/website/src/content/docs/zh-cn/session-state.mdx index 1c4663ae2d..0a0936adf4 100644 --- a/docs/next/website/src/content/docs/zh-cn/session-state.mdx +++ b/docs/next/website/src/content/docs/zh-cn/session-state.mdx @@ -76,6 +76,7 @@ Herdr 只会恢复那些通过当前官方 Herdr 集成上报了原生会话引 | Droid | `2` | `droid --resume ` | | Kimi Code CLI | `3` | `kimi --session ` | | Qoder CLI | `2` | `qodercli --resume ` | +| Qwen Code | `1` | `qwen --resume ` | | OpenCode | `5` | `opencode --session ` | | Kilo Code CLI | `1` | `kilo --session ` | | Hermes Agent | `2` | `hermes --resume ` | diff --git a/docs/next/website/src/data/config-reference.json b/docs/next/website/src/data/config-reference.json index bcc3f02ee0..231f7521e8 100644 --- a/docs/next/website/src/data/config-reference.json +++ b/docs/next/website/src/data/config-reference.json @@ -1099,6 +1099,17 @@ "off" ] }, + { + "key": "ui.sound.agents.qwen", + "type": "enum", + "default": "\"default\"", + "description": "Sound override for detected Qwen Code agents.", + "values": [ + "default", + "on", + "off" + ] + }, { "key": "ui.sound.agents.maki", "type": "enum", @@ -1192,7 +1203,7 @@ "key": "experimental.cjk_ime_agents", "type": "list of strings", "default": "[]", - "description": "Restrict `reveal_hidden_cursor_for_cjk_ime` to focused panes whose detected agent matches one of these names (case-insensitive). Empty list means apply to any focused pane. Unknown agent names are ignored; if the list contains no valid names, the reveal does not apply. Accepted names: pi, claude, codex, gemini, cursor, devin, cline, opencode, copilot, kimi, kiro, droid, amp, grok, hermes, kilo, qodercli, qoder, maki." + "description": "Restrict `reveal_hidden_cursor_for_cjk_ime` to focused panes whose detected agent matches one of these names (case-insensitive). Empty list means apply to any focused pane. Unknown agent names are ignored; if the list contains no valid names, the reveal does not apply. Accepted names: pi, claude, codex, gemini, cursor, devin, cline, opencode, copilot, kimi, kiro, droid, amp, grok, hermes, kilo, qodercli, qoder, qwen, qwen-code, maki." }, { "key": "experimental.cjk_ime_cursor_shape", diff --git a/src/agent_resume.rs b/src/agent_resume.rs index e2fc47e9b7..cd1ae7bb2f 100644 --- a/src/agent_resume.rs +++ b/src/agent_resume.rs @@ -71,9 +71,10 @@ pub fn session_ref_from_report( pub fn normalize_session_start_source(value: Option) -> Option { match value.as_deref().map(str::trim) { - Some(source @ ("startup" | "resume" | "clear" | "compact" | "new" | "fork" | "select")) => { - Some(source.to_string()) - } + Some( + source @ ("startup" | "resume" | "clear" | "compact" | "branch" | "new" | "fork" + | "select"), + ) => Some(source.to_string()), _ => None, } } @@ -87,6 +88,7 @@ pub fn is_reserved_native_state_source(source: &str, agent: &str) -> bool { | ("herdr:devin", "devin") | ("herdr:droid", "droid") | ("herdr:qodercli", "qodercli") + | ("herdr:qwen", "qwen") | ("herdr:cursor", "cursor") | ("herdr:grok", "grok") ) @@ -177,6 +179,9 @@ pub fn plan(source: &str, agent: &str, session_ref: &AgentSessionRef) -> Option< session_ref.value.clone(), ] } + ("herdr:qwen", "qwen", AgentSessionRefKind::Id) => { + vec!["qwen".into(), "--resume".into(), session_ref.value.clone()] + } ("herdr:kilo", "kilo", AgentSessionRefKind::Id) => { vec!["kilo".into(), "--session".into(), session_ref.value.clone()] } @@ -234,6 +239,7 @@ pub(crate) fn is_official_agent_source(source: &str, agent: &str) -> bool { | ("herdr:hermes", "hermes") | ("herdr:opencode", "opencode") | ("herdr:qodercli", "qodercli") + | ("herdr:qwen", "qwen") | ("herdr:kilo", "kilo") | ("herdr:cursor", "cursor") | ("herdr:antigravity_cli", "agy") @@ -400,6 +406,16 @@ mod tests { .argv, vec!["qodercli", "--resume", "qoder-session"] ); + assert_eq!( + plan( + "herdr:qwen", + "qwen", + &AgentSessionRef::id("qwen-session").unwrap() + ) + .unwrap() + .argv, + vec!["qwen", "--resume", "qwen-session"] + ); assert_eq!( plan( "herdr:kilo", @@ -575,6 +591,11 @@ mod tests { assert_eq!(session_ref.kind, AgentSessionRefKind::Id); assert_eq!(session_ref.value, "qoder-id"); + let session_ref = + session_ref_from_report("herdr:qwen", "qwen", Some("qwen-id".into()), None).unwrap(); + assert_eq!(session_ref.kind, AgentSessionRefKind::Id); + assert_eq!(session_ref.value, "qwen-id"); + let session_ref = session_ref_from_report("herdr:antigravity_cli", "agy", Some("agy-id".into()), None) .unwrap(); @@ -600,6 +621,10 @@ mod tests { normalize_session_start_source(Some("compact".into())), Some("compact".into()) ); + assert_eq!( + normalize_session_start_source(Some("branch".into())), + Some("branch".into()) + ); assert_eq!( normalize_session_start_source(Some("new".into())), Some("new".into()) diff --git a/src/api/schema/integrations.rs b/src/api/schema/integrations.rs index ef55f8e295..6151dedbcb 100644 --- a/src/api/schema/integrations.rs +++ b/src/api/schema/integrations.rs @@ -25,6 +25,7 @@ pub enum IntegrationTarget { Kilo, Hermes, Qodercli, + Qwen, Cursor, Mastracode, AntigravityCli, @@ -32,7 +33,7 @@ pub enum IntegrationTarget { } impl IntegrationTarget { - pub(crate) const ALL: [Self; 16] = [ + pub(crate) const ALL: [Self; 17] = [ Self::Pi, Self::Omp, Self::Claude, @@ -45,6 +46,7 @@ impl IntegrationTarget { Self::Kilo, Self::Hermes, Self::Qodercli, + Self::Qwen, Self::Cursor, Self::Mastracode, Self::AntigravityCli, diff --git a/src/cli/integration.rs b/src/cli/integration.rs index 27620590a6..d0a53f7b53 100644 --- a/src/cli/integration.rs +++ b/src/cli/integration.rs @@ -110,13 +110,13 @@ fn parse_integration_target( ) -> std::io::Result> { let Some(target) = args.first().map(|arg| arg.as_str()) else { eprintln!( - "usage: herdr integration {action} " + "usage: herdr integration {action} " ); return Ok(None); }; if args.len() != 1 { eprintln!( - "usage: herdr integration {action} " + "usage: herdr integration {action} " ); return Ok(None); } @@ -134,6 +134,7 @@ fn parse_integration_target( "kilo" => IntegrationTarget::Kilo, "hermes" => IntegrationTarget::Hermes, "qodercli" => IntegrationTarget::Qodercli, + "qwen" => IntegrationTarget::Qwen, "cursor" => IntegrationTarget::Cursor, "mastracode" => IntegrationTarget::Mastracode, "antigravity-cli" | "antigravity_cli" => IntegrationTarget::AntigravityCli, @@ -141,7 +142,7 @@ fn parse_integration_target( _ => { eprintln!("unknown integration target: {target}"); eprintln!( - "currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, cursor, mastracode, antigravity-cli, grok" + "currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, qwen, cursor, mastracode, antigravity-cli, grok" ); return Ok(None); } @@ -164,6 +165,7 @@ fn print_integration_help() { eprintln!(" herdr integration install kilo"); eprintln!(" herdr integration install hermes"); eprintln!(" herdr integration install qodercli"); + eprintln!(" herdr integration install qwen"); eprintln!(" herdr integration install cursor"); eprintln!(" herdr integration install mastracode"); eprintln!(" herdr integration install antigravity-cli"); @@ -180,6 +182,7 @@ fn print_integration_help() { eprintln!(" herdr integration uninstall kilo"); eprintln!(" herdr integration uninstall hermes"); eprintln!(" herdr integration uninstall qodercli"); + eprintln!(" herdr integration uninstall qwen"); eprintln!(" herdr integration uninstall cursor"); eprintln!(" herdr integration uninstall mastracode"); eprintln!(" herdr integration uninstall antigravity-cli"); diff --git a/src/config/model.rs b/src/config/model.rs index 9b5904fbb4..6d6083ad77 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -989,7 +989,7 @@ pub struct ExperimentalConfig { /// if the list contains no valid names, the reveal does not apply. /// Accepted names: pi, claude, codex, gemini, cursor, devin, cline, /// opencode, copilot, kimi, kiro, droid, amp, grok, hermes, kilo, - /// qodercli, qoder, maki. + /// qodercli, qoder, qwen, qwen-code, maki. /// Default: empty. pub cjk_ime_agents: Vec, /// Cursor shape rendered for the IME anchor when diff --git a/src/config/sidebar.rs b/src/config/sidebar.rs index f7201134f9..c548c839b2 100644 --- a/src/config/sidebar.rs +++ b/src/config/sidebar.rs @@ -621,6 +621,7 @@ rows = [[{ token = "git_status", fg = "#ff00aa" }], [{ token = "$jj", bold = tru Agent::Hermes, Agent::Kilo, Agent::Qodercli, + Agent::Qwen, Agent::Maki, ]; let entries = agents diff --git a/src/config/sound.rs b/src/config/sound.rs index 217737d850..c84f0d59c9 100644 --- a/src/config/sound.rs +++ b/src/config/sound.rs @@ -43,6 +43,7 @@ pub struct AgentSoundOverrides { pub hermes: AgentSoundSetting, pub kilo: AgentSoundSetting, pub qodercli: AgentSoundSetting, + pub qwen: AgentSoundSetting, pub maki: AgentSoundSetting, } @@ -139,6 +140,7 @@ impl AgentSoundOverrides { Some(Agent::Hermes) => self.hermes, Some(Agent::Kilo) => self.kilo, Some(Agent::Qodercli) => self.qodercli, + Some(Agent::Qwen) => self.qwen, Some(Agent::Maki) => self.maki, None => AgentSoundSetting::Default, } @@ -178,6 +180,7 @@ impl Default for AgentSoundOverrides { hermes: AgentSoundSetting::Default, kilo: AgentSoundSetting::Default, qodercli: AgentSoundSetting::Default, + qwen: AgentSoundSetting::Default, maki: AgentSoundSetting::Default, } } diff --git a/src/detect/manifest.rs b/src/detect/manifest.rs index ed3de50787..b9fe786a08 100644 --- a/src/detect/manifest.rs +++ b/src/detect/manifest.rs @@ -255,6 +255,7 @@ const BUNDLED_MANIFESTS: &[(&str, &str)] = &[ ("opencode", include_str!("manifests/opencode.toml")), ("pi", include_str!("manifests/pi.toml")), ("qodercli", include_str!("manifests/qodercli.toml")), + ("qwen", include_str!("manifests/qwen.toml")), ("copilot", include_str!("manifests/github-copilot.toml")), ]; diff --git a/src/detect/manifests/qwen.toml b/src/detect/manifests/qwen.toml new file mode 100644 index 0000000000..52ab242a4d --- /dev/null +++ b/src/detect/manifests/qwen.toml @@ -0,0 +1,89 @@ +id = "qwen" +version = "2026.08.12.1" +min_engine_version = 2 +updated_at = "2026-08-12T00:00:00Z" +aliases = ["qwen-code", "qwen code"] + +# Qwen Code keeps its composer visible while responding, so the prompt box is +# not idle evidence by itself. Active turns show an "esc to cancel" status +# line; confirmation flows replace that line with a stable waiting phrase. +# OSC 9;4 is supplemental because Qwen emits it only while a tool is executing +# and only in terminals that advertise progress support. + +[[rules]] +id = "waiting_for_confirmation" +state = "blocked" +priority = 1000 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["waiting for user confirmation"] + +[[rules]] +id = "tool_confirmation" +state = "blocked" +priority = 990 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["yes, allow once"] +any = [ + { contains = ["apply this change?"] }, + { contains = ["allow execution of:"] }, + { contains = ["allow execution of mcp tool"] }, + { contains = ["do you want to proceed?"] }, + { contains = ["shell command execution"] }, +] + +[[rules]] +id = "question_dialog" +state = "blocked" +priority = 980 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["enter: select", "esc: cancel"] +line_regex = ['^\s*[❯›]\s*1\.\s+'] + +[[rules]] +id = "folder_trust_dialog" +state = "blocked" +priority = 970 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["do you trust this folder?", "trust folder (", "don't trust (esc)"] + +[[rules]] +id = "cancel_hint_working" +state = "working" +priority = 900 +region = "bottom_non_empty_lines(8)" +visible_working = true +line_regex = ['^\s*(?:[⠁-⣿]|\.{1,2})\s+.*\(\d+(?:m(?:\s+\d+s)?|s).*\s·\sesc to cancel\)\s*$'] + +[[rules]] +id = "narrow_cancel_hint_working" +state = "working" +priority = 890 +region = "bottom_non_empty_lines(8)" +visible_working = true +line_regex = ['(?i)^\s*\(esc to cancel\)\s*$'] + +[[rules]] +id = "osc_tool_progress_working" +state = "working" +priority = 850 +region = "osc_progress" +visible_working = true +regex = ['^4;3(?:;|$)'] + +[[rules]] +id = "composer_idle" +state = "idle" +priority = 100 +region = "bottom_non_empty_lines(30)" +visible_idle = true +line_regex = ['^\s*>\s*(?:type\s*)?.*$'] +any = [ + { contains = ["type your message"] }, + { contains = ["your message or @path/to/file"] }, + { contains = ["@path/to/file"] }, + { contains = ["type", "mes", "sage", "@pat", "h/to", "/fil"] }, +] diff --git a/src/detect/mod.rs b/src/detect/mod.rs index b9f9655f5a..f78567f6ac 100644 --- a/src/detect/mod.rs +++ b/src/detect/mod.rs @@ -61,11 +61,12 @@ pub enum Agent { Hermes, Kilo, Qodercli, + Qwen, Maki, } impl Agent { - pub const ALL: [Self; 21] = [ + pub const ALL: [Self; 22] = [ Self::Pi, Self::Claude, Self::Codex, @@ -86,10 +87,11 @@ impl Agent { Self::Hermes, Self::Kilo, Self::Qodercli, + Self::Qwen, Self::Maki, ]; - pub const SCREEN_MANIFEST_AGENTS: [Self; 19] = [ + pub const SCREEN_MANIFEST_AGENTS: [Self; 20] = [ Self::Pi, Self::Claude, Self::Codex, @@ -108,6 +110,7 @@ impl Agent { Self::Hermes, Self::Kilo, Self::Qodercli, + Self::Qwen, Self::Maki, ]; } @@ -134,6 +137,7 @@ pub fn agent_label(agent: Agent) -> &'static str { Agent::Hermes => "hermes", Agent::Kilo => "kilo", Agent::Qodercli => "qodercli", + Agent::Qwen => "qwen", Agent::Maki => "maki", } } @@ -166,6 +170,7 @@ pub fn interactive_agent_executable(agent: Agent) -> &'static str { Agent::Hermes => "hermes", Agent::Kilo => "kilo", Agent::Qodercli => "qodercli", + Agent::Qwen => "qwen", Agent::Maki => "maki", } } @@ -202,6 +207,7 @@ fn lookup_agent(name: &str) -> Option { "hermes" | "hermes-agent" => Some(Agent::Hermes), "kilo" | "kilo-code" | "kilo code" => Some(Agent::Kilo), "qodercli" | "qoderclicn" | "qoder" | "qodercn" => Some(Agent::Qodercli), + "qwen" | "qwen-code" | "qwen code" => Some(Agent::Qwen), "maki" => Some(Agent::Maki), _ => None, } @@ -301,7 +307,7 @@ pub(crate) fn full_lifecycle_hook_authority(source: &str, agent_label: &str) -> pub(crate) fn session_identity_only_integration(source: &str, agent_label: &str) -> bool { matches!( (source, agent_label), - ("herdr:hermes", "hermes") | ("herdr:antigravity_cli", "agy") + ("herdr:hermes", "hermes") | ("herdr:qwen", "qwen") | ("herdr:antigravity_cli", "agy") ) } @@ -345,6 +351,19 @@ fn normalized_process_name(process: &crate::platform::ForegroundProcess) -> Stri return effective.to_string(); } + if let Some(runtime) = process.argv.as_deref().and_then(|argv| argv.first()) { + let runtime_name = normalized_agent_lookup_name(path_basename(runtime)); + if matches!(runtime_name.as_str(), "node" | "bun") { + if let Some(wrapped_agent) = + wrapped_agent_name_from_runtime_argv(runtime, process.argv.as_deref()) + { + if identify_agent(&wrapped_agent) == Some(Agent::Qwen) { + return wrapped_agent; + } + } + } + } + if let Some(wrapped_agent) = argv0_agent_name(process.argv.as_deref()) .or_else(|| cmdline_argv0_agent_name(process.cmdline.as_deref().unwrap_or_default())) { @@ -550,6 +569,9 @@ fn agent_name_from_known_package_path(path: &str) -> Option { { return Some(agent_label(Agent::Pi).to_string()); } + if window == ["node_modules", "@qwen-code", "qwen-code", "dist", "index"] { + return Some(agent_label(Agent::Qwen).to_string()); + } } for window in components.windows(4) { if window == ["node_modules", "mastracode", "dist", "cli"] { @@ -709,6 +731,8 @@ mod tests { assert_eq!(identify_agent("hermes-agent"), Some(Agent::Hermes)); assert_eq!(identify_agent("kilo"), Some(Agent::Kilo)); assert_eq!(identify_agent("kilo-code"), Some(Agent::Kilo)); + assert_eq!(identify_agent("qwen"), Some(Agent::Qwen)); + assert_eq!(identify_agent("Qwen Code"), Some(Agent::Qwen)); assert_eq!(identify_agent("maki"), Some(Agent::Maki)); } @@ -734,6 +758,7 @@ mod tests { assert_eq!(parse_agent_label("kiro-cli"), Some(Agent::Kiro)); assert_eq!(parse_agent_label("grok-build"), Some(Agent::Grok)); assert_eq!(parse_agent_label("hermes-agent"), Some(Agent::Hermes)); + assert_eq!(parse_agent_label("qwen-code"), Some(Agent::Qwen)); assert_eq!(parse_agent_label("maki"), Some(Agent::Maki)); assert_eq!(parse_agent_label("kilo-code"), Some(Agent::Kilo)); } @@ -777,6 +802,7 @@ mod tests { (Agent::Hermes, "hermes"), (Agent::Kilo, "kilo"), (Agent::Qodercli, "qodercli"), + (Agent::Qwen, "qwen"), (Agent::Maki, "maki"), ]; assert_eq!(expected.len(), Agent::ALL.len()); @@ -806,6 +832,7 @@ mod tests { fn session_identity_integrations_leave_state_to_screen_detection() { for (source, label, agent) in [ ("herdr:hermes", "hermes", Agent::Hermes), + ("herdr:qwen", "qwen", Agent::Qwen), ("herdr:antigravity_cli", "agy", Agent::Antigravity), ] { assert!(!full_lifecycle_hook_authority(source, label)); @@ -846,6 +873,27 @@ mod tests { ); } + #[test] + fn identify_agent_in_job_detects_node_wrapped_qwen() { + for argv in [ + vec!["node", "/home/user/.fnm/bin/qwen"], + vec![ + "node.exe", + r"C:\Users\user\AppData\Roaming\npm\node_modules\@qwen-code\qwen-code\dist\index.js", + ], + ] { + let job = crate::platform::ForegroundJob { + process_group_id: 123, + processes: vec![foreground_process(123, "MainThread", &argv)], + }; + + assert_eq!( + identify_agent_in_job(&job), + Some((Agent::Qwen, "qwen".to_string())) + ); + } + } + #[test] fn identify_agent_in_job_prefers_recognized_process_group_leader() { let job = crate::platform::ForegroundJob { diff --git a/src/integration/actions.rs b/src/integration/actions.rs index 00bf8c8f4d..24d1f9e37c 100644 --- a/src/integration/actions.rs +++ b/src/integration/actions.rs @@ -4,11 +4,11 @@ use super::registry::{integration_target_label, integration_target_supported}; use super::targets::{ install_antigravity_cli, install_claude, install_codex, install_copilot, install_cursor, install_devin, install_droid, install_grok, install_hermes, install_kilo, install_kimi, - install_mastracode, install_omp, install_opencode, install_pi, install_qodercli, + install_mastracode, install_omp, install_opencode, install_pi, install_qodercli, install_qwen, uninstall_antigravity_cli, uninstall_claude, uninstall_codex, uninstall_copilot, uninstall_cursor, uninstall_devin, uninstall_droid, uninstall_grok, uninstall_hermes, uninstall_kilo, uninstall_kimi, uninstall_mastracode, uninstall_omp, uninstall_opencode, - uninstall_pi, uninstall_qodercli, + uninstall_pi, uninstall_qodercli, uninstall_qwen, }; use super::version::{agent_version_requirement, enforce_agent_version}; use super::{KIMI_MIN_VERSION, PI_EXTENSION_INSTALL_NAME}; @@ -192,6 +192,19 @@ fn install_target_inner(target: crate::api::schema::IntegrationTarget) -> io::Re ), ] } + crate::api::schema::IntegrationTarget::Qwen => { + let installed = install_qwen()?; + vec![ + format!( + "installed qwen integration hook to {}", + installed.hook_path.display() + ), + format!( + "ensured qwen settings at {}", + installed.settings_path.display() + ), + ] + } crate::api::schema::IntegrationTarget::Cursor => { let installed = install_cursor()?; vec![ @@ -559,6 +572,33 @@ pub(crate) fn uninstall_target( } messages } + crate::api::schema::IntegrationTarget::Qwen => { + let result = uninstall_qwen()?; + let mut messages = Vec::new(); + if result.removed_hook_file { + messages.push(format!( + "removed qwen hook at {}", + result.hook_path.display() + )); + } else { + messages.push(format!( + "no qwen hook found at {}", + result.hook_path.display() + )); + } + if result.updated_settings { + messages.push(format!( + "removed herdr qwen hook entries from {}", + result.settings_path.display() + )); + } else { + messages.push(format!( + "no herdr qwen hook entries found in {}", + result.settings_path.display() + )); + } + messages + } crate::api::schema::IntegrationTarget::Cursor => { let result = uninstall_cursor()?; let mut messages = Vec::new(); diff --git a/src/integration/assets/qwen/herdr-agent-session.ps1 b/src/integration/assets/qwen/herdr-agent-session.ps1 new file mode 100644 index 0000000000..882caf8616 --- /dev/null +++ b/src/integration/assets/qwen/herdr-agent-session.ps1 @@ -0,0 +1,37 @@ +# installed by herdr +# managed by herdr; reinstalling or updating the integration overwrites this file. +# add custom hooks beside this file instead of editing it. +# HERDR_INTEGRATION_ID=qwen +# HERDR_INTEGRATION_VERSION=1 + +param([string]$Action = "") + +if ($Action -ne "session") { exit 0 } +if ($env:HERDR_ENV -ne "1") { exit 0 } +if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { exit 0 } +if ([string]::IsNullOrWhiteSpace($env:HERDR_SOCKET_PATH)) { exit 0 } + +$inputText = [Console]::In.ReadToEnd() +try { + $payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json } +} catch { + $payload = $null +} + +if ($null -eq $payload -or [string]::IsNullOrWhiteSpace($payload.session_id)) { exit 0 } + +$seq = [DateTime]::UtcNow.Ticks +$herdr = if ([string]::IsNullOrWhiteSpace($env:HERDR_BIN_PATH)) { "herdr" } else { $env:HERDR_BIN_PATH } +$commandArgs = @( + "pane", "report-agent-session", $env:HERDR_PANE_ID, + "--source", "herdr:qwen", "--agent", "qwen", + "--agent-session-id", [string]$payload.session_id, + "--seq", [string]$seq +) +if ($payload.source -in @("startup", "resume", "clear", "compact", "branch")) { + $commandArgs += @("--session-start-source", [string]$payload.source) +} +try { + & $herdr @commandArgs 2>$null | Out-Null +} catch { +} diff --git a/src/integration/assets/qwen/herdr-agent-session.sh b/src/integration/assets/qwen/herdr-agent-session.sh new file mode 100644 index 0000000000..226c2a97eb --- /dev/null +++ b/src/integration/assets/qwen/herdr-agent-session.sh @@ -0,0 +1,48 @@ +#!/bin/sh +# managed by herdr; reinstalling the integration replaces this file. +# HERDR_INTEGRATION_ID=qwen +# HERDR_INTEGRATION_VERSION=1 + +[ "${1:-}" = "session" ] || exit 0 +[ "${HERDR_ENV:-}" = "1" ] || exit 0 +[ -n "${HERDR_PANE_ID:-}" ] || exit 0 +[ -n "${HERDR_SOCKET_PATH:-}" ] || exit 0 +if [ -n "${HERDR_BIN_PATH:-}" ]; then + [ -x "$HERDR_BIN_PATH" ] || exit 0 +else + command -v herdr >/dev/null 2>&1 || exit 0 +fi +command -v python3 >/dev/null 2>&1 || exit 0 + +python3 -c ' +import json +import os +import subprocess +import sys +import time + +try: + payload = json.load(sys.stdin) + session_id = payload.get("session_id") + source = payload.get("source") + if not isinstance(session_id, str) or not session_id: + raise ValueError + command = os.environ.get("HERDR_BIN_PATH") or "herdr" + args = [ + command, "pane", "report-agent-session", os.environ["HERDR_PANE_ID"], + "--source", "herdr:qwen", "--agent", "qwen", + "--agent-session-id", session_id, "--seq", str(time.time_ns()), + ] + if source in ("startup", "resume", "clear", "compact", "branch"): + args.extend(["--session-start-source", source]) + subprocess.run( + args, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=1, + check=False, + ) +except Exception: + pass +' 2>/dev/null || true diff --git a/src/integration/env.rs b/src/integration/env.rs index e86f6ee88e..8f3a9e655d 100644 --- a/src/integration/env.rs +++ b/src/integration/env.rs @@ -16,6 +16,7 @@ pub(crate) const CODEX_HOME_ENV_VAR: &str = "CODEX_HOME"; pub(crate) const KIMI_CODE_HOME_ENV_VAR: &str = "KIMI_CODE_HOME"; pub(crate) const COPILOT_HOME_ENV_VAR: &str = "COPILOT_HOME"; pub(crate) const QODERCLI_CONFIG_DIR_ENV_VAR: &str = "QODER_CONFIG_DIR"; +pub(crate) const QWEN_HOME_ENV_VAR: &str = "QWEN_HOME"; pub(crate) const CURSOR_CONFIG_DIR_ENV_VAR: &str = "CURSOR_CONFIG_DIR"; pub(crate) const ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR: &str = "ANTIGRAVITY_CLI_CONFIG_DIR"; pub(crate) const GROK_CONFIG_DIR_ENV_VAR: &str = "GROK_CONFIG_DIR"; @@ -157,6 +158,10 @@ pub(crate) fn qodercli_dir() -> io::Result { config_dir_from_env_or_home(QODERCLI_CONFIG_DIR_ENV_VAR, &[".qoder"]) } +pub(crate) fn qwen_dir() -> io::Result { + config_dir_from_env_or_home(QWEN_HOME_ENV_VAR, &[".qwen"]) +} + pub(crate) fn cursor_dir() -> io::Result { config_dir_from_env_or_home(CURSOR_CONFIG_DIR_ENV_VAR, &[".cursor"]) } diff --git a/src/integration/mod.rs b/src/integration/mod.rs index 597aabc5f8..c87ccac6cc 100644 --- a/src/integration/mod.rs +++ b/src/integration/mod.rs @@ -192,6 +192,18 @@ const QODERCLI_HOOK_ASSET: &str = if cfg!(windows) { }; const QODERCLI_INTEGRATION_VERSION: u32 = 3; const QODERCLI_HOOK_EVENTS: [(&str, &str); 1] = [("SessionStart", "session")]; +const QWEN_HOOK_INSTALL_NAME: &str = if cfg!(windows) { + "herdr-agent-session.ps1" +} else { + "herdr-agent-session.sh" +}; +const QWEN_HOOK_ASSET: &str = if cfg!(windows) { + include_str!("assets/qwen/herdr-agent-session.ps1") +} else { + include_str!("assets/qwen/herdr-agent-session.sh") +}; +const QWEN_INTEGRATION_VERSION: u32 = 1; +const QWEN_HOOK_EVENTS: [(&str, &str); 1] = [("SessionStart", "session")]; const QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS: [(&str, &str); 12] = [ ("SessionStart", "idle"), ("UserPromptSubmit", "working"), diff --git a/src/integration/registry.rs b/src/integration/registry.rs index b7bb3bf200..4625197059 100644 --- a/src/integration/registry.rs +++ b/src/integration/registry.rs @@ -20,6 +20,7 @@ pub(crate) fn integration_target_label( crate::api::schema::IntegrationTarget::Kilo => "kilo", crate::api::schema::IntegrationTarget::Hermes => "hermes", crate::api::schema::IntegrationTarget::Qodercli => "qodercli", + crate::api::schema::IntegrationTarget::Qwen => "qwen", crate::api::schema::IntegrationTarget::Cursor => "cursor", crate::api::schema::IntegrationTarget::Mastracode => "mastracode", crate::api::schema::IntegrationTarget::AntigravityCli => "antigravity-cli", @@ -49,6 +50,7 @@ pub(crate) fn integration_target_command_names( crate::api::schema::IntegrationTarget::Kilo => &["kilo", "kilo-code"], crate::api::schema::IntegrationTarget::Hermes => &["hermes"], crate::api::schema::IntegrationTarget::Qodercli => qodercli_command_names(), + crate::api::schema::IntegrationTarget::Qwen => &["qwen"], crate::api::schema::IntegrationTarget::Cursor => cursor_command_names(), crate::api::schema::IntegrationTarget::Mastracode => &["mastracode"], crate::api::schema::IntegrationTarget::AntigravityCli => &["agy"], @@ -75,6 +77,7 @@ pub(crate) fn integration_target_supported(target: crate::api::schema::Integrati | crate::api::schema::IntegrationTarget::Droid | crate::api::schema::IntegrationTarget::Kimi | crate::api::schema::IntegrationTarget::Qodercli + | crate::api::schema::IntegrationTarget::Qwen | crate::api::schema::IntegrationTarget::AntigravityCli | crate::api::schema::IntegrationTarget::Devin | crate::api::schema::IntegrationTarget::Hermes @@ -264,7 +267,7 @@ fn integration_specs() -> [( crate::api::schema::IntegrationTarget, io::Result, u32, -); 16] { +); 17] { [ ( crate::api::schema::IntegrationTarget::Pi, @@ -329,6 +332,11 @@ fn integration_specs() -> [( qodercli_dir().map(|dir| dir.join("hooks").join(super::QODERCLI_HOOK_INSTALL_NAME)), super::QODERCLI_INTEGRATION_VERSION, ), + ( + crate::api::schema::IntegrationTarget::Qwen, + qwen_dir().map(|dir| dir.join("hooks").join(super::QWEN_HOOK_INSTALL_NAME)), + super::QWEN_INTEGRATION_VERSION, + ), ( crate::api::schema::IntegrationTarget::Cursor, cursor_dir().map(|dir| dir.join(super::CURSOR_HOOK_INSTALL_NAME)), diff --git a/src/integration/targets.rs b/src/integration/targets.rs index 375d27139e..0150ccaa83 100644 --- a/src/integration/targets.rs +++ b/src/integration/targets.rs @@ -20,7 +20,7 @@ use super::config_edit::{ use super::env::{ antigravity_cli_dir, claude_dir, codex_dir, copilot_dir, cursor_dir, devin_dir, droid_dir, grok_dir, hermes_dir, hermes_plugin_dir, kilo_dir, kimi_dir, mastracode_dir, omp_extension_dir, - opencode_dir, pi_extension_dir, qodercli_dir, + opencode_dir, pi_extension_dir, qodercli_dir, qwen_dir, }; use super::file_ops::{ make_executable, remove_dir_all_if_exists, remove_file_if_exists, remove_legacy_bash_hook_file, @@ -37,6 +37,7 @@ use super::types::{ KiloUninstallResult, KimiInstallPaths, KimiUninstallResult, MastracodeInstallPaths, MastracodeUninstallResult, OmpInstallPaths, OmpUninstallResult, OpenCodeInstallPaths, OpenCodeUninstallResult, PiUninstallResult, QodercliInstallPaths, QodercliUninstallResult, + QwenInstallPaths, QwenUninstallResult, }; use super::{ ANTIGRAVITY_CLI_HOOK_ASSET, ANTIGRAVITY_CLI_HOOK_BLOCK_NAME, ANTIGRAVITY_CLI_HOOK_EVENTS, @@ -55,6 +56,7 @@ use super::{ OPENCODE_PLUGIN_INSTALL_NAME, OPENCODE_TUI_PLUGIN_ASSET, OPENCODE_TUI_PLUGIN_INSTALL_NAME, OPENCODE_TUI_PLUGIN_SPEC, PI_EXTENSION_ASSET, PI_EXTENSION_INSTALL_NAME, QODERCLI_HOOK_ASSET, QODERCLI_HOOK_EVENTS, QODERCLI_HOOK_INSTALL_NAME, QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS, + QWEN_HOOK_ASSET, QWEN_HOOK_EVENTS, QWEN_HOOK_INSTALL_NAME, }; fn ensure_extension_dir(dir: &Path, agent: &str) -> io::Result<()> { @@ -945,6 +947,59 @@ pub(crate) fn install_qodercli() -> io::Result { }) } +pub(crate) fn install_qwen() -> io::Result { + let dir = qwen_dir()?; + if !dir.is_dir() { + return Err(io::Error::other(format!( + "qwen code config directory not found at {}. install qwen code first", + dir.display() + ))); + } + + let hooks_dir = dir.join("hooks"); + fs::create_dir_all(&hooks_dir)?; + + let hook_path = hooks_dir.join(QWEN_HOOK_INSTALL_NAME); + fs::write(&hook_path, QWEN_HOOK_ASSET)?; + make_executable(&hook_path)?; + + let settings_path = dir.join("settings.json"); + let mut settings = if settings_path.is_file() { + serde_json::from_str::(&fs::read_to_string(&settings_path)?).map_err(|err| { + io::Error::other(format!( + "failed to parse {}: {err}", + settings_path.display() + )) + })? + } else { + json!({}) + }; + + let hooks = ensure_hooks_object( + &mut settings, + &settings_path, + "qwen settings", + "qwen settings hooks", + )?; + for (event, action) in QWEN_HOOK_EVENTS { + remove_hook_commands(hooks, event, &hook_path, Some(action))?; + ensure_command_hook( + hooks, + event, + hook_command(&hook_path, Some(action)), + 10_000, + Some("*"), + )?; + } + + fs::write(&settings_path, serde_json::to_string_pretty(&settings)?)?; + + Ok(QwenInstallPaths { + hook_path, + settings_path, + }) +} + pub(crate) fn install_cursor() -> io::Result { let dir = cursor_dir()?; if !dir.is_dir() { @@ -1047,6 +1102,46 @@ pub(crate) fn uninstall_qodercli() -> io::Result { }) } +pub(crate) fn uninstall_qwen() -> io::Result { + let hook_path = qwen_dir()?.join("hooks").join(QWEN_HOOK_INSTALL_NAME); + let settings_path = qwen_dir()?.join("settings.json"); + let mut updated_settings = false; + + if settings_path.is_file() { + let mut settings = serde_json::from_str::(&fs::read_to_string(&settings_path)?) + .map_err(|err| { + io::Error::other(format!( + "failed to parse {}: {err}", + settings_path.display() + )) + })?; + + if let Some(hooks) = hooks_object_if_present( + &mut settings, + &settings_path, + "qwen settings", + "qwen settings hooks", + )? { + for (event, action) in QWEN_HOOK_EVENTS { + updated_settings |= remove_hook_commands(hooks, event, &hook_path, Some(action))?; + } + } + + if updated_settings { + fs::write(&settings_path, serde_json::to_string_pretty(&settings)?)?; + } + } + + let removed_hook_file = remove_file_if_exists(&hook_path)?; + + Ok(QwenUninstallResult { + hook_path, + settings_path, + removed_hook_file, + updated_settings, + }) +} + pub(crate) fn uninstall_cursor() -> io::Result { let cursor_home = cursor_dir()?; let hook_path = cursor_home.join(CURSOR_HOOK_INSTALL_NAME); diff --git a/src/integration/tests.rs b/src/integration/tests.rs index 324ae6ce94..d54183f295 100644 --- a/src/integration/tests.rs +++ b/src/integration/tests.rs @@ -101,6 +101,7 @@ fn clear_integration_path_env() { std::env::remove_var(KIMI_CODE_HOME_ENV_VAR); std::env::remove_var("XDG_CONFIG_HOME"); std::env::remove_var(QODERCLI_CONFIG_DIR_ENV_VAR); + std::env::remove_var(QWEN_HOME_ENV_VAR); std::env::remove_var(CURSOR_CONFIG_DIR_ENV_VAR); std::env::remove_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR); std::env::remove_var(GROK_CONFIG_DIR_ENV_VAR); @@ -195,6 +196,7 @@ fn windows_supports_portable_integrations() { assert!(integration_target_supported(IntegrationTarget::Droid)); assert!(integration_target_supported(IntegrationTarget::Kimi)); assert!(integration_target_supported(IntegrationTarget::Qodercli)); + assert!(integration_target_supported(IntegrationTarget::Qwen)); } #[cfg(windows)] @@ -3186,6 +3188,108 @@ fn install_qodercli_errors_when_config_dir_missing() { let _ = fs::remove_dir_all(base); } +#[test] +fn install_qwen_writes_session_hook_and_preserves_settings() { + let _lock = integration_env_lock(); + let base = unique_base(); + let qwen_dir = base.join(".qwen"); + fs::create_dir_all(&qwen_dir).unwrap(); + fs::write( + qwen_dir.join("settings.json"), + r#"{"permissions":{"allow":["Read"]},"hooks":{}}"#, + ) + .unwrap(); + std::env::set_var(QWEN_HOME_ENV_VAR, &qwen_dir); + + let installed = install_qwen().unwrap(); + + assert_eq!( + installed.hook_path, + qwen_dir.join("hooks").join(QWEN_HOOK_INSTALL_NAME) + ); + assert_eq!(installed.settings_path, qwen_dir.join("settings.json")); + assert!(installed.hook_path.is_file()); + + let settings: Value = + serde_json::from_str(&fs::read_to_string(&installed.settings_path).unwrap()).unwrap(); + let entries = settings["hooks"]["SessionStart"].as_array().unwrap(); + assert_eq!(entries.len(), 1); + assert_eq!(entries[0]["matcher"], "*"); + assert_eq!(entries[0]["hooks"][0]["timeout"], 10_000); + let command = entries[0]["hooks"][0]["command"].as_str().unwrap(); + assert!(command.contains(QWEN_HOOK_INSTALL_NAME)); + assert!(command.ends_with("session")); + assert!(settings.get("permissions").is_some()); + let hook_asset = fs::read_to_string(&installed.hook_path).unwrap(); + assert!(hook_asset.contains("HERDR_INTEGRATION_ID=qwen")); + assert!(hook_asset.contains("HERDR_INTEGRATION_VERSION=1")); + assert!(hook_asset.contains("herdr:qwen")); + + install_qwen().unwrap(); + let settings: Value = + serde_json::from_str(&fs::read_to_string(&installed.settings_path).unwrap()).unwrap(); + assert_eq!( + settings["hooks"]["SessionStart"].as_array().unwrap().len(), + 1 + ); + + std::env::remove_var(QWEN_HOME_ENV_VAR); + let _ = fs::remove_dir_all(base); +} + +#[test] +fn uninstall_qwen_removes_only_herdr_hook() { + let _lock = integration_env_lock(); + let base = unique_base(); + let qwen_dir = base.join(".qwen"); + fs::create_dir_all(&qwen_dir).unwrap(); + std::env::set_var(QWEN_HOME_ENV_VAR, &qwen_dir); + + install_qwen().unwrap(); + let settings_path = qwen_dir.join("settings.json"); + let mut settings: Value = + serde_json::from_str(&fs::read_to_string(&settings_path).unwrap()).unwrap(); + settings["hooks"]["SessionStart"] + .as_array_mut() + .unwrap() + .push(json!({ + "matcher": "resume", + "hooks": [{"type": "command", "command": "echo user-defined"}], + })); + fs::write( + &settings_path, + serde_json::to_string_pretty(&settings).unwrap(), + ) + .unwrap(); + + let result = uninstall_qwen().unwrap(); + assert!(result.removed_hook_file); + assert!(result.updated_settings); + + let settings: Value = + serde_json::from_str(&fs::read_to_string(&settings_path).unwrap()).unwrap(); + let remaining = settings["hooks"]["SessionStart"].as_array().unwrap(); + assert_eq!(remaining.len(), 1); + assert_eq!(remaining[0]["hooks"][0]["command"], "echo user-defined"); + + std::env::remove_var(QWEN_HOME_ENV_VAR); + let _ = fs::remove_dir_all(base); +} + +#[test] +fn install_qwen_errors_when_config_dir_missing() { + let _lock = integration_env_lock(); + let base = unique_base(); + let missing = base.join(".qwen"); + std::env::set_var(QWEN_HOME_ENV_VAR, &missing); + + let err = install_qwen().unwrap_err().to_string(); + assert!(err.contains("qwen code config directory not found")); + + std::env::remove_var(QWEN_HOME_ENV_VAR); + let _ = fs::remove_dir_all(base); +} + #[test] fn install_cursor_writes_hook_and_updates_hooks_json() { let _lock = integration_env_lock(); diff --git a/src/integration/types.rs b/src/integration/types.rs index c5c2426746..c7eaf795e6 100644 --- a/src/integration/types.rs +++ b/src/integration/types.rs @@ -69,6 +69,12 @@ pub(crate) struct QodercliInstallPaths { pub settings_path: PathBuf, } +#[derive(Debug)] +pub(crate) struct QwenInstallPaths { + pub hook_path: PathBuf, + pub settings_path: PathBuf, +} + #[derive(Debug)] pub(crate) struct CursorInstallPaths { pub hook_path: PathBuf, @@ -119,6 +125,14 @@ pub(crate) struct QodercliUninstallResult { pub updated_settings: bool, } +#[derive(Debug)] +pub(crate) struct QwenUninstallResult { + pub hook_path: PathBuf, + pub settings_path: PathBuf, + pub removed_hook_file: bool, + pub updated_settings: bool, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct IntegrationStatus { pub target: crate::api::schema::IntegrationTarget, diff --git a/src/main.rs b/src/main.rs index 4d915158d3..d3b7219256 100644 --- a/src/main.rs +++ b/src/main.rs @@ -450,7 +450,8 @@ pane_history = false # matches one of these names. Empty means apply to any focused pane. # If the list contains no valid names, the reveal does not apply. # Accepted: pi, claude, codex, gemini, cursor, devin, cline, opencode, -# copilot, kimi, kiro, droid, amp, grok, hermes, kilo, qodercli, qoder. +# copilot, kimi, kiro, droid, amp, grok, hermes, kilo, qodercli, qoder, qwen, +# qwen-code, maki. # cjk_ime_agents = [] # Cursor shape rendered when reveal_hidden_cursor_for_cjk_ime is true. # Values: block, steady_block (default), underline, steady_underline, bar, steady_bar. diff --git a/src/terminal/state.rs b/src/terminal/state.rs index 4b1fdaef4a..5c659eb9f4 100644 --- a/src/terminal/state.rs +++ b/src/terminal/state.rs @@ -1334,6 +1334,11 @@ impl TerminalState { "omp", Some("startup" | "new" | "resume" | "fork") ) + | ( + "herdr:qwen", + "qwen", + Some("startup" | "clear" | "resume" | "compact" | "branch") + ) | ("herdr:antigravity_cli", "agy", None) ) } @@ -4551,6 +4556,72 @@ mod tests { } } + #[test] + fn qwen_lifecycle_session_ref_replaces_existing_session_ref() { + for session_start_source in ["startup", "clear", "resume", "compact", "branch"] { + let mut terminal = test_terminal(); + terminal.set_detected_state(Some(Agent::Qwen), AgentState::Idle); + terminal + .set_agent_session_ref( + "herdr:qwen".into(), + "qwen".into(), + crate::agent_resume::AgentSessionRef::id("qwen-session"), + Some(20), + ) + .expect("initial session should be accepted"); + + let next_session = format!("qwen-{session_start_source}-session"); + let mutation = terminal + .set_agent_session_ref_for_session_start( + "herdr:qwen".into(), + "qwen".into(), + crate::agent_resume::AgentSessionRef::id(&next_session), + Some(21), + Some(session_start_source.into()), + ) + .unwrap_or_else(|| panic!("{session_start_source} should replace the session")); + + assert!(mutation.session_ref_changed); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| session.session_ref.value.as_str()), + Some(next_session.as_str()) + ); + } + } + + #[test] + fn qwen_session_ref_does_not_replace_without_foreground_qwen() { + let mut terminal = test_terminal(); + terminal + .set_agent_session_ref( + "herdr:qwen".into(), + "qwen".into(), + crate::agent_resume::AgentSessionRef::id("qwen-parent"), + Some(20), + ) + .expect("initial session should be accepted"); + + let mutation = terminal.set_agent_session_ref_for_session_start( + "herdr:qwen".into(), + "qwen".into(), + crate::agent_resume::AgentSessionRef::id("qwen-branch"), + Some(21), + Some("branch".into()), + ); + + assert!(mutation.is_none()); + assert_eq!( + terminal + .persisted_agent_session + .as_ref() + .map(|session| session.session_ref.value.as_str()), + Some("qwen-parent") + ); + } + #[test] fn opencode_server_new_does_not_replace_existing_session_ref() { let mut terminal = test_terminal(); diff --git a/website/agent-detection/index.toml b/website/agent-detection/index.toml index fba996ea39..f44a83065a 100644 --- a/website/agent-detection/index.toml +++ b/website/agent-detection/index.toml @@ -72,6 +72,10 @@ path = "pi.toml" id = "qodercli" path = "qodercli.toml" +[[agents]] +id = "qwen" +path = "qwen.toml" + [[agents]] id = "copilot" path = "github-copilot.toml" diff --git a/website/agent-detection/qwen.toml b/website/agent-detection/qwen.toml new file mode 100644 index 0000000000..52ab242a4d --- /dev/null +++ b/website/agent-detection/qwen.toml @@ -0,0 +1,89 @@ +id = "qwen" +version = "2026.08.12.1" +min_engine_version = 2 +updated_at = "2026-08-12T00:00:00Z" +aliases = ["qwen-code", "qwen code"] + +# Qwen Code keeps its composer visible while responding, so the prompt box is +# not idle evidence by itself. Active turns show an "esc to cancel" status +# line; confirmation flows replace that line with a stable waiting phrase. +# OSC 9;4 is supplemental because Qwen emits it only while a tool is executing +# and only in terminals that advertise progress support. + +[[rules]] +id = "waiting_for_confirmation" +state = "blocked" +priority = 1000 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["waiting for user confirmation"] + +[[rules]] +id = "tool_confirmation" +state = "blocked" +priority = 990 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["yes, allow once"] +any = [ + { contains = ["apply this change?"] }, + { contains = ["allow execution of:"] }, + { contains = ["allow execution of mcp tool"] }, + { contains = ["do you want to proceed?"] }, + { contains = ["shell command execution"] }, +] + +[[rules]] +id = "question_dialog" +state = "blocked" +priority = 980 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["enter: select", "esc: cancel"] +line_regex = ['^\s*[❯›]\s*1\.\s+'] + +[[rules]] +id = "folder_trust_dialog" +state = "blocked" +priority = 970 +region = "bottom_non_empty_lines(20)" +visible_blocker = true +contains = ["do you trust this folder?", "trust folder (", "don't trust (esc)"] + +[[rules]] +id = "cancel_hint_working" +state = "working" +priority = 900 +region = "bottom_non_empty_lines(8)" +visible_working = true +line_regex = ['^\s*(?:[⠁-⣿]|\.{1,2})\s+.*\(\d+(?:m(?:\s+\d+s)?|s).*\s·\sesc to cancel\)\s*$'] + +[[rules]] +id = "narrow_cancel_hint_working" +state = "working" +priority = 890 +region = "bottom_non_empty_lines(8)" +visible_working = true +line_regex = ['(?i)^\s*\(esc to cancel\)\s*$'] + +[[rules]] +id = "osc_tool_progress_working" +state = "working" +priority = 850 +region = "osc_progress" +visible_working = true +regex = ['^4;3(?:;|$)'] + +[[rules]] +id = "composer_idle" +state = "idle" +priority = 100 +region = "bottom_non_empty_lines(30)" +visible_idle = true +line_regex = ['^\s*>\s*(?:type\s*)?.*$'] +any = [ + { contains = ["type your message"] }, + { contains = ["your message or @path/to/file"] }, + { contains = ["@path/to/file"] }, + { contains = ["type", "mes", "sage", "@pat", "h/to", "/fil"] }, +]