diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 2c232b46..36b011f5 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -48,7 +48,7 @@ Electron main process
- `src/main/services/agent-runtime/` is a separate lifecycle boundary and is not controlled by the Browser access switch. When CanvasTTY status hooks are enabled, every agent PTY receives a distinct capability for a protected user-local socket/pipe. Provider command hooks and the OpenCode event plugin may report only the fixed status enum, bounded event name, and optional opaque turn/prompt ID; prompt text, responses, tool input, and arbitrary telemetry are rejected by the exact gateway schema. Electron helper commands carry `ELECTRON_RUN_AS_NODE=1` inside the exact hook command only; the provider PTY never inherits that process-mode flag, so a provider cannot accidentally launch a second CanvasTTY GUI instance. The user can revoke this capability from Agents settings, immediately returning live agent status to `unavailable`; re-enabling requires a new/restarted PTY. Explicitly trusted plugin hooks use a separate process runner which re-checks the private PluginManager registry on every invocation and strips CanvasTTY internal capabilities before passing the provider payload to third-party code. Provider-native review remains an independent gate; CanvasTTY does not bypass Codex hook trust globally.
- Lifecycle adapters use launch-only settings for Claude, Codex, Qwen, and OpenCode. Kimi, Hermes, and Grok, whose hook discovery is home-config based, receive ownership-checked temporary entries shared across live CanvasTTY sessions. Kimi and Hermes keep recovery journals and exact backups; Grok uses a dedicated owned hook file. Cleanup restores exact original bytes when no concurrent edit occurred and otherwise removes only CanvasTTY-owned entries.
- `TerminalManager` injects the MCP helper per launch without leaving permanent provider configuration. Claude Code, Codex, and Qwen Code receive CLI arguments; Qwen gets one inline `--mcp-config` entry that overrides only the CanvasTTY server name and leaves unrelated user servers available. OpenCode receives a merged launch-only `OPENCODE_CONFIG_CONTENT` entry plus one scoped browser-tool permission; Kimi uses its per-run MCP configuration when supported. Older Kimi versions receive a compare-and-swap temporary CanvasTTY entry and one exact permission rule with an atomic recovery journal. Hermes receives a temporary `mcp_servers.canvastty_browser` entry in `HERMES_HOME/config.yaml` (defaulting to `~/.hermes/config.yaml` on POSIX or `%LOCALAPPDATA%\hermes\config.yaml` on Windows); sensitive capability values stay as child-environment placeholders. Temporary Kimi and Hermes configuration remains until the final owning PTY session ends, then exact original bytes are restored when safe. A journal repairs an interrupted Hermes launch at the next CanvasTTY startup, while compare-and-swap checks preserve concurrent user edits. Unrelated MCP entries, credentials, and file/shell permissions are preserved. Qwen, OpenCode, and Hermes YOLO remain launch-only and do not change persistent permission settings.
-- `src/main/services/providerCliRegistry.ts` is the single owner of provider CLI discovery. During main-process startup it creates a shared snapshot for Codex, Claude, Qwen Code, Kimi, OpenCode, Hermes, Grok Build, OMP, and Pi by checking smoke-only overrides, the inherited `PATH`, platform defaults, and known per-user/provider directories in that order. Available entries retain an absolute executable, launcher kind, and supplemented child `PATH`; POSIX entries must be executable files and Windows entries must be supported native or batch launchers. `TerminalManager`, `LimitsService`, agent-browser probes, and provider smoke tests consume that same snapshot and never repeat command lookup. Missing entries produce a failed session with copyable checked-path diagnostics before PTY or temporary browser configuration creation, while the limit adapter reports `cli-not-found`; its HOME row is hidden until explicitly selected after CLI detection. CanvasTTY never reads shell startup scripts. Agents settings can recheck candidate paths, atomically replace the registry snapshot, reconcile saved launcher and limit selections, and refresh CLI-bound adapters without restarting the app. Existing sessions keep running; a newly found CLI remains disabled until selected.
+- `src/main/services/providerCliRegistry.ts` is the single owner of provider CLI discovery. During main-process startup it creates a shared snapshot for every provider in `PROVIDER_CLI_DEFINITIONS` — each definition declares the executable command names it may install as (which may differ from the provider ID, e.g. a provider shipping as `mcode`) and optional known home-relative or Windows LOCALAPPDATA-relative directories — by checking smoke-only overrides, the inherited `PATH`, platform defaults, and those known directories in that order. Available entries retain an absolute executable, launcher kind, and supplemented child `PATH`; POSIX entries must be executable files and Windows entries must be supported native or batch launchers. `TerminalManager`, `LimitsService`, agent-browser probes, and provider smoke tests consume that same snapshot and never repeat command lookup. Missing entries produce a failed session with copyable checked-path diagnostics before PTY or temporary browser configuration creation, while the limit adapter reports `cli-not-found`; its HOME row is hidden until explicitly selected after CLI detection. Launchers keep a provider without a local CLI visible when an account bound to a remote computer or a saved container profile provides another route. CanvasTTY never reads shell startup scripts. Agents settings can recheck candidate paths, atomically replace the registry snapshot, reconcile saved launcher and limit selections, and refresh CLI-bound adapters without restarting the app. Existing sessions keep running; a newly found CLI remains disabled until selected.
The primary `BrowserWindow` is created and shown with a lightweight local startup page before settings, plugins, media, and IPC services initialize. Successful initialization replaces that page with the trusted renderer; bootstrap failures replace it with a visible error page and retain a native-dialog fallback. The main process holds Electron's single-instance lock; a rejected second launch raises the running window through the `second-instance` handler so the app never appears to ignore a launch, while background plugin and browser requests never restore, show, or focus an existing window. Native browser contents are focused programmatically only while their owner `BrowserWindow` is already focused; explicit user pointer input remains the only cross-surface focus route.
diff --git a/docs/ARCHITECTURE.ru.md b/docs/ARCHITECTURE.ru.md
index 7f1e2c56..06c9c768 100644
--- a/docs/ARCHITECTURE.ru.md
+++ b/docs/ARCHITECTURE.ru.md
@@ -39,7 +39,7 @@ Electron main process
- `src/main/services/agent-runtime/` — отдельная всегда включённая lifecycle-граница, не зависящая от переключателя Browser access. Каждый agent PTY получает собственный capability для защищённого user-local socket/pipe. Provider command hooks и OpenCode event plugin могут передать только фиксированный status enum, ограниченное имя события и необязательный opaque turn/prompt ID; точная schema Gateway отклоняет prompt text, ответы, tool input и произвольную telemetry. При завершении PTY capability и временные файлы отзываются.
- Claude, Codex, Qwen и OpenCode получают lifecycle hooks только на текущий запуск. Для Kimi, Hermes и Grok, которые ищут hooks в home-конфигурации, используются ownership-checked временные записи с совместным владением живых сессий. Kimi и Hermes используют recovery journals и точные backups, Grok — отдельный owned hook file; cleanup восстанавливает исходные байты или удаляет только записи CanvasTTY при конкурентных изменениях.
- `TerminalManager` подмешивает MCP helper, не оставляя постоянных изменений в provider-конфигах. Claude Code, Codex и Qwen Code получают CLI arguments; Qwen получает одну inline-запись `--mcp-config`, которая переопределяет только имя сервера CanvasTTY и не скрывает сторонние user servers. OpenCode — объединённый launch-only `OPENCODE_CONFIG_CONTENT` с одной scoped browser-tool permission, Kimi — per-run MCP config или временную запись с compare-and-swap и recovery journal для старых версий. Hermes получает временную запись `mcp_servers.canvastty_browser` в `HERMES_HOME/config.yaml` (по умолчанию `~/.hermes/config.yaml` в POSIX или `%LOCALAPPDATA%\hermes\config.yaml` в Windows); чувствительные capability-значения остаются ссылками на окружение дочернего процесса. Временная конфигурация Kimi и Hermes живёт до завершения последней владеющей PTY-сессии, после чего исходные байты точно восстанавливаются, если файл не менялся параллельно. Journal восстанавливает Hermes после прерванного запуска при следующем старте CanvasTTY, а compare-and-swap сохраняет одновременные пользовательские изменения. Сторонние MCP-записи, credentials и file/shell permissions не затрагиваются. Qwen, OpenCode и Hermes YOLO остаются launch-only и не меняют постоянные permission-настройки.
-- `src/main/services/providerCliRegistry.ts` — единственный владелец обнаружения provider CLI. При запуске main-процесса он создаёт общий snapshot для Codex, Claude, Qwen Code, Kimi, OpenCode, Hermes, Grok Build, OMP и Pi, последовательно проверяя smoke-only overrides, унаследованный `PATH`, системные каталоги платформы и известные пользовательские/provider-каталоги. Доступная запись хранит абсолютный executable, тип launcher-а и дополненный дочерний `PATH`; POSIX-кандидат обязан быть исполняемым файлом, а Windows-кандидат — поддерживаемым native или batch launcher-ом. `TerminalManager`, `LimitsService`, agent-browser probes и provider smoke используют один и тот же snapshot и не повторяют поиск команды. Недоступный CLI создаёт failed-сессию с копируемой диагностикой проверенных путей до создания PTY или временной browser-конфигурации, а адаптер лимитов сообщает `cli-not-found`; строка HOME скрыта до ручного выбора после обнаружения CLI. CanvasTTY не читает shell startup scripts. Настройки агентов позволяют повторно проверить пути, атомарно заменить snapshot registry, согласовать сохранённые списки запуска и лимитов и обновить зависящие от CLI адаптеры без перезапуска. Работающие сессии продолжаются; найденный позже CLI остаётся выключенным до ручного выбора.
+- `src/main/services/providerCliRegistry.ts` — единственный владелец обнаружения provider CLI. При запуске main-процесса он создаёт общий snapshot для каждого провайдера из `PROVIDER_CLI_DEFINITIONS` — каждое определение объявляет имена executable-команд, под которыми провайдер может устанавливаться (они могут отличаться от ID провайдера, например провайдер с командой `mcode`), и опциональные известные каталоги (относительно home или Windows LOCALAPPDATA), — последовательно проверяя smoke-only overrides, унаследованный `PATH`, системные каталоги платформы и эти известные каталоги. Доступная запись хранит абсолютный executable, тип launcher-а и дополненный дочерний `PATH`; POSIX-кандидат обязан быть исполняемым файлом, а Windows-кандидат — поддерживаемым native или batch launcher-ом. `TerminalManager`, `LimitsService`, agent-browser probes и provider smoke используют один и тот же snapshot и не повторяют поиск команды. Недоступный CLI создаёт failed-сессию с копируемой диагностикой проверенных путей до создания PTY или временной browser-конфигурации, а адаптер лимитов сообщает `cli-not-found`; строка HOME скрыта до ручного выбора после обнаружения CLI. Провайдер без локального CLI остаётся в панелях запуска, если для него есть аккаунт на удалённом компьютере или сохранённый контейнерный профиль. CanvasTTY не читает shell startup scripts. Настройки агентов позволяют повторно проверить пути, атомарно заменить snapshot registry, согласовать сохранённые списки запуска и лимитов и обновить зависящие от CLI адаптеры без перезапуска. Работающие сессии продолжаются; найденный позже CLI остаётся выключенным до ручного выбора.
Основной `BrowserWindow` создаётся и показывается с лёгкой локальной стартовой страницей до инициализации settings, plugins, media и IPC. Успешная инициализация заменяет её доверенным renderer; bootstrap failure показывает видимую error page и сохраняет fallback на native dialog. Main process удерживает single-instance lock и восстанавливает/фокусирует существующее окно при повторном запуске.
diff --git a/docs/ARCHITECTURE.zh-CN.md b/docs/ARCHITECTURE.zh-CN.md
index 1cf3fc8c..cfae79fb 100644
--- a/docs/ARCHITECTURE.zh-CN.md
+++ b/docs/ARCHITECTURE.zh-CN.md
@@ -39,7 +39,7 @@ Electron main process
- `src/main/services/agent-runtime/` 是独立且始终启用的 lifecycle 边界,不受 Browser access 开关控制。每个 agent PTY 都为受保护的 user-local socket/pipe 获得独立 capability。Provider command hook 与 OpenCode event plugin 只能提交固定 status enum、受限 event 名称和可选 opaque turn/prompt ID;Gateway 的精确 schema 会拒绝 prompt text、回复、tool input 与任意 telemetry。PTY 退出时 capability 与临时文件都会被撤销。
- Claude、Codex、Qwen 与 OpenCode 使用仅本次启动有效的 lifecycle hook。Kimi、Hermes 与 Grok 只能从 home 配置发现 hook,因此使用由实时 CanvasTTY 会话共享、带 ownership 检查的临时条目。Kimi 与 Hermes 使用 recovery journal 和精确 backup;Grok 使用独立 owned hook 文件。Cleanup 在无并发编辑时逐字恢复原文件,否则只移除 CanvasTTY 自己的条目。
- `TerminalManager` 注入 MCP helper 时不会留下永久的服务商配置变更。Claude Code、Codex 与 Qwen Code 使用 CLI 参数;Qwen 使用一个 inline `--mcp-config`,只覆盖 CanvasTTY 服务名,不隐藏无关用户服务。OpenCode 使用合并后的、仅本次启动有效的 `OPENCODE_CONFIG_CONTENT` 和一条 scoped browser-tool 权限;Kimi 使用 per-run MCP 配置,旧版本则使用带 compare-and-swap 与 recovery journal 的临时配置。Hermes 会在 `HERMES_HOME/config.yaml` 中获得临时 `mcp_servers.canvastty_browser` 配置项(POSIX 默认路径为 `~/.hermes/config.yaml`,Windows 默认路径为 `%LOCALAPPDATA%\hermes\config.yaml`),敏感 capability 值仍以子进程环境变量占位符保存。Kimi 与 Hermes 的临时配置会保留到最后一个所属 PTY 会话结束;若文件未被并发修改,则精确恢复原始字节。若 Hermes 启动意外中断,journal 会在 CanvasTTY 下次启动时修复配置,compare-and-swap 则保留用户的并发修改。其他 MCP 配置项、凭据和文件/shell 权限不会受影响。Qwen、OpenCode 与 Hermes 的 YOLO 都不修改持久权限设置。
-- `src/main/services/providerCliRegistry.ts` 是服务商 CLI 发现的唯一职责边界。main 进程启动时,它按 smoke-only override、继承的 `PATH`、平台默认目录、已知用户/服务商目录的顺序,为 Codex、Claude、Qwen Code、Kimi、OpenCode、Hermes、Grok Build、OMP 与 Pi 创建一个共享快照。可用条目保存绝对 executable、launcher 类型以及补充后的子进程 `PATH`;POSIX 候选必须是可执行文件,Windows 候选必须是受支持的 native 或 batch launcher。`TerminalManager`、`LimitsService`、agent-browser probe 与 provider smoke 共用该快照,不再各自查找命令。CLI 不可用时,系统会在创建 PTY 或临时 browser 配置之前生成 failed session,并提供可复制的已检查路径诊断;限额适配器报告 `cli-not-found`;HOME 行保持隐藏,直到检测到 CLI 后由用户手动选择。CanvasTTY 不读取 shell startup script。Agents 设置可重新检查候选路径、原子替换 registry 快照、调整已保存的启动器和限额选择,并在无需重启的情况下刷新依赖 CLI 的适配器。运行中的 session 保持不变;新检测到的 CLI 需手动启用。
+- `src/main/services/providerCliRegistry.ts` 是服务商 CLI 发现的唯一职责边界。main 进程启动时,它按 smoke-only override、继承的 `PATH`、平台默认目录、已知用户/服务商目录的顺序,为 `PROVIDER_CLI_DEFINITIONS` 中的每个服务商创建一个共享快照——每个定义声明该服务商可能安装的 executable 命令名(可以与服务商 ID 不同,例如命令为 `mcode` 的服务商),以及可选的已知目录(相对 home 或 Windows LOCALAPPDATA)。可用条目保存绝对 executable、launcher 类型以及补充后的子进程 `PATH`;POSIX 候选必须是可执行文件,Windows 候选必须是受支持的 native 或 batch launcher。`TerminalManager`、`LimitsService`、agent-browser probe 与 provider smoke 共用该快照,不再各自查找命令。CLI 不可用时,系统会在创建 PTY 或临时 browser 配置之前生成 failed session,并提供可复制的已检查路径诊断;限额适配器报告 `cli-not-found`;HOME 行保持隐藏,直到检测到 CLI 后由用户手动选择。若某服务商绑定了远程计算机上的账户或已保存的容器配置,即使本地没有 CLI,启动器仍会显示它。CanvasTTY 不读取 shell startup script。Agents 设置可重新检查候选路径、原子替换 registry 快照、调整已保存的启动器和限额选择,并在无需重启的情况下刷新依赖 CLI 的适配器。运行中的 session 保持不变;新检测到的 CLI 需手动启用。
主 `BrowserWindow` 在 settings、plugins、media 和 IPC 服务初始化之前创建并显示轻量本地启动页。初始化成功后替换为可信 renderer;bootstrap 失败后替换为可见错误页,并保留原生对话框 fallback。主进程持有 Electron single-instance lock;再次启动时恢复并聚焦已有窗口。
diff --git a/docs/adr/ADR-20260921-provider-cli-command-definitions.md b/docs/adr/ADR-20260921-provider-cli-command-definitions.md
new file mode 100644
index 00000000..38e18900
--- /dev/null
+++ b/docs/adr/ADR-20260921-provider-cli-command-definitions.md
@@ -0,0 +1,81 @@
+# ADR: Declarative Provider CLI Command Definitions
+
+**Date:** 2026-09-21
+**Scope / Component:** provider CLI discovery (`providerCliRegistry.ts`)
+**Risk/Strictness Profile:** Production
+**Status:** Proposed
+
+**Implementation:** [`providerCliRegistry.ts`](../../src/main/services/providerCliRegistry.ts)
+
+## Context and Problem Statement
+
+Provider resolution historically derived every candidate path from the provider ID itself:
+`codex` → `
/codex`, `qwen` → `/qwen`. Per-provider knowledge lived in an
+`if (provider === …)` chain inside `knownProviderDirectories` (OpenCode, Kimi, Grok home
+directories, the Codex Windows LOCALAPPDATA path). That coupling is already false for incoming
+providers: MiniMax Code installs as `mcode`, Cursor as `agent`, Google Antigravity as `agy`.
+Without a change, each such provider would grow a new special case in the resolution loop, and
+`executable === provider` would remain a hidden invariant no type enforces.
+
+## Decision Drivers
+
+- Adding a provider whose executable differs from its ID must not require changes to the
+ resolution algorithm, only data.
+- Existing providers must keep resolving to byte-identical executables, candidate orders, and
+ child `PATH` values.
+- The registry stays an immutable, startup-once snapshot; nothing here may introduce per-launch
+ lookups.
+- Definitions are trusted, in-repo configuration: structural mistakes (duplicate provider,
+ empty command list) should fail fast and loudly rather than silently resolve nothing.
+
+## Options Considered
+
+### Keep the ID-derived mapping and add per-provider overrides where needed
+
+Each new mismatched provider adds both a `commands` special case and possibly a directory
+special case. Rejected: the special-case count grows with every provider and the invariant
+stays implicit.
+
+### Resolve through the user's shell (`which`/`where`) per launch
+
+Rejected earlier and unchanged: startup-once resolution without shell startup scripts is a
+documented product invariant.
+
+## Decision Outcome
+
+Resolution is driven by `ProviderCliDefinition`:
+
+```ts
+interface ProviderCliDefinition {
+ id: AgentProviderId;
+ commands: readonly string[];
+ knownDirectories?: readonly ProviderCliKnownDirectory[];
+}
+```
+
+`PROVIDER_CLI_DEFINITIONS` is a frozen, exhaustive `Record` — adding a
+provider to the union without a definition is a compile error. Candidate generation walks
+directories in the established order (inherited `PATH`, platform defaults, known provider
+directories, shared user directories) and, within each directory, tries each command in
+declaration order with each platform launcher extension. `knownDirectories` replaces the
+`if`-chain with `home`-relative and Windows `LOCALAPPDATA`-relative specifiers resolved at
+startup. `createProviderCliRegistry` accepts an optional `definitions` override used by tests
+to exercise definitions for providers not yet in the union; production always passes none.
+
+Definitions with an empty `commands` list or duplicate IDs throw at registry creation.
+
+## Consequences
+
+- The executable may legitimately differ from the provider ID; consumers already work from
+ `AvailableProviderCli.executable`, so no downstream change is needed.
+- Command declaration order is a real priority within one directory: the first listed command
+ wins when several are installed in the same directory.
+- Per-provider directory knowledge is now reviewable data instead of control flow; a reviewer
+ can diff provider support without reading the resolution algorithm.
+
+## Invariants
+
+- With default definitions, every pre-existing provider resolves exactly as before this change
+ (same executable, same candidate order, same child `PATH`).
+- A provider with no definition cannot compile into the union; a definition without commands
+ cannot create a registry.
diff --git a/integrations/even-g2/index.html b/integrations/even-g2/index.html
index b0bd250b..9d13f4a3 100644
--- a/integrations/even-g2/index.html
+++ b/integrations/even-g2/index.html
@@ -60,6 +60,10 @@
Терминалы
+
+
+
+
diff --git a/integrations/even-g2/src/create-menu.mjs b/integrations/even-g2/src/create-menu.mjs
index f5a51aef..65062c61 100644
--- a/integrations/even-g2/src/create-menu.mjs
+++ b/integrations/even-g2/src/create-menu.mjs
@@ -1,4 +1,4 @@
-import { CANVAS_LAUNCHER_ITEMS, PROVIDER_LABELS } from "../../../src/shared/contracts.ts";
+import { CANVAS_LAUNCHER_ITEMS, PROVIDER_LABELS } from "../../../src/shared/providerCatalog.ts";
// Codex and Terminal retain their existing direct OS menu actions.
export const MORE_AGENTS = CANVAS_LAUNCHER_ITEMS
diff --git a/src/main/ipc/registerIpc.ts b/src/main/ipc/registerIpc.ts
index c4fe7b3f..f6ac6ead 100644
--- a/src/main/ipc/registerIpc.ts
+++ b/src/main/ipc/registerIpc.ts
@@ -738,7 +738,7 @@ async function pickPluginMediaLibrary(
}
function providerValue(value: unknown): ProviderId {
- if (value === "terminal" || value === "codex" || value === "claude" || value === "qwen" || value === "kimi" || value === "opencode" || value === "hermes" || value === "grok" || value === "omp" || value === "pi") return value;
+ if (value === "terminal" || value === "codex" || value === "claude" || value === "qwen" || value === "kimi" || value === "opencode" || value === "hermes" || value === "grok" || value === "omp" || value === "pi" || value === "cursor" || value === "minimax" || value === "devin" || value === "antigravity") return value;
throw new Error("Plugin requested an unknown launcher provider.");
}
diff --git a/src/main/services/PluginManager.ts b/src/main/services/PluginManager.ts
index 44aa5e24..d0cac463 100644
--- a/src/main/services/PluginManager.ts
+++ b/src/main/services/PluginManager.ts
@@ -73,7 +73,7 @@ const MAX_RUNTIME_HOOK_REGISTRY_BYTES = 1024 * 1024;
const MAX_PLUGIN_ICON_BYTES = 512 * 1024;
const PLUGIN_INPUT_BRIDGE_URL = "canvastty-plugin://host/input-bridge.js";
const AGENT_PROVIDERS = new Set([
- "codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi"
+ "codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin", "antigravity"
]);
const PLUGIN_HOOK_EVENTS = new Set([
"session-start",
diff --git a/src/main/services/SettingsStore.ts b/src/main/services/SettingsStore.ts
index e4b0ded5..5b916de7 100644
--- a/src/main/services/SettingsStore.ts
+++ b/src/main/services/SettingsStore.ts
@@ -63,16 +63,16 @@ const SESSION_ROW_COLOR_MODES = new Set(["monochrome", "sta
const CANVAS_COLORS = new Set(["sage", "lilac", "night", "sand", "mist", "rose", "slate"]);
const PATTERNS = new Set(["dots", "grid", "waves", "diagonal", "rings", "none"]);
const MEDIA_FITS = new Set(["cover", "contain"]);
-const SETTINGS_VERSION = 15;
+const SETTINGS_VERSION = 19;
const GROK_LAUNCHER_SETTINGS_VERSION = 3;
const EXPANDED_LIMIT_SETTINGS_VERSION = 5;
const QWEN_SETTINGS_VERSION = 6;
-const PROVIDER_ADDITIONS_SETTINGS_VERSION = 15;
+const PROVIDER_ADDITIONS_SETTINGS_VERSION = 19;
// Providers appended to the persisted HOME dock for operators whose profile predates them.
-const ADDED_AGENT_PROVIDERS: AgentProviderId[] = ["omp", "pi"];
+const ADDED_AGENT_PROVIDERS: AgentProviderId[] = ["omp", "pi", "cursor", "minimax", "devin", "antigravity"];
const LEGACY_AGENT_PROVIDERS: AgentProviderId[] = ["codex", "claude", "kimi", "opencode", "hermes"];
const PRE_QWEN_AGENT_PROVIDERS: AgentProviderId[] = [...LEGACY_AGENT_PROVIDERS, "grok"];
-const AGENT_PROVIDERS = new Set(["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi"]);
+const AGENT_PROVIDERS = new Set(["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin", "antigravity"]);
const LEGACY_LIMIT_PROVIDERS: LimitProviderId[] = ["codex", "claude", "kimi"];
const PRE_QWEN_LIMIT_PROVIDERS: LimitProviderId[] = [...LEGACY_LIMIT_PROVIDERS, "opencode", "grok"];
const LIMIT_PROVIDERS: LimitProviderId[] = ["codex", "claude", "qwen", "kimi", "opencode", "grok"];
diff --git a/src/main/services/TerminalManager.ts b/src/main/services/TerminalManager.ts
index 8980c848..84385c89 100644
--- a/src/main/services/TerminalManager.ts
+++ b/src/main/services/TerminalManager.ts
@@ -555,7 +555,11 @@ export class TerminalManager {
try {
// omp and pi take no browser bridge, exactly like grok: the adapter chain below
// ends in the Kimi MCP configuration, which would hand them foreign launch flags.
- agentBrowser = provider === "terminal" || provider === "grok" || provider === "omp" || provider === "pi"
+ // cursor stays out too until its CLI grows a measured browser adapter,
+ // and minimax until its MCP configuration is wired (plain PTY for now).
+ // devin is cloud-session oriented and takes no browser adapter yet,
+ // and antigravity keeps plain PTY integration for the same reason.
+ agentBrowser = provider === "terminal" || provider === "grok" || provider === "omp" || provider === "pi" || provider === "cursor" || provider === "minimax" || provider === "devin" || provider === "antigravity"
? null
: this.agentBrowser?.prepareLaunch({ terminalSessionId: id, provider, cwd }) ?? null;
const baseEnvironment = terminalEnvironment();
diff --git a/src/main/services/TerminalSessionStore.ts b/src/main/services/TerminalSessionStore.ts
index 824b1b8c..c338d6e7 100644
--- a/src/main/services/TerminalSessionStore.ts
+++ b/src/main/services/TerminalSessionStore.ts
@@ -20,7 +20,11 @@ const PROVIDERS = new Set([
"hermes",
"grok",
"omp",
- "pi"
+ "pi",
+ "cursor",
+ "minimax",
+ "devin",
+ "antigravity"
]);
export interface PersistedTerminalSession {
diff --git a/src/main/services/agent-runtime/ProviderRuntimeLaunch.ts b/src/main/services/agent-runtime/ProviderRuntimeLaunch.ts
index f9d5aa68..a5f798bd 100644
--- a/src/main/services/agent-runtime/ProviderRuntimeLaunch.ts
+++ b/src/main/services/agent-runtime/ProviderRuntimeLaunch.ts
@@ -84,6 +84,8 @@ export interface PreparedProviderRuntimeLaunch {
releaseConfiguration(): void;
}
+const HOOK_PROVIDERS: ReadonlySet = new Set(["claude", "codex", "qwen", "opencode", "kimi", "hermes", "grok"]);
+
export class ProviderRuntimeLaunchAdapters {
private readonly options: ProviderRuntimeLaunchOptions;
private readonly environment: Readonly>;
@@ -148,9 +150,9 @@ export class ProviderRuntimeLaunchAdapters {
): PreparedProviderRuntimeLaunch {
const pluginRegistrations = this.options.pluginHooks?.list(provider) ?? [];
const pluginCommands = this.pluginHookCommands(provider, pluginRegistrations);
- // omp and pi have no hook adapter. Without this they would fall through to the Grok
- // overlay at the end of this method and write Grok hook configuration for them.
- const hasHooks = provider !== "omp" && provider !== "pi"
+ // Only providers with a hook adapter get lifecycle configuration. Anything else (omp, pi,
+ // cursor, minimax, devin, antigravity) must never reach Grok's shared hook overlay.
+ const hasHooks = HOOK_PROVIDERS.has(provider)
&& (coreHooksEnabled || pluginCommands.length > 0 || (provider === "opencode" && pluginRegistrations.length > 0));
const environment = hasHooks
? {
@@ -201,7 +203,8 @@ export class ProviderRuntimeLaunchAdapters {
if (provider === "hermes") {
return prepared([], environment, this.acquireHermes(coreHooksEnabled, pluginCommands));
}
- return prepared([], environment, this.acquireGrok(coreHooksEnabled, pluginCommands));
+ if (provider === "grok") return prepared([], environment, this.acquireGrok(coreHooksEnabled, pluginCommands));
+ return prepared([], environment);
}
recoverConfigurations(): void {
@@ -468,7 +471,16 @@ const PLUGIN_HOOK_TRIGGERS: Record([
- "codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi"
+ "codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin", "antigravity"
]);
const MAX_RUNTIME_SESSIONS = 32;
diff --git a/src/main/services/providerCliRegistry.ts b/src/main/services/providerCliRegistry.ts
index e4df8619..ffb8fddb 100644
--- a/src/main/services/providerCliRegistry.ts
+++ b/src/main/services/providerCliRegistry.ts
@@ -1,22 +1,67 @@
-import { accessSync, constants, statSync } from "node:fs";
+import { accessSync, constants, realpathSync, statSync } from "node:fs";
import { homedir } from "node:os";
import { posix, win32 } from "node:path";
import type { AgentCliAvailability, AgentProviderId } from "../../shared/contracts.ts";
-export const PROVIDER_CLI_IDS: readonly AgentProviderId[] = Object.freeze([
- "codex",
- "claude",
- "qwen",
- "kimi",
- "opencode",
- "hermes",
- "grok",
- "omp",
- "pi"
-]);
+// A provider ID and the executable names it may install as are independent
+// facts: MiniMax Code ships as `mcode`, Cursor as `agent`. Definitions keep
+// that mapping declarative so new providers never grow resolution special
+// cases.
+export type ProviderCliKnownDirectoryRoot = "home" | "windows-local-appdata";
+
+export interface ProviderCliKnownDirectory {
+ root: ProviderCliKnownDirectoryRoot;
+ segments: readonly string[];
+}
+
+export interface ProviderCliDefinition {
+ id: AgentProviderId;
+ commands: readonly string[];
+ knownDirectories?: readonly ProviderCliKnownDirectory[];
+}
+
+function defineProviderCli(
+ id: AgentProviderId,
+ commands: readonly string[],
+ knownDirectories?: readonly ProviderCliKnownDirectory[]
+): ProviderCliDefinition {
+ return Object.freeze({
+ id,
+ commands: Object.freeze([...commands]),
+ ...(knownDirectories
+ ? { knownDirectories: Object.freeze(knownDirectories.map((directory) => Object.freeze(directory))) }
+ : {})
+ });
+}
+
+export const PROVIDER_CLI_DEFINITIONS: Readonly> = Object.freeze({
+ codex: defineProviderCli("codex", ["codex"], [
+ { root: "windows-local-appdata", segments: ["Programs", "OpenAI", "Codex", "bin"] }
+ ]),
+ claude: defineProviderCli("claude", ["claude"]),
+ qwen: defineProviderCli("qwen", ["qwen"]),
+ kimi: defineProviderCli("kimi", ["kimi"], [{ root: "home", segments: [".kimi-code", "bin"] }]),
+ opencode: defineProviderCli("opencode", ["opencode"], [{ root: "home", segments: [".opencode", "bin"] }]),
+ hermes: defineProviderCli("hermes", ["hermes"]),
+ grok: defineProviderCli("grok", ["grok"], [{ root: "home", segments: [".grok", "bin"] }]),
+ omp: defineProviderCli("omp", ["omp"]),
+ pi: defineProviderCli("pi", ["pi"]),
+ // Prefer the unique spelling across PATH. Generic agent requires a verified
+ // Cursor real path or an explicit user override; Grok also installs agent.
+ cursor: defineProviderCli("cursor", ["cursor-agent", "agent"]),
+ // MiniMax Code (@minimax-ai/code) installs its TUI as `mcode`.
+ minimax: defineProviderCli("minimax", ["mcode"], [{ root: "home", segments: [".minimax-code", "bin"] }]),
+ devin: defineProviderCli("devin", ["devin"]),
+ // Google Antigravity CLI installs as `agy` (Gemini CLI's successor).
+ antigravity: defineProviderCli("antigravity", ["agy"])
+});
+
+export const PROVIDER_CLI_IDS: readonly AgentProviderId[] = Object.freeze(
+ Object.keys(PROVIDER_CLI_DEFINITIONS) as AgentProviderId[]
+);
export type ProviderCliLauncher = "native" | "batch";
-export type ProviderCliRejectionReason = "missing" | "not-file" | "not-executable" | "unsupported-launcher";
+export type ProviderCliRejectionReason = "missing" | "not-file" | "not-executable" | "unsupported-launcher" | "unverified-identity";
export interface ProviderCliCheck {
path: string;
@@ -63,6 +108,7 @@ interface ProviderCliRegistryOptions {
startupDirectory?: string;
overrides?: Partial>;
platformRoot?: string;
+ definitions?: readonly ProviderCliDefinition[];
inspectCandidate?: (path: string, platform: NodeJS.Platform) => ProviderCliRejectionReason | null;
directoryExists?: (path: string) => boolean;
}
@@ -81,31 +127,33 @@ export function createProviderCliRegistry(options: ProviderCliRegistryOptions =
const inputDirectories = pathEntries(environment[pathKey], platform, startupDirectory);
const platformDirectories = defaultPlatformDirectories(platform, options.platformRoot);
const sharedDirectories = sharedUserDirectories(platform, environment, homeDirectory);
+ const definitions = normalizeProviderCliDefinitions(options.definitions);
const resolveAll = (): Readonly> => {
const childDirectories = uniquePaths(
[...inputDirectories, ...platformDirectories, ...sharedDirectories].filter(directoryExists),
platform
);
const childPath = childDirectories.join(platform === "win32" ? ";" : ":");
- const resolutions = Object.fromEntries(PROVIDER_CLI_IDS.map((provider) => {
+ const resolutions = Object.fromEntries(definitions.map((definition) => {
const providerDirectories = uniquePaths([
...inputDirectories,
...platformDirectories,
- ...knownProviderDirectories(provider, platform, environment, homeDirectory),
+ ...knownProviderDirectories(definition, platform, environment, homeDirectory),
...sharedDirectories
], platform);
- return [provider, resolveProviderCli({
- provider,
+ return [definition.id, resolveProviderCli({
+ provider: definition.id,
+ commands: definition.commands,
platform,
environment,
- override: normalizeOverride(options.overrides?.[provider], platform, startupDirectory),
+ override: normalizeOverride(options.overrides?.[definition.id], platform, startupDirectory),
directories: providerDirectories,
pathKey,
childPath,
inspectCandidate
})];
})) as Record;
- for (const provider of PROVIDER_CLI_IDS) Object.freeze(resolutions[provider]);
+ for (const definition of definitions) Object.freeze(resolutions[definition.id]);
return Object.freeze(resolutions);
};
let resolutions = resolveAll();
@@ -125,6 +173,7 @@ export function createProviderCliRegistry(options: ProviderCliRegistryOptions =
interface ResolveProviderCliInput {
provider: AgentProviderId;
+ commands: readonly string[];
platform: NodeJS.Platform;
environment: Readonly;
override?: string;
@@ -136,8 +185,8 @@ interface ResolveProviderCliInput {
function resolveProviderCli(input: ResolveProviderCliInput): ProviderCliResolution {
const candidates = input.override
- ? [input.override, ...providerCandidates(input.provider, input.directories, input.platform)]
- : providerCandidates(input.provider, input.directories, input.platform);
+ ? [input.override, ...providerCandidates(input.commands, input.directories, input.platform, input.provider === "cursor")]
+ : providerCandidates(input.commands, input.directories, input.platform, input.provider === "cursor");
const checked: ProviderCliCheck[] = [];
for (const candidate of uniquePaths(candidates, input.platform)) {
@@ -147,6 +196,10 @@ function resolveProviderCli(input: ResolveProviderCliInput): ProviderCliResoluti
checked.push({ path: absoluteCandidate, result: rejection });
continue;
}
+ if (input.provider === "cursor" && /(?:^|[\\/])agent(?:\.(?:exe|com|cmd|bat))?$/iu.test(absoluteCandidate) && absoluteCandidate !== input.override && !verifiedCursorPath(absoluteCandidate)) {
+ checked.push({ path: absoluteCandidate, result: "unverified-identity" });
+ continue;
+ }
const launcher = launcherKind(absoluteCandidate, input.platform);
if (!launcher) {
checked.push({ path: absoluteCandidate, result: "unsupported-launcher" });
@@ -238,12 +291,14 @@ function escapeCommandPromptArgument(value: string): string {
return `"${escaped}"`.replace(COMMAND_PROMPT_META_CHARACTERS, "^$1");
}
-function providerCandidates(provider: AgentProviderId, directories: string[], platform: NodeJS.Platform): string[] {
+function providerCandidates(commands: readonly string[], directories: string[], platform: NodeJS.Platform, commandFirst = false): string[] {
const path = platform === "win32" ? win32 : posix;
const extensions = platform === "win32"
? [...WINDOWS_NATIVE_EXTENSIONS, ...WINDOWS_BATCH_EXTENSIONS]
: [""];
- return directories.flatMap((directory) => extensions.map((extension) => path.join(directory, `${provider}${extension}`)));
+ const candidate = (directory: string, command: string): string[] => extensions.map((extension) => path.join(directory, `${command}${extension}`));
+ return commandFirst ? commands.flatMap((command) => directories.flatMap((directory) => candidate(directory, command)))
+ : directories.flatMap((directory) => commands.flatMap((command) => candidate(directory, command)));
}
function launcherKind(path: string, platform: NodeJS.Platform): ProviderCliLauncher | null {
@@ -281,23 +336,46 @@ function defaultPlatformDirectories(platform: NodeJS.Platform, platformRoot = "/
}
function knownProviderDirectories(
- provider: AgentProviderId,
+ definition: ProviderCliDefinition,
platform: NodeJS.Platform,
environment: Readonly,
homeDirectory: string
): string[] {
const path = platform === "win32" ? win32 : posix;
const directories: string[] = [];
- if (platform === "win32" && provider === "codex") {
- const localAppData = environment.LOCALAPPDATA ?? path.join(homeDirectory, "AppData", "Local");
- directories.push(path.join(localAppData, "Programs", "OpenAI", "Codex", "bin"));
+ for (const known of definition.knownDirectories ?? []) {
+ switch (known.root) {
+ case "home":
+ directories.push(path.join(homeDirectory, ...known.segments));
+ break;
+ case "windows-local-appdata": {
+ if (platform !== "win32") break;
+ const localAppData = environment.LOCALAPPDATA ?? path.join(homeDirectory, "AppData", "Local");
+ directories.push(path.join(localAppData, ...known.segments));
+ break;
+ }
+ }
}
- if (provider === "opencode") directories.push(path.join(homeDirectory, ".opencode", "bin"));
- if (provider === "kimi") directories.push(path.join(homeDirectory, ".kimi-code", "bin"));
- if (provider === "grok") directories.push(path.join(homeDirectory, ".grok", "bin"));
return directories;
}
+function normalizeProviderCliDefinitions(
+ definitions: readonly ProviderCliDefinition[] | undefined
+): readonly ProviderCliDefinition[] {
+ if (!definitions) return PROVIDER_CLI_IDS.map((id) => PROVIDER_CLI_DEFINITIONS[id]);
+ const seen = new Set();
+ for (const definition of definitions) {
+ if (definition.commands.length === 0) {
+ throw new Error(`Provider ${definition.id} must declare at least one CLI command.`);
+ }
+ if (seen.has(definition.id)) {
+ throw new Error(`Provider ${definition.id} is declared more than once.`);
+ }
+ seen.add(definition.id);
+ }
+ return definitions;
+}
+
function sharedUserDirectories(
platform: NodeJS.Platform,
environment: Readonly,
@@ -390,3 +468,7 @@ function providerLabel(provider: AgentProviderId): string {
if (provider === "pi") return "Pi";
return `${provider[0].toUpperCase()}${provider.slice(1)}`;
}
+
+function verifiedCursorPath(path: string): boolean {
+ try { return /(?:^|[\\/])(?:\.cursor|cursor-agent|cursor)(?:[\\/])/iu.test(realpathSync(path)); } catch { return false; }
+}
diff --git a/src/main/services/terminalLaunch.ts b/src/main/services/terminalLaunch.ts
index 1fb8459b..0f4e795c 100644
--- a/src/main/services/terminalLaunch.ts
+++ b/src/main/services/terminalLaunch.ts
@@ -84,7 +84,14 @@ const RESUME_ARGUMENTS: Record, string[]> = {
hermes: ["--continue"],
grok: ["--continue"],
omp: ["--continue"],
- pi: ["--continue"]
+ pi: ["--continue"],
+ cursor: ["--continue"],
+ minimax: ["--continue"],
+ devin: ["--continue"],
+ // Antigravity resumes only via the interactive /resume command or
+ // `--conversation `; there is no latest-session launch flag, so
+ // restore starts a fresh session.
+ antigravity: []
};
const DANGEROUS_ARGUMENTS: Record, string[]> = {
@@ -99,7 +106,21 @@ const DANGEROUS_ARGUMENTS: Record,
omp: ["--auto-approve"],
// pi 0.85.1 has no permission system, so it has no auto-approve flag. `-a, --approve`
// only skips its one prompt (trust project-local settings for this run).
- pi: ["--approve"]
+ pi: ["--approve"],
+ // The Cursor CLI follows Claude Code conventions; its permission bypass is the
+ // same flag Claude Code documents.
+ cursor: ["--dangerously-skip-permissions"],
+ // Measured on @minimax-ai/code 0.5.1: the CLI has no permission bypass flag.
+ // Permission modes (default/auto/bypassPermissions/off) are settings.json and
+ // TUI state (/permission, Alt+M) only, so YOLO launches the stock CLI.
+ minimax: [],
+ // Devin CLI documents --permission-mode; `dangerous` (aliases yolo/bypass)
+ // auto-approves every tool call. `smart` (an AI gatekeeper that approves only
+ // clearly-safe actions) is a supervised mode, deliberately NOT mapped here.
+ devin: ["--permission-mode", "dangerous"],
+ // Documented on antigravity.google/docs/cli: --dangerously-skip-permissions
+ // and --sandbox exist; no --yolo spelling.
+ antigravity: ["--dangerously-skip-permissions"]
};
function resolveWindowsShell(
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx
index b324a354..28ba058a 100644
--- a/src/renderer/src/App.tsx
+++ b/src/renderer/src/App.tsx
@@ -76,7 +76,7 @@ const FALLBACK_SETTINGS: AppSettings = {
homeAccentPreset: "classic",
homeAccentColors: { ...DEFAULT_HOME_ACCENT_COLORS },
sessionRowColorMode: "status",
- homeLauncherProviders: ["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi"],
+ homeLauncherProviders: ["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin", "antigravity"],
homeLimitProviders: ["codex", "claude", "qwen", "kimi", "opencode", "grok"],
canvasLauncherItems: [...DEFAULT_CANVAS_LAUNCHER_ITEMS],
radialLauncherItems: [...DEFAULT_RADIAL_LAUNCHER_ITEMS],
diff --git a/src/renderer/src/assets/providers/README.md b/src/renderer/src/assets/providers/README.md
index 4fe11b93..27c3eeaf 100644
--- a/src/renderer/src/assets/providers/README.md
+++ b/src/renderer/src/assets/providers/README.md
@@ -11,5 +11,9 @@ These files are vendor-supplied marks. Do not redraw, recolor, or modify them.
- `qwen.svg` — unmodified Qwen Code [`packages/desktop-shell/bootstrap/qwen-code-logo.svg`](https://github.com/QwenLM/qwen-code/blob/c3d9279932f592c39d8bf24de5da56c53d4ca60f/packages/desktop-shell/bootstrap/qwen-code-logo.svg).
- `omp.svg` — unmodified oh-my-pi [`packages/collab-web/public/favicon.svg`](https://github.com/can1357/oh-my-pi/blob/d3606c36ec3e23d7b8dbd02bf1db50bd97a87cb6/packages/collab-web/public/favicon.svg), byte-identical to the mark served by .
- `pi.svg` — unmodified Pi mark served by .
+- `cursor.ico` — official [Cursor favicon](https://www.cursor.com/favicon.ico), containing 32 and 16 px variants.
+- `minimax.ico` — official [MiniMax favicon](https://www.minimaxi.com/favicon.ico), 32 px.
+- `devin.ico` — official [Devin favicon](https://devin.ai/favicon.ico), containing 32 and 16 px variants.
+- `antigravity.ico` — official [Antigravity favicon](https://antigravity.google/favicon.ico), containing 32 and 32-bit plus 16 px variants.
The marks remain property of their respective owners and must be used according to each vendor's brand terms.
diff --git a/src/renderer/src/assets/providers/antigravity.ico b/src/renderer/src/assets/providers/antigravity.ico
new file mode 100644
index 00000000..f2ba9528
Binary files /dev/null and b/src/renderer/src/assets/providers/antigravity.ico differ
diff --git a/src/renderer/src/assets/providers/cursor.ico b/src/renderer/src/assets/providers/cursor.ico
new file mode 100644
index 00000000..1e3cdc0a
Binary files /dev/null and b/src/renderer/src/assets/providers/cursor.ico differ
diff --git a/src/renderer/src/assets/providers/devin.ico b/src/renderer/src/assets/providers/devin.ico
new file mode 100644
index 00000000..e60ef9ae
Binary files /dev/null and b/src/renderer/src/assets/providers/devin.ico differ
diff --git a/src/renderer/src/assets/providers/minimax.ico b/src/renderer/src/assets/providers/minimax.ico
new file mode 100644
index 00000000..38c0025d
Binary files /dev/null and b/src/renderer/src/assets/providers/minimax.ico differ
diff --git a/src/renderer/src/components/ProviderIcon.tsx b/src/renderer/src/components/ProviderIcon.tsx
index 2f83d853..772c74c6 100644
--- a/src/renderer/src/components/ProviderIcon.tsx
+++ b/src/renderer/src/components/ProviderIcon.tsx
@@ -9,6 +9,10 @@ import grokIcon from "../assets/providers/grok.png";
import ompIcon from "../assets/providers/omp.svg";
import piIcon from "../assets/providers/pi.svg";
import qwenIcon from "../assets/providers/qwen.svg";
+import cursorIcon from "../assets/providers/cursor.ico";
+import minimaxIcon from "../assets/providers/minimax.ico";
+import devinIcon from "../assets/providers/devin.ico";
+import antigravityIcon from "../assets/providers/antigravity.ico";
interface ProviderIconProps {
provider: ProviderId;
@@ -24,7 +28,11 @@ const PROVIDER_ASSETS = {
hermes: hermesIcon,
grok: grokIcon,
omp: ompIcon,
- pi: piIcon
+ pi: piIcon,
+ cursor: cursorIcon,
+ minimax: minimaxIcon,
+ devin: devinIcon,
+ antigravity: antigravityIcon
} as const;
export function ProviderIcon({ provider, size = "medium" }: ProviderIconProps): React.JSX.Element {
diff --git a/src/renderer/src/features/launcher/QuickRadialMenu.tsx b/src/renderer/src/features/launcher/QuickRadialMenu.tsx
index 7577031b..cdc2a580 100644
--- a/src/renderer/src/features/launcher/QuickRadialMenu.tsx
+++ b/src/renderer/src/features/launcher/QuickRadialMenu.tsx
@@ -22,7 +22,7 @@ interface RadialLauncherProps {
}
const PROVIDER_IDS = new Set([
- "terminal", "codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi"
+ "terminal", "codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin", "antigravity"
]);
export function RadialLauncher({
diff --git a/src/renderer/src/features/plugins/PluginFrame.tsx b/src/renderer/src/features/plugins/PluginFrame.tsx
index 07d02dea..8d352e06 100644
--- a/src/renderer/src/features/plugins/PluginFrame.tsx
+++ b/src/renderer/src/features/plugins/PluginFrame.tsx
@@ -410,7 +410,7 @@ function secretValue(value: unknown): string {
}
function isProvider(value: string): value is ProviderId {
- return value === "terminal" || value === "codex" || value === "claude" || value === "qwen" || value === "kimi" || value === "opencode" || value === "hermes" || value === "grok" || value === "omp" || value === "pi";
+ return value === "terminal" || value === "codex" || value === "claude" || value === "qwen" || value === "kimi" || value === "opencode" || value === "hermes" || value === "grok" || value === "omp" || value === "pi" || value === "cursor" || value === "minimax" || value === "devin" || value === "antigravity";
}
function encodeAssetPath(value: string): string {
diff --git a/src/renderer/src/lib/i18n.ts b/src/renderer/src/lib/i18n.ts
index 2e9b499c..6c24c60c 100644
--- a/src/renderer/src/lib/i18n.ts
+++ b/src/renderer/src/lib/i18n.ts
@@ -239,6 +239,10 @@ const ru = {
dangerGrok: "Grok Build будет автоматически одобрять все tool executions в рамках этого запуска.",
dangerOmp: "OMP будет автоматически одобрять все вызовы инструментов в рамках этого запуска.",
dangerPi: "Pi доверится проектным настройкам и расширениям для этого запуска, не спрашивая.",
+ dangerCursor: "Cursor пропустит все подтверждения разрешений в рамках этого запуска.",
+ dangerMinimax: "У MiniMax Code нет флага обхода разрешений: YOLO запускает обычный CLI, режим подтверждений переключается внутри через /permission.",
+ dangerDevin: "Devin будет автоматически одобрять все tool-вызовы без запросов в рамках этого запуска (--permission-mode dangerous).",
+ dangerAntigravity: "Antigravity пропустит все проверки разрешений в рамках этого запуска (--dangerously-skip-permissions).",
language: "Язык",
terminalSessionRestore: "Окна после перезапуска",
terminalSessionRestoreDescription: "Сохраняет открытые окна; агенты продолжают последнюю сессию в той же папке, обычные терминалы открываются заново.",
@@ -678,6 +682,10 @@ const en: Record = {
dangerGrok: "Grok Build automatically approves every tool execution for this launch.",
dangerOmp: "OMP automatically approves every tool call for this launch.",
dangerPi: "Pi trusts project-local settings and extensions for this launch without asking.",
+ dangerCursor: "Cursor skips every permission approval for this launch.",
+ dangerMinimax: "MiniMax Code has no permission bypass flag: YOLO launches the stock CLI; switch confirmation modes inside it via /permission.",
+ dangerDevin: "Devin auto-approves every tool call without prompting for this launch (--permission-mode dangerous).",
+ dangerAntigravity: "Antigravity skips every permission check for this launch (--dangerously-skip-permissions).",
language: "Language",
terminalSessionRestore: "Windows after restart",
terminalSessionRestoreDescription: "Saves open windows; agents continue the latest session in the same folder, while plain terminals reopen.",
diff --git a/src/renderer/src/lib/providers.ts b/src/renderer/src/lib/providers.ts
index ef01f73a..570f6eb1 100644
--- a/src/renderer/src/lib/providers.ts
+++ b/src/renderer/src/lib/providers.ts
@@ -20,10 +20,14 @@ export const PROVIDERS: Record = {
hermes: { id: "hermes", label: PROVIDER_LABELS.hermes, dangerKey: "dangerHermes", installUrl: "https://hermes-agent.nousresearch.com/docs/getting-started/installation" },
grok: { id: "grok", label: PROVIDER_LABELS.grok, dangerKey: "dangerGrok", installUrl: "https://docs.x.ai/build/overview" },
omp: { id: "omp", label: PROVIDER_LABELS.omp, dangerKey: "dangerOmp", installUrl: "https://github.com/can1357/oh-my-pi" },
- pi: { id: "pi", label: PROVIDER_LABELS.pi, dangerKey: "dangerPi", installUrl: "https://pi.dev/docs/latest" }
+ pi: { id: "pi", label: PROVIDER_LABELS.pi, dangerKey: "dangerPi", installUrl: "https://pi.dev/docs/latest" },
+ cursor: { id: "cursor", label: PROVIDER_LABELS.cursor, dangerKey: "dangerCursor", installUrl: "https://cursor.com/docs/cli/installation" },
+ minimax: { id: "minimax", label: PROVIDER_LABELS.minimax, dangerKey: "dangerMinimax", installUrl: "https://github.com/MiniMax-AI/minimax-code/blob/main/docs/installation.md" },
+ devin: { id: "devin", label: PROVIDER_LABELS.devin, dangerKey: "dangerDevin", installUrl: "https://docs.devin.ai/cli" },
+ antigravity: { id: "antigravity", label: PROVIDER_LABELS.antigravity, dangerKey: "dangerAntigravity", installUrl: "https://antigravity.google/docs/cli/install/" }
};
-export const AGENT_PROVIDERS: AgentProviderId[] = ["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi"];
+export const AGENT_PROVIDERS: AgentProviderId[] = ["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin", "antigravity"];
export const LIMIT_PROVIDERS: LimitProviderId[] = ["codex", "claude", "qwen", "kimi", "opencode", "grok"];
export function resolveHomeLauncherProviders(
diff --git a/src/shared/companion.ts b/src/shared/companion.ts
index f4efa257..8a3426ef 100644
--- a/src/shared/companion.ts
+++ b/src/shared/companion.ts
@@ -1,5 +1,5 @@
-import type { ProviderId, SessionStatus } from "./contracts.ts";
-import { CANVAS_LAUNCHER_ITEMS } from "./contracts.ts";
+import type { SessionStatus } from "./contracts.ts";
+import { CANVAS_LAUNCHER_ITEMS, type ProviderId } from "./providerCatalog.ts";
export const COMPANION_PROTOCOL_VERSION = 1;
diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts
index bc12a26e..0b783acd 100644
--- a/src/shared/contracts.ts
+++ b/src/shared/contracts.ts
@@ -1,9 +1,6 @@
-export type ProviderId = "terminal" | "codex" | "claude" | "qwen" | "kimi" | "opencode" | "hermes" | "grok" | "omp" | "pi";
-export const PROVIDER_LABELS: Record = {
- terminal: "Terminal", codex: "Codex", claude: "Claude", qwen: "Qwen Code",
- kimi: "Kimi", opencode: "OpenCode", hermes: "Hermes", grok: "Grok Build",
- omp: "OMP", pi: "Pi",
-};
+import { CANVAS_LAUNCHER_ITEMS, PROVIDER_LABELS, type CanvasLauncherItemId, type ProviderId } from "./providerCatalog.ts";
+export { CANVAS_LAUNCHER_ITEMS, PROVIDER_LABELS };
+export type { CanvasLauncherItemId, ProviderId };
export type AgentProviderId = Exclude;
export type AgentCliAvailability = Record;
export type LimitProviderId = Extract;
@@ -25,23 +22,9 @@ export type MinimapInteractionMode = "click" | "drag";
export type BrowserViewportSurface = "native" | "placeholder" | "hidden";
export type FocusActivation = "off" | "single" | "double";
export type ShortcutAction = "home" | "renameWindow";
-export type CanvasLauncherItemId = ProviderId;
export type RadialLauncherActionId = "note" | "browser" | "settings";
export type RadialLauncherItemId = ProviderId | RadialLauncherActionId;
-export const CANVAS_LAUNCHER_ITEMS: readonly CanvasLauncherItemId[] = [
- "codex",
- "claude",
- "qwen",
- "kimi",
- "opencode",
- "hermes",
- "grok",
- "omp",
- "pi",
- "terminal"
-];
-
// Keeps the safe provider subset proposed by @TroopJostle in PR #23 while
// region, note, Browser, and Settings remain fixed top-level menu actions.
export const DEFAULT_CANVAS_LAUNCHER_ITEMS: readonly CanvasLauncherItemId[] = [
@@ -62,6 +45,10 @@ export const RADIAL_LAUNCHER_ITEMS: readonly RadialLauncherItemId[] = [
"grok",
"omp",
"pi",
+ "cursor",
+ "minimax",
+ "devin",
+ "antigravity",
"terminal",
"note",
"browser",
@@ -563,10 +550,14 @@ export const BROWSER_PROVIDER_COLORS: Record = {
opencode: "#5A5858",
hermes: "#D6A700",
grok: "#111111",
- // OMP and Pi never reach the browser bridge, so these two values are never
- // rendered; they exist only to keep the record total over the provider union.
+ // OMP, Pi, Cursor, and MiniMax never reach the browser bridge, so these values
+ // are never rendered; they exist only to keep the record total over the provider union.
omp: "#6E6A8A",
pi: "#4F7C8A",
+ cursor: "#1F1F1F",
+ minimax: "#3C2A6B",
+ devin: "#4E5BA6",
+ antigravity: "#1A73E8",
unknown: "#7A8291"
};
diff --git a/src/shared/providerCatalog.ts b/src/shared/providerCatalog.ts
new file mode 100644
index 00000000..6bc0c62f
--- /dev/null
+++ b/src/shared/providerCatalog.ts
@@ -0,0 +1,27 @@
+/** Provider ids, labels and launcher order. Kept free of imports and other data so small
+ * bundles, such as the Even G2 companion, can use it without the rest of the contracts. */
+export type ProviderId = "terminal" | "codex" | "claude" | "qwen" | "kimi" | "opencode" | "hermes" | "grok" | "omp" | "pi" | "cursor" | "minimax" | "devin" | "antigravity";
+export const PROVIDER_LABELS: Record = {
+ terminal: "Terminal", codex: "Codex", claude: "Claude", qwen: "Qwen Code",
+ kimi: "Kimi", opencode: "OpenCode", hermes: "Hermes", grok: "Grok Build",
+ omp: "OMP", pi: "Pi", cursor: "Cursor", minimax: "MiniMax Code",
+ devin: "Devin", antigravity: "Antigravity",
+};
+export type CanvasLauncherItemId = ProviderId;
+
+export const CANVAS_LAUNCHER_ITEMS: readonly CanvasLauncherItemId[] = [
+ "codex",
+ "claude",
+ "qwen",
+ "kimi",
+ "opencode",
+ "hermes",
+ "grok",
+ "omp",
+ "pi",
+ "cursor",
+ "minimax",
+ "devin",
+ "antigravity",
+ "terminal"
+];
diff --git a/tests/agent-runtime-provider-launch.test.mjs b/tests/agent-runtime-provider-launch.test.mjs
index df04f843..6f0ea671 100644
--- a/tests/agent-runtime-provider-launch.test.mjs
+++ b/tests/agent-runtime-provider-launch.test.mjs
@@ -70,7 +70,7 @@ test("helper process flags stay scoped to hook commands instead of the agent PTY
test("revoking CanvasTTY lifecycle hooks leaves every provider launch unmodified", async (t) => {
const root = await fixture(t);
const adapters = adaptersFor(root);
- for (const provider of ["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi"]) {
+ for (const provider of ["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin", "antigravity"]) {
const launch = adapters.prepare(provider, `session-${provider}`, false);
assert.deepEqual(launch.args, []);
assert.deepEqual(launch.environment, {});
@@ -78,6 +78,18 @@ test("revoking CanvasTTY lifecycle hooks leaves every provider launch unmodified
}
});
+test("providers without a hook adapter never write Grok's shared hook configuration", async (t) => {
+ const root = await fixture(t);
+ const adapters = adaptersFor(root);
+ for (const provider of ["omp", "pi", "cursor", "minimax", "devin", "antigravity"]) {
+ const launch = adapters.prepare(provider, `session-${provider}`, true);
+ assert.deepEqual(launch.args, [], provider);
+ assert.deepEqual(launch.environment, {}, provider);
+ await assert.rejects(readFile(join(root, "grok", "hooks", "canvastty-runtime-hooks.json"), "utf8"), /ENOENT/u, provider);
+ launch.releaseConfiguration();
+ }
+});
+
test("a revoked shared-config launch removes stale hooks held by an older live session", async (t) => {
const root = await fixture(t);
const adapters = adaptersFor(root);
diff --git a/tests/provider-cli-registry.test.mjs b/tests/provider-cli-registry.test.mjs
index 49bf6980..1020fd12 100644
--- a/tests/provider-cli-registry.test.mjs
+++ b/tests/provider-cli-registry.test.mjs
@@ -138,6 +138,95 @@ test("Windows batch launch uses the startup-resolved command prompt", () => {
assert.equal(launch.windowsVerbatimArguments, true);
});
+test("Cursor permits an explicitly configured generic agent executable", () => {
+ const agent = "/test-home/.local/bin/agent";
+ const registry = createProviderCliRegistry({
+ platform: "darwin",
+ overrides: { cursor: agent },
+ environment: { PATH: "/usr/bin:/bin" },
+ homeDirectory: "/test-home",
+ inspectCandidate: inspection(new Map([
+ [agent, null],
+ ["/usr/bin/cursor", null]
+ ])),
+ directoryExists: (path) => ["/usr/bin", "/bin", "/test-home/.local/bin"].includes(path)
+ });
+
+ const resolution = registry.get("cursor");
+ assert.equal(resolution.state, "available");
+ assert.equal(resolution.provider, "cursor");
+ assert.equal(resolution.executable, agent);
+ assert.equal(resolution.environment.PATH, "/usr/bin:/bin:/test-home/.local/bin");
+ // A literal `cursor` executable must never be selected for the cursor provider.
+ assert.equal(resolution.checked.some((candidate) => candidate.path.endsWith("/cursor")), false);
+});
+
+test("Cursor falls back to the cursor-agent spelling when agent is absent", () => {
+ const legacy = "/usr/local/bin/cursor-agent";
+ const registry = createProviderCliRegistry({
+ platform: "linux",
+ environment: { PATH: "/usr/bin:/usr/local/bin" },
+ inspectCandidate: inspection(new Map([[legacy, null]])),
+ directoryExists: () => true
+ });
+
+ const resolution = registry.get("cursor");
+ assert.equal(resolution.state, "available");
+ assert.equal(resolution.executable, legacy);
+});
+
+test("MiniMax Code resolves through its mcode command instead of the provider id", () => {
+ const mcode = "/test-home/.npm-global/bin/mcode";
+ const registry = createProviderCliRegistry({
+ platform: "linux",
+ environment: { PATH: "/usr/bin" },
+ homeDirectory: "/test-home",
+ inspectCandidate: inspection(new Map([
+ [mcode, null],
+ ["/usr/bin/minimax", null]
+ ])),
+ directoryExists: (path) => ["/usr/bin", "/test-home/.npm-global/bin"].includes(path)
+ });
+
+ const resolution = registry.get("minimax");
+ assert.equal(resolution.state, "available");
+ assert.equal(resolution.executable, mcode);
+ assert.equal(resolution.checked.some((candidate) => candidate.path.endsWith("/minimax")), false);
+});
+
+test("Devin resolves through its devin command", () => {
+ const devin = "/opt/homebrew/bin/devin";
+ const registry = createProviderCliRegistry({
+ platform: "darwin",
+ environment: { PATH: "/usr/bin:/bin" },
+ inspectCandidate: inspection(new Map([[devin, null]])),
+ directoryExists: (path) => ["/usr/bin", "/bin", "/opt/homebrew/bin"].includes(path)
+ });
+
+ const resolution = registry.get("devin");
+ assert.equal(resolution.state, "available");
+ assert.equal(resolution.executable, devin);
+});
+
+test("Antigravity resolves through its agy command instead of the provider id", () => {
+ const agy = "/test-home/.local/bin/agy";
+ const registry = createProviderCliRegistry({
+ platform: "linux",
+ environment: { PATH: "/usr/bin" },
+ homeDirectory: "/test-home",
+ inspectCandidate: inspection(new Map([
+ [agy, null],
+ ["/usr/bin/antigravity", null]
+ ])),
+ directoryExists: (path) => ["/usr/bin", "/test-home/.local/bin"].includes(path)
+ });
+
+ const resolution = registry.get("antigravity");
+ assert.equal(resolution.state, "available");
+ assert.equal(resolution.executable, agy);
+ assert.equal(resolution.checked.some((candidate) => candidate.path.endsWith("/antigravity")), false);
+});
+
test("registry snapshot and provider resolutions are immutable", () => {
const registry = createProviderCliRegistry({
platform: "linux",
@@ -150,6 +239,105 @@ test("registry snapshot and provider resolutions are immutable", () => {
assert.equal(Object.isFrozen(registry.get("codex")), true);
});
+test("custom definitions resolve executables that do not match the provider id", () => {
+ const registry = createProviderCliRegistry({
+ platform: "linux",
+ environment: { PATH: "/usr/bin" },
+ definitions: [{ id: "example", commands: ["exa"] }],
+ inspectCandidate: inspection(new Map([["/usr/bin/exa", null]])),
+ directoryExists: () => true
+ });
+
+ const resolution = registry.get("example");
+ assert.equal(resolution.state, "available");
+ assert.equal(resolution.provider, "example");
+ assert.equal(resolution.executable, "/usr/bin/exa");
+ assert.deepEqual(Object.keys(registry.snapshot()), ["example"]);
+});
+
+test("definitions fall back to later commands when the primary command is missing", () => {
+ const registry = createProviderCliRegistry({
+ platform: "linux",
+ environment: { PATH: "/usr/bin" },
+ definitions: [{ id: "example", commands: ["exa", "example-agent"] }],
+ inspectCandidate: inspection(new Map([["/usr/bin/example-agent", null]])),
+ directoryExists: () => true
+ });
+
+ const resolution = registry.get("example");
+ assert.equal(resolution.state, "available");
+ assert.equal(resolution.executable, "/usr/bin/example-agent");
+ assert.deepEqual(
+ resolution.checked.map((candidate) => candidate.path),
+ ["/usr/bin/exa", "/usr/bin/example-agent"]
+ );
+});
+
+test("definition known directories participate in resolution and child PATH", () => {
+ const registry = createProviderCliRegistry({
+ platform: "darwin",
+ environment: { PATH: "/usr/bin:/bin" },
+ homeDirectory: "/test-home",
+ definitions: [{
+ id: "example",
+ commands: ["exa"],
+ knownDirectories: [{ root: "home", segments: [".example", "bin"] }]
+ }],
+ inspectCandidate: inspection(new Map([["/test-home/.example/bin/exa", null]])),
+ directoryExists: (path) => ["/usr/bin", "/bin", "/test-home/.example/bin"].includes(path)
+ });
+
+ const resolution = registry.get("example");
+ assert.equal(resolution.state, "available");
+ assert.equal(resolution.executable, "/test-home/.example/bin/exa");
+ assert.equal(resolution.environment.PATH, "/usr/bin:/bin:/test-home/.example/bin");
+});
+
+test("windows-local-appdata known directories are ignored outside Windows", () => {
+ const registry = createProviderCliRegistry({
+ platform: "darwin",
+ environment: { PATH: "/usr/bin" },
+ homeDirectory: "/test-home",
+ definitions: [{
+ id: "example",
+ commands: ["exa"],
+ knownDirectories: [{ root: "windows-local-appdata", segments: ["Programs", "Example", "bin"] }]
+ }],
+ inspectCandidate: () => "missing",
+ directoryExists: () => true
+ });
+
+ const resolution = registry.get("example");
+ assert.equal(resolution.state, "unavailable");
+ assert.equal(
+ resolution.checked.some((candidate) => candidate.path.includes("AppData")),
+ false
+ );
+});
+
+test("definitions without commands or with duplicate ids are rejected", () => {
+ const base = {
+ platform: "linux",
+ environment: {},
+ inspectCandidate: () => "missing",
+ directoryExists: () => false
+ };
+ assert.throws(
+ () => createProviderCliRegistry({ ...base, definitions: [{ id: "example", commands: [] }] }),
+ /at least one CLI command/u
+ );
+ assert.throws(
+ () => createProviderCliRegistry({
+ ...base,
+ definitions: [
+ { id: "example", commands: ["exa"] },
+ { id: "example", commands: ["exa2"] }
+ ]
+ }),
+ /declared more than once/u
+ );
+});
+
test("refresh detects installed and removed CLIs without changing an earlier snapshot", () => {
const executable = "/tools/codex";
const present = new Set();
diff --git a/tests/settings-normalizer.test.mjs b/tests/settings-normalizer.test.mjs
index 0311b184..515c4ba9 100644
--- a/tests/settings-normalizer.test.mjs
+++ b/tests/settings-normalizer.test.mjs
@@ -25,7 +25,7 @@ const fallback = {
media: "#D5A2C9"
},
sessionRowColorMode: "status",
- homeLauncherProviders: ["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi"],
+ homeLauncherProviders: ["codex", "claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin", "antigravity"],
homeLimitProviders: ["codex", "claude", "qwen", "kimi", "opencode", "grok"],
canvasLauncherItems: ["codex", "claude", "qwen", "opencode", "terminal"],
radialLauncherItems: ["codex", "claude", "qwen", "opencode", "note", "terminal", "browser", "settings"],
@@ -313,7 +313,7 @@ test("a persisted pre-Grok launcher subset gains Grok exactly once", async () =>
}));
const store = new SettingsStore(dir, "en");
const loaded = await store.load();
- assert.deepEqual(loaded.homeLauncherProviders, ["codex", "kimi", "hermes", "grok", "omp", "pi"]);
+ assert.deepEqual(loaded.homeLauncherProviders, ["codex", "kimi", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin", "antigravity"]);
await store.update({ homeLauncherProviders: ["codex", "claude", "kimi", "opencode", "hermes"] });
const reloaded = new SettingsStore(dir, "en");
@@ -347,7 +347,7 @@ test("the pre-Qwen default selections gain Qwen while curated subsets remain unc
homeLimitProviders: ["kimi"]
}));
const curated = await new SettingsStore(curatedDir, "en").load();
- assert.deepEqual(curated.homeLauncherProviders, ["codex", "hermes", "omp", "pi"]);
+ assert.deepEqual(curated.homeLauncherProviders, ["codex", "hermes", "omp", "pi", "cursor", "minimax", "devin", "antigravity"]);
assert.deepEqual(curated.homeLimitProviders, ["kimi"]);
} finally {
await Promise.all([
@@ -369,7 +369,7 @@ test("the Qwen migration does not rerun the older expanded-limit migration", asy
assert.deepEqual(loaded.homeLimitProviders, ["codex", "claude", "kimi"]);
const persisted = JSON.parse(await readFile(join(dir, "settings.json"), "utf8"));
- assert.equal(persisted.settingsVersion, 15);
+ assert.equal(persisted.settingsVersion, 19);
assert.equal(persisted.agentLifecycleHooksEnabled, true);
assert.deepEqual(persisted.homeLimitProviders, ["codex", "claude", "kimi"]);
} finally {
@@ -388,11 +388,11 @@ test("the limit-display migration preserves a version-three launcher subset", as
}));
const store = new SettingsStore(dir, "en");
const loaded = await store.load();
- assert.deepEqual(loaded.homeLauncherProviders, ["codex", "kimi", "omp", "pi"]);
+ assert.deepEqual(loaded.homeLauncherProviders, ["codex", "kimi", "omp", "pi", "cursor", "minimax", "devin", "antigravity"]);
assert.deepEqual(loaded.homeLimitProviders, fallback.homeLimitProviders);
const persisted = JSON.parse(await readFile(join(dir, "settings.json"), "utf8"));
- assert.equal(persisted.settingsVersion, 15);
+ assert.equal(persisted.settingsVersion, 19);
assert.equal(persisted.agentLifecycleHooksEnabled, true);
assert.deepEqual(persisted.homeLimitProviders, fallback.homeLimitProviders);
} finally {
@@ -412,7 +412,7 @@ test("the expanded limit migration preserves a curated version-four subset", asy
assert.deepEqual(loaded.homeLimitProviders, ["kimi"]);
const persisted = JSON.parse(await readFile(join(dir, "settings.json"), "utf8"));
- assert.equal(persisted.settingsVersion, 15);
+ assert.equal(persisted.settingsVersion, 19);
assert.equal(persisted.agentLifecycleHooksEnabled, true);
assert.deepEqual(persisted.homeLimitProviders, ["kimi"]);
} finally {
@@ -564,7 +564,7 @@ test("existing profiles migrate minimap interaction to click and persist later c
const store = new SettingsStore(dir, "en");
assert.equal((await store.load()).minimapInteractionMode, "click");
let persisted = JSON.parse(await readFile(join(dir, "settings.json"), "utf8"));
- assert.equal(persisted.settingsVersion, 15);
+ assert.equal(persisted.settingsVersion, 19);
assert.equal(persisted.minimapInteractionMode, "click");
await store.update({ minimapInteractionMode: "drag" });
diff --git a/tests/terminal-launch.test.mjs b/tests/terminal-launch.test.mjs
index b40f21bf..f542a3da 100644
--- a/tests/terminal-launch.test.mjs
+++ b/tests/terminal-launch.test.mjs
@@ -72,13 +72,19 @@ test("restored agent windows use each provider's native continue mode", () => {
"--last"
]);
- for (const provider of ["claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi"]) {
+ for (const provider of ["claude", "qwen", "kimi", "opencode", "hermes", "grok", "omp", "pi", "cursor", "minimax", "devin"]) {
const launch = resolveTerminalLaunch(provider, "normal", ["--bridge"], {
providerCli: available(provider, `/resolved/${provider}`),
resumePrevious: true
});
assert.deepEqual(launch.args, ["--bridge", "--continue"]);
}
+ // antigravity deliberately restores fresh: no latest-session launch flag.
+ const restored = resolveTerminalLaunch("antigravity", "normal", ["--bridge"], {
+ providerCli: available("antigravity", "/resolved/agy"),
+ resumePrevious: true
+ });
+ assert.deepEqual(restored.args, ["--bridge"]);
});
test("OMP and Pi use their documented dangerous flags instead of the legacy default", () => {
@@ -89,6 +95,26 @@ test("OMP and Pi use their documented dangerous flags instead of the legacy defa
assert.deepEqual(pi.args, ["--approve"]);
});
+test("Cursor YOLO uses its Claude-Code-style permission bypass", () => {
+ const cursor = resolveTerminalLaunch("cursor", "yolo", [], { providerCli: available("cursor", "/resolved/agent") });
+ assert.deepEqual(cursor.args, ["--dangerously-skip-permissions"]);
+});
+
+test("MiniMax YOLO launches the stock CLI because mcode has no bypass flag", () => {
+ const minimax = resolveTerminalLaunch("minimax", "yolo", [], { providerCli: available("minimax", "/resolved/mcode") });
+ assert.deepEqual(minimax.args, []);
+});
+
+test("Devin YOLO selects the documented dangerous permission mode", () => {
+ const devin = resolveTerminalLaunch("devin", "yolo", [], { providerCli: available("devin", "/resolved/devin") });
+ assert.deepEqual(devin.args, ["--permission-mode", "dangerous"]);
+});
+
+test("Antigravity YOLO uses its documented bypass flag", () => {
+ const yolo = resolveTerminalLaunch("antigravity", "yolo", [], { providerCli: available("antigravity", "/resolved/agy") });
+ assert.deepEqual(yolo.args, ["--dangerously-skip-permissions"]);
+});
+
test("OpenCode merges YOLO config with the registry child environment", () => {
const providerCli = available("opencode", "/test-home/.local/bin/opencode");
const launch = resolveTerminalLaunch("opencode", "yolo", [], {
diff --git a/tests/terminal-session-store.test.mjs b/tests/terminal-session-store.test.mjs
index f63358ac..f0b06df3 100644
--- a/tests/terminal-session-store.test.mjs
+++ b/tests/terminal-session-store.test.mjs
@@ -36,6 +36,19 @@ test("terminal window descriptors persist atomically without scrollback or envir
}
});
+test("cursor agent sessions persist like every other provider", async () => {
+ const directory = await mkdtemp(join(tmpdir(), "canvastty-terminal-state-cursor-"));
+ try {
+ const cursorSession = { ...descriptor, id: "0f9e8d7c-6b5a-4c3b-2a19-f8e7d6c5b4a3", provider: "cursor" };
+ const store = new TerminalSessionStore(directory);
+ await store.replace([cursorSession]);
+ const restored = await new TerminalSessionStore(directory).load();
+ assert.deepEqual(restored.map((session) => session.provider), ["cursor"]);
+ } finally {
+ await rm(directory, { recursive: true, force: true });
+ }
+});
+
test("invalid descriptors are removed and geometry is bounded", () => {
const normalized = normalizePersistedTerminalSessions({
version: 1,