mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-06 08:02:04 +00:00
feat(agents): Muse Code and jcode support, native hooks for Empryo, Muse Code and jcode (#1071)
* feat(agents): recognise Muse Code and jcode Muse Code's `muse` launcher execs a versioned `muse-bin-<version>` binary, so detection also maps that name to Muse. Muse resumes with `muse resume <id>` (root options on either side of the subcommand); jcode with `jcode --resume <id>`. * feat(agents): install native Empryo and jcode hooks * feat(agents): add jcode lifecycle hooks * fix(agents): preserve jcode hooks and correct Empryo removal contract * feat(agents): install Muse native plugin hooks * docs(agents): list Empryo, Muse Code and jcode as hook-backed * fix(i18n): translate Empryo, Muse Code and jcode search keywords * fix(agents): read jcode hook payload from env, route its reports like Codex, bound muse CLI calls jcode runs hooks with stdin closed and puts session_id/cwd in JCODE_HOOK_PAYLOAD, so reports carried no session and resume never learned an id. Its shared server also runs every session's hooks with the first client's TTY7_PANE, so reports go through the same session/directory routing as Codex. muse plugins calls now time out after 30s and surface stderr. Adds ru strings for the new keys and reattaches Kimi's doc comment to KIMI_HOOK_EVENTS. * docs: count Muse Code and jcode in the README agent totals --------- Co-authored-by: kalpakprod <307643502+kalpakprod@users.noreply.github.com> Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
co-authored by
kalpakprod
l0ng-ai
parent
73cd393590
commit
d3f7dadd5d
@@ -52,7 +52,7 @@ Native builds for macOS, Windows, and Linux on [**Releases**](https://github.com
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Agent-aware** | per-pane detection (26 CLIs) · status dot · notifications · branch + diff · tray icon when input is needed · resume after reboot · reopen any past session from Search Everywhere · one-key launch · tab sidebar grouped by repository, with pinned groups |
|
||||
| **Agent-aware** | per-pane detection (28 CLIs) · status dot · notifications · branch + diff · tray icon when input is needed · resume after reboot · reopen any past session from Search Everywhere · one-key launch · tab sidebar grouped by repository, with pinned groups |
|
||||
| **CLI + Skills** | bundled `tty7` CLI · [agent skill](skills/tty7/SKILL.md) · `run` streams a command and exits with its code · `split` · `send` · `wait --until free` · `capture` |
|
||||
| **Editor-grade input** | ghost suggestions from history · explained tab completion · syntax highlighting · multi-line editing · click places the caret · <kbd>⌃ R</kbd> fuzzy history |
|
||||
| **Window** | tabs & splits · <kbd>⌘ P</kbd> Search Everywhere · <kbd>⌘ F</kbd> scrollback search · hibernate a tab to free its memory · IME |
|
||||
@@ -71,7 +71,7 @@ that tells tty7 which session to fork. **Past sessions** are read from the agent
|
||||
own history files and listed in Search Everywhere, where <kbd>⏎</kbd> resumes one.
|
||||
|
||||
<details>
|
||||
<summary>The full support matrix, all twenty-six</summary>
|
||||
<summary>The full support matrix, all twenty-eight</summary>
|
||||
|
||||
| Agent | Detected | Status · resume | Fork | Past sessions |
|
||||
|---|:-:|:-:|:-:|:-:|
|
||||
@@ -95,12 +95,14 @@ own history files and listed in Search Everywhere, where <kbd>⏎</kbd> resumes
|
||||
| **Crush** | ✓ | ✓ | | |
|
||||
| **Antigravity** | ✓ | ✓ | | |
|
||||
| **Cursor** | ✓ | ✓ | | ✓ |
|
||||
| **Empryo** | ✓ | ✓ | | |
|
||||
| **Muse Code** | ✓ | ✓ | | |
|
||||
| **jcode** | ✓ | ✓ | | |
|
||||
| Aider | ✓ | | | |
|
||||
| Amp | ✓ | | | |
|
||||
| Auggie | ✓ | | | |
|
||||
| Hermes | ✓ | | | |
|
||||
| Vibe | ✓ | | | |
|
||||
| Empryo | ✓ | | | |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
+5
-3
@@ -51,7 +51,7 @@ macOS、Windows、Linux 的原生构建都在 [**Releases**](https://github.com/
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Agent 感知** | 逐 pane 识别 26 个 CLI agent · 状态点 · 通知 · 分支 + diff · 需要输入时托盘图标提醒 · 重启后续上会话 · 在随处搜索里找回任意历史会话 · 一键启动 · 侧边栏按仓库分组,可置顶 |
|
||||
| **Agent 感知** | 逐 pane 识别 28 个 CLI agent · 状态点 · 通知 · 分支 + diff · 需要输入时托盘图标提醒 · 重启后续上会话 · 在随处搜索里找回任意历史会话 · 一键启动 · 侧边栏按仓库分组,可置顶 |
|
||||
| **CLI + Skills** | 安装包自带 `tty7` CLI · [agent skill](skills/tty7/SKILL.md) · `run` 转发命令输出并原样返回退出码 · `split` · `send` · `wait --until free` · `capture` |
|
||||
| **编辑器级输入** | 基于历史的内联补全建议 · Tab 补全附带说明 · 语法高亮 · 多行编辑 · 点击定位光标 · <kbd>⌃ R</kbd> 模糊搜索历史 |
|
||||
| **窗口** | 标签页与分屏 · <kbd>⌘ P</kbd> 随处搜索 · <kbd>⌘ F</kbd> 回滚搜索 · 休眠标签页以释放内存 · 输入法 |
|
||||
@@ -68,7 +68,7 @@ macOS、Windows、Linux 的原生构建都在 [**Releases**](https://github.com/
|
||||
**历史会话**从 agent 自己的历史文件读取,列在随处搜索里,<kbd>⏎</kbd> 即可续上。
|
||||
|
||||
<details>
|
||||
<summary>26 个 agent 的完整支持矩阵</summary>
|
||||
<summary>28 个 agent 的完整支持矩阵</summary>
|
||||
|
||||
| Agent | 识别 | 状态 · 重启恢复 | Fork | 历史会话 |
|
||||
|---|:-:|:-:|:-:|:-:|
|
||||
@@ -92,12 +92,14 @@ macOS、Windows、Linux 的原生构建都在 [**Releases**](https://github.com/
|
||||
| **Crush** | ✓ | ✓ | | |
|
||||
| **Antigravity** | ✓ | ✓ | | |
|
||||
| **Cursor** | ✓ | ✓ | | ✓ |
|
||||
| **Empryo** | ✓ | ✓ | | |
|
||||
| **Muse Code** | ✓ | ✓ | | |
|
||||
| **jcode** | ✓ | ✓ | | |
|
||||
| Aider | ✓ | | | |
|
||||
| Amp | ✓ | | | |
|
||||
| Auggie | ✓ | | | |
|
||||
| Hermes | ✓ | | | |
|
||||
| Vibe | ✓ | | | |
|
||||
| Empryo | ✓ | | | |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ pub fn run_agent_hook(agent: &str, event: &str) {
|
||||
if !std::io::stdin().is_terminal() {
|
||||
let _ = std::io::stdin().take(MAX_STDIN).read_to_string(&mut input);
|
||||
}
|
||||
let input = hook_payload(agent, input, |k| std::env::var(k).ok());
|
||||
let Some(event) = effective_event(agent, event, &input) else {
|
||||
return;
|
||||
};
|
||||
@@ -65,6 +66,20 @@ fn detach_console() {
|
||||
#[cfg(unix)]
|
||||
fn detach_console() {}
|
||||
|
||||
/// The event's payload: what the agent wrote on stdin, or — for jcode, which
|
||||
/// leaves stdin closed and describes the event in its environment instead —
|
||||
/// the JSON mirror it exports as `JCODE_HOOK_PAYLOAD` (`session_id`, `cwd`,
|
||||
/// and the event's own fields).
|
||||
fn hook_payload(agent: &str, stdin: String, env: impl Fn(&str) -> Option<String>) -> String {
|
||||
if agent == "jcode"
|
||||
&& stdin.trim().is_empty()
|
||||
&& let Some(payload) = env("JCODE_HOOK_PAYLOAD").filter(|p| !p.trim().is_empty())
|
||||
{
|
||||
return payload;
|
||||
}
|
||||
stdin
|
||||
}
|
||||
|
||||
fn effective_agent(agent: &str, ran_by_grok: bool) -> &str {
|
||||
if ran_by_grok { "grok" } else { agent }
|
||||
}
|
||||
@@ -602,10 +617,13 @@ pub enum HookAgent {
|
||||
PrimeAgent,
|
||||
Antigravity,
|
||||
QoderCLICn,
|
||||
Empryo,
|
||||
Jcode,
|
||||
Muse,
|
||||
}
|
||||
|
||||
impl HookAgent {
|
||||
pub const ALL: [HookAgent; 20] = [
|
||||
pub const ALL: [HookAgent; 23] = [
|
||||
HookAgent::Claude,
|
||||
HookAgent::Codex,
|
||||
HookAgent::TraeCode,
|
||||
@@ -626,6 +644,9 @@ impl HookAgent {
|
||||
HookAgent::Cursor,
|
||||
HookAgent::PrimeAgent,
|
||||
HookAgent::Antigravity,
|
||||
HookAgent::Empryo,
|
||||
HookAgent::Jcode,
|
||||
HookAgent::Muse,
|
||||
];
|
||||
|
||||
/// The hooks behind a detected agent process, if it has any.
|
||||
@@ -645,6 +666,9 @@ impl HookAgent {
|
||||
CLIAgent::OhMyPi => Some(HookAgent::OhMyPi),
|
||||
CLIAgent::PrimeAgent => Some(HookAgent::PrimeAgent),
|
||||
CLIAgent::Antigravity => Some(HookAgent::Antigravity),
|
||||
CLIAgent::Empryo => Some(HookAgent::Empryo),
|
||||
CLIAgent::Jcode => Some(HookAgent::Jcode),
|
||||
CLIAgent::Muse => Some(HookAgent::Muse),
|
||||
CLIAgent::Gemini => Some(HookAgent::Gemini),
|
||||
CLIAgent::Droid => Some(HookAgent::Droid),
|
||||
CLIAgent::Qwen => Some(HookAgent::Qwen),
|
||||
@@ -659,8 +683,7 @@ impl HookAgent {
|
||||
| CLIAgent::Amp
|
||||
| CLIAgent::Auggie
|
||||
| CLIAgent::Hermes
|
||||
| CLIAgent::Vibe
|
||||
| CLIAgent::Empryo => None,
|
||||
| CLIAgent::Vibe => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -676,6 +699,8 @@ impl HookAgent {
|
||||
HookAgent::Droid => Some(DROID_HOOK_EVENTS),
|
||||
HookAgent::Qwen => Some(QWEN_HOOK_EVENTS),
|
||||
HookAgent::QoderCLI | HookAgent::QoderCLICn => Some(QODER_HOOK_EVENTS),
|
||||
HookAgent::Empryo => Some(EMPRYO_HOOK_EVENTS),
|
||||
HookAgent::Jcode => Some(JCODE_HOOK_EVENTS),
|
||||
HookAgent::Crush => Some(CRUSH_HOOK_EVENTS),
|
||||
HookAgent::CodeBuddy => Some(CODEBUDDY_HOOK_EVENTS),
|
||||
HookAgent::Cursor => Some(CURSOR_HOOK_EVENTS),
|
||||
@@ -687,7 +712,8 @@ impl HookAgent {
|
||||
| HookAgent::PrimeAgent
|
||||
| HookAgent::Antigravity
|
||||
| HookAgent::Goose
|
||||
| HookAgent::Kimi => None,
|
||||
| HookAgent::Kimi
|
||||
| HookAgent::Muse => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,6 +770,9 @@ impl HookAgent {
|
||||
HookAgent::OhMyPi => "omp",
|
||||
HookAgent::PrimeAgent => "prime-agent",
|
||||
HookAgent::Antigravity => "antigravity",
|
||||
HookAgent::Empryo => "empryo",
|
||||
HookAgent::Jcode => "jcode",
|
||||
HookAgent::Muse => "muse",
|
||||
HookAgent::Gemini => "gemini",
|
||||
HookAgent::Droid => "droid",
|
||||
HookAgent::Qwen => "qwen",
|
||||
@@ -769,6 +798,9 @@ impl HookAgent {
|
||||
HookAgent::OhMyPi => "Oh My Pi",
|
||||
HookAgent::PrimeAgent => "Prime Agent",
|
||||
HookAgent::Antigravity => "Antigravity",
|
||||
HookAgent::Empryo => "Empryo",
|
||||
HookAgent::Jcode => "jcode",
|
||||
HookAgent::Muse => "Muse Code",
|
||||
HookAgent::Gemini => "Gemini",
|
||||
HookAgent::Droid => "Droid",
|
||||
HookAgent::Qwen => "Qwen Code",
|
||||
@@ -801,6 +833,17 @@ impl HookAgent {
|
||||
target.under_home(&[".prime", "agent", "extensions", "tty7", "index.ts"])
|
||||
}
|
||||
HookAgent::Antigravity => target.under_home(&[".gemini", "config", "hooks.json"]),
|
||||
HookAgent::Empryo => target.under_home(&[".empryo", "hooks.json"]),
|
||||
HookAgent::Jcode => target.jcode_config_path(),
|
||||
HookAgent::Muse => target.under_home(&[
|
||||
".local",
|
||||
"share",
|
||||
"tty7",
|
||||
"agent-hooks",
|
||||
"muse",
|
||||
".muse-plugin",
|
||||
"plugin.json",
|
||||
]),
|
||||
HookAgent::Grok => target.under_home(&[".grok", "hooks", OWNED_FILE_STEM_JSON]),
|
||||
HookAgent::OhMyPi => {
|
||||
target.under_home(&[".omp", "agent", "extensions", "tty7", "index.ts"])
|
||||
@@ -939,6 +982,15 @@ impl<'a> HookTarget<'a> {
|
||||
self.under_home(&[".config"])
|
||||
}
|
||||
|
||||
fn jcode_config_path(&self) -> PathBuf {
|
||||
if self.is_local()
|
||||
&& let Some(dir) = std::env::var_os("JCODE_HOME").filter(|d| !d.is_empty())
|
||||
{
|
||||
return PathBuf::from(dir).join("config.toml");
|
||||
}
|
||||
self.under_home(&[".jcode", "config.toml"])
|
||||
}
|
||||
|
||||
fn kimi_config_path(&self) -> PathBuf {
|
||||
if self.is_local()
|
||||
&& let Some(dir) = std::env::var_os("KIMI_CODE_HOME").filter(|d| !d.is_empty())
|
||||
@@ -1081,9 +1133,15 @@ pub enum HooksState {
|
||||
|
||||
pub fn hooks_state(target: &HookTarget, agent: HookAgent) -> HooksState {
|
||||
let path = agent.target_path(target);
|
||||
if agent == HookAgent::Muse {
|
||||
return muse_hooks_state(target);
|
||||
}
|
||||
if agent == HookAgent::Antigravity {
|
||||
return named_hook_set_state(target, &path, &antigravity_hook_set(target));
|
||||
}
|
||||
if agent == HookAgent::Jcode {
|
||||
return jcode_hooks_state(target, &path);
|
||||
}
|
||||
if let Some(events) = agent.toml_hook_events() {
|
||||
return toml_hooks_state(target, &path, agent, events);
|
||||
}
|
||||
@@ -1109,6 +1167,8 @@ pub enum HookOutcome {
|
||||
/// Installed on a remote, where `codex features enable hooks` still has to
|
||||
/// be run once by hand.
|
||||
InstalledEnableCodexThere,
|
||||
/// Bundle prepared on a remote; its own Muse CLI must install and approve it.
|
||||
MuseInstallManually(String),
|
||||
/// Installed, but running `codex features enable hooks` here failed.
|
||||
InstalledCodexEnableFailed(String),
|
||||
Removed,
|
||||
@@ -1120,10 +1180,17 @@ pub enum HookOutcome {
|
||||
|
||||
pub fn install_hooks(target: &HookTarget, agent: HookAgent) -> anyhow::Result<HookOutcome> {
|
||||
let path = agent.target_path(target);
|
||||
if agent == HookAgent::Muse {
|
||||
return muse_hooks_install(target, &path);
|
||||
}
|
||||
if agent == HookAgent::Antigravity {
|
||||
named_hook_set_install(target, &path, antigravity_hook_set(target))?;
|
||||
return Ok(HookOutcome::Installed);
|
||||
}
|
||||
if agent == HookAgent::Jcode {
|
||||
jcode_hooks_install(target, &path)?;
|
||||
return Ok(HookOutcome::Installed);
|
||||
}
|
||||
if let Some(events) = agent.toml_hook_events() {
|
||||
toml_hooks_install(target, &path, agent, events)?;
|
||||
return Ok(HookOutcome::Installed);
|
||||
@@ -1149,9 +1216,15 @@ pub fn install_hooks(target: &HookTarget, agent: HookAgent) -> anyhow::Result<Ho
|
||||
|
||||
pub fn uninstall_hooks(target: &HookTarget, agent: HookAgent) -> anyhow::Result<HookOutcome> {
|
||||
let path = agent.target_path(target);
|
||||
if agent == HookAgent::Muse {
|
||||
return muse_hooks_uninstall(target, &path);
|
||||
}
|
||||
if agent == HookAgent::Antigravity {
|
||||
return named_hook_set_uninstall(target, &path);
|
||||
}
|
||||
if agent == HookAgent::Jcode {
|
||||
return jcode_hooks_uninstall(target, &path);
|
||||
}
|
||||
if agent.toml_hook_events().is_some() {
|
||||
return toml_hooks_uninstall(target, &path, agent);
|
||||
}
|
||||
@@ -1268,6 +1341,16 @@ const GEMINI_HOOK_EVENTS: &[(&str, &str)] = &[
|
||||
("SessionEnd", "session-end"),
|
||||
];
|
||||
|
||||
/// Empryo uses Claude-compatible lifecycle hooks in ~/.empryo/hooks.json.
|
||||
const EMPRYO_HOOK_EVENTS: &[(&str, &str)] = &[
|
||||
("SessionStart", "session-start"),
|
||||
("UserPromptSubmit", "prompt-submit"),
|
||||
("PostToolUse", "tool-complete"),
|
||||
("Stop", "stop"),
|
||||
("StopFailure", "stop"),
|
||||
("SessionEnd", "session-end"),
|
||||
];
|
||||
|
||||
const DROID_HOOK_EVENTS: &[(&str, &str)] = &[
|
||||
("SessionStart", "session-start"),
|
||||
("UserPromptSubmit", "prompt-submit"),
|
||||
@@ -1290,6 +1373,18 @@ const QWEN_HOOK_EVENTS: &[(&str, &str)] = &[
|
||||
("SessionEnd", "session-end"),
|
||||
];
|
||||
|
||||
/// jcode's lifecycle keys under `[hooks]` in its `config.toml`, each holding
|
||||
/// one command or an ordered list of them. jcode runs these as detached
|
||||
/// observers and describes the event in `JCODE_HOOK_*` environment variables
|
||||
/// rather than on stdin — see [`hook_payload`].
|
||||
const JCODE_HOOK_EVENTS: &[(&str, &str)] = &[
|
||||
("turn_start", "prompt-submit"),
|
||||
("turn_end", "stop"),
|
||||
("session_start", "session-start"),
|
||||
("session_end", "session-end"),
|
||||
("post_tool", "tool-complete"),
|
||||
];
|
||||
|
||||
/// Kimi Code's hooks live as `[[hooks]]` entries in its main `config.toml` —
|
||||
/// the same file that holds the user's providers and models — so they go
|
||||
/// through the TOML merge strategy rather than a JSON map or an owned file.
|
||||
@@ -1568,6 +1663,318 @@ fn marker_hook<'a>(entry: &'a serde_json::Value, marker: &str) -> Option<&'a ser
|
||||
.find(|h| ours(h))
|
||||
}
|
||||
|
||||
const MUSE_PLUGIN_ID: &str = "tty7-presence";
|
||||
const MUSE_HOOK_EVENTS: &[(&str, &str, &str)] = &[
|
||||
("session-start", "SessionStart", "session-start"),
|
||||
("prompt-submit", "UserPromptSubmit", "prompt-submit"),
|
||||
(
|
||||
"permission-request",
|
||||
"PermissionRequest",
|
||||
"permission-request",
|
||||
),
|
||||
("tool-complete", "PostToolUse", "tool-complete"),
|
||||
("stop", "Stop", "stop"),
|
||||
("stop-failure", "StopFailure", "stop"),
|
||||
("interrupt", "Interrupt", "stop"),
|
||||
("session-end", "SessionEnd", "session-end"),
|
||||
];
|
||||
|
||||
fn muse_plugin_manifest(target: &HookTarget) -> serde_json::Value {
|
||||
let hooks: Vec<_> = MUSE_HOOK_EVENTS
|
||||
.iter()
|
||||
.map(|(id, event, tty7_event)| {
|
||||
serde_json::json!({
|
||||
"id": id, "event": event,
|
||||
"command": [target.exe.to_string_lossy(), "agent-hook", "muse", tty7_event],
|
||||
"timeoutMs": 5000, "async": *event == "Interrupt",
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
serde_json::json!({
|
||||
"schemaVersion": 1, "name": MUSE_PLUGIN_ID, "displayName": "tty7 presence",
|
||||
"version": "1.0.0", "description": "tty7 agent-hook muse presence",
|
||||
"compat": {"source": "native", "manifestDir": ".muse-plugin"},
|
||||
"capabilities": {"hooks": hooks},
|
||||
})
|
||||
}
|
||||
|
||||
/// Muse's native CLI owns its cache and approval records; do not write those directly.
|
||||
fn muse_cli(args: &[String]) -> anyhow::Result<serde_json::Value> {
|
||||
let mut cmd = std::process::Command::new("muse");
|
||||
cmd.env("MUSE_NO_AUTO_UPDATE", "1")
|
||||
.args(["plugins"])
|
||||
.args(args)
|
||||
.arg("--json");
|
||||
// Settings and `tty7 doctor` both wait on this; a Muse that hangs (a
|
||||
// login prompt, a stuck update check) must not take either down with it.
|
||||
let out = crate::core::proc::output_within(
|
||||
crate::core::proc::hide_console(&mut cmd),
|
||||
std::time::Duration::from_secs(30),
|
||||
)?;
|
||||
if !out.status.success() {
|
||||
anyhow::bail!(
|
||||
"muse plugins {} failed ({}): {}",
|
||||
args.first().map(String::as_str).unwrap_or(""),
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
Ok(serde_json::from_slice(&out.stdout)?)
|
||||
}
|
||||
|
||||
fn muse_inspected_state(target: &HookTarget, inspected: &serde_json::Value) -> HooksState {
|
||||
if inspected["plugin"]["id"] != MUSE_PLUGIN_ID {
|
||||
return HooksState::NotInstalled;
|
||||
}
|
||||
let expected = muse_plugin_manifest(target);
|
||||
let complete = inspected["active"] == true
|
||||
&& inspected["valid"] == true
|
||||
&& expected["capabilities"]["hooks"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|hook| {
|
||||
let declared = inspected["plugin"]["capabilities"]["hooks"]
|
||||
.as_array()
|
||||
.is_some_and(|hooks| {
|
||||
hooks.iter().any(|installed| {
|
||||
installed["id"] == hook["id"]
|
||||
&& installed["event"] == hook["event"]
|
||||
&& installed["command"] == hook["command"]
|
||||
&& installed["async"] == hook["async"]
|
||||
})
|
||||
});
|
||||
let approved =
|
||||
inspected["runtime_capabilities"]
|
||||
.as_array()
|
||||
.is_some_and(|entries| {
|
||||
entries.iter().any(|entry| {
|
||||
entry["candidate"]["kind"] == "hook"
|
||||
&& entry["candidate"]["capability_id"] == hook["id"]
|
||||
&& entry["status"] == "trusted_enabled"
|
||||
})
|
||||
});
|
||||
declared && approved
|
||||
});
|
||||
if complete {
|
||||
HooksState::Installed
|
||||
} else {
|
||||
HooksState::Outdated
|
||||
}
|
||||
}
|
||||
|
||||
fn muse_hooks_state(target: &HookTarget) -> HooksState {
|
||||
// A source bundle alone is not an installed plugin. Host has no remote exec API.
|
||||
if !target.is_local() {
|
||||
return HooksState::NotInstalled;
|
||||
}
|
||||
muse_cli(&["inspect".into(), MUSE_PLUGIN_ID.into()])
|
||||
.map(|value| muse_inspected_state(target, &value))
|
||||
.unwrap_or(HooksState::NotInstalled)
|
||||
}
|
||||
|
||||
fn muse_install_commands(bundle: &Path) -> String {
|
||||
format!(
|
||||
"muse plugins install \"{}\" --scope user && muse plugins approve {MUSE_PLUGIN_ID} && muse plugins enable {MUSE_PLUGIN_ID}",
|
||||
bundle.display()
|
||||
)
|
||||
}
|
||||
|
||||
fn muse_install_with(
|
||||
target: &HookTarget,
|
||||
path: &Path,
|
||||
mut run: impl FnMut(&[String]) -> anyhow::Result<serde_json::Value>,
|
||||
) -> anyhow::Result<HookOutcome> {
|
||||
let bundle = path
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.ok_or_else(|| anyhow::anyhow!("invalid Muse bundle path"))?;
|
||||
let content = serde_json::to_string_pretty(&muse_plugin_manifest(target))?;
|
||||
owned_file_install(target, path, &content, &HookAgent::Muse.marker())?;
|
||||
if !target.is_local() {
|
||||
return Ok(HookOutcome::MuseInstallManually(muse_install_commands(
|
||||
bundle,
|
||||
)));
|
||||
}
|
||||
run(&[
|
||||
"install".into(),
|
||||
bundle.to_string_lossy().into_owned(),
|
||||
"--scope".into(),
|
||||
"user".into(),
|
||||
])?;
|
||||
run(&["approve".into(), MUSE_PLUGIN_ID.into()])?;
|
||||
run(&["enable".into(), MUSE_PLUGIN_ID.into()])?;
|
||||
let inspected = run(&["inspect".into(), MUSE_PLUGIN_ID.into()])?;
|
||||
if muse_inspected_state(target, &inspected) != HooksState::Installed {
|
||||
anyhow::bail!("Muse plugin was not activated; inspect {MUSE_PLUGIN_ID} before retrying");
|
||||
}
|
||||
Ok(HookOutcome::Installed)
|
||||
}
|
||||
|
||||
fn muse_hooks_install(target: &HookTarget, path: &Path) -> anyhow::Result<HookOutcome> {
|
||||
muse_install_with(target, path, muse_cli)
|
||||
}
|
||||
|
||||
fn muse_hooks_uninstall(target: &HookTarget, path: &Path) -> anyhow::Result<HookOutcome> {
|
||||
if !target.is_local() {
|
||||
return Ok(HookOutcome::MuseInstallManually(format!(
|
||||
"muse plugins remove {MUSE_PLUGIN_ID}"
|
||||
)));
|
||||
}
|
||||
if muse_hooks_state(target) != HooksState::NotInstalled {
|
||||
muse_cli(&["remove".into(), MUSE_PLUGIN_ID.into()])?;
|
||||
}
|
||||
owned_file_uninstall(target, path, &HookAgent::Muse.marker())
|
||||
}
|
||||
|
||||
/// jcode accepts either one command or an ordered list (HookCommands).
|
||||
fn jcode_commands(item: &toml_edit::Item) -> anyhow::Result<Vec<String>> {
|
||||
if let Some(command) = item.as_str() {
|
||||
return Ok(vec![command.to_owned()]);
|
||||
}
|
||||
if let Some(array) = item.as_array() {
|
||||
return array
|
||||
.iter()
|
||||
.map(|value| {
|
||||
value.as_str().map(str::to_owned).ok_or_else(|| {
|
||||
anyhow::anyhow!("jcode hook commands must be strings; not touching it")
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
anyhow::bail!("jcode hook must be a string or an array of strings; not touching it")
|
||||
}
|
||||
|
||||
fn set_jcode_commands(table: &mut toml_edit::Table, key: &str, commands: &[String]) {
|
||||
match commands {
|
||||
[] => {
|
||||
table.remove(key);
|
||||
}
|
||||
[command] => {
|
||||
table[key] = toml_edit::value(command.clone());
|
||||
}
|
||||
_ => {
|
||||
let mut array = toml_edit::Array::new();
|
||||
for command in commands {
|
||||
array.push(command.as_str());
|
||||
}
|
||||
table[key] = toml_edit::value(array);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn jcode_hooks_state(target: &HookTarget, path: &Path) -> HooksState {
|
||||
let Ok(text) = target.read(path) else {
|
||||
return HooksState::NotInstalled;
|
||||
};
|
||||
let Ok(doc) = text.parse::<toml_edit::DocumentMut>() else {
|
||||
return HooksState::NotInstalled;
|
||||
};
|
||||
let Some(table) = doc.get("hooks").and_then(|item| item.as_table()) else {
|
||||
return HooksState::NotInstalled;
|
||||
};
|
||||
let marker = HookAgent::Jcode.marker();
|
||||
let commands: Vec<_> = JCODE_HOOK_EVENTS
|
||||
.iter()
|
||||
.map(|(key, _)| {
|
||||
table
|
||||
.get(*key)
|
||||
.map(jcode_commands)
|
||||
.transpose()
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect();
|
||||
let ours = commands
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|command| command.contains(&marker));
|
||||
let complete = JCODE_HOOK_EVENTS
|
||||
.iter()
|
||||
.zip(&commands)
|
||||
.all(|((_, event), list)| {
|
||||
let expected = target.hook_command(HookAgent::Jcode, event);
|
||||
let marked: Vec<_> = list
|
||||
.iter()
|
||||
.filter(|command| command.contains(&marker))
|
||||
.collect();
|
||||
marked.len() == 1 && marked[0] == &expected
|
||||
});
|
||||
if complete {
|
||||
HooksState::Installed
|
||||
} else if ours {
|
||||
HooksState::Outdated
|
||||
} else {
|
||||
HooksState::NotInstalled
|
||||
}
|
||||
}
|
||||
|
||||
fn jcode_hooks_install(target: &HookTarget, path: &Path) -> anyhow::Result<()> {
|
||||
let mut doc: toml_edit::DocumentMut = match target.read(path) {
|
||||
Ok(text) => text.parse().map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"{} is not valid TOML ({e}); not touching it",
|
||||
path.display()
|
||||
)
|
||||
})?,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => toml_edit::DocumentMut::new(),
|
||||
Err(e) => return Err(anyhow::anyhow!("read {}: {e}", path.display())),
|
||||
};
|
||||
let hooks = doc.entry("hooks").or_insert(toml_edit::table());
|
||||
let table = hooks.as_table_mut().ok_or_else(|| {
|
||||
anyhow::anyhow!("{} hooks is not a table; not touching it", path.display())
|
||||
})?;
|
||||
let marker = HookAgent::Jcode.marker();
|
||||
for (key, event) in JCODE_HOOK_EVENTS {
|
||||
let mut commands = table
|
||||
.get(*key)
|
||||
.map(jcode_commands)
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
commands.retain(|command| !command.contains(&marker));
|
||||
commands.push(target.hook_command(HookAgent::Jcode, event));
|
||||
set_jcode_commands(table, key, &commands);
|
||||
}
|
||||
target.write(path, doc.to_string().as_bytes())
|
||||
}
|
||||
|
||||
fn jcode_hooks_uninstall(target: &HookTarget, path: &Path) -> anyhow::Result<HookOutcome> {
|
||||
let text = match target.read(path) {
|
||||
Ok(text) => text,
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(HookOutcome::NothingInstalled),
|
||||
Err(e) => return Err(anyhow::anyhow!("read {}: {e}", path.display())),
|
||||
};
|
||||
let mut doc: toml_edit::DocumentMut = text.parse().map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"{} is not valid TOML ({e}); not touching it",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let Some(table) = doc.get_mut("hooks").and_then(|v| v.as_table_mut()) else {
|
||||
return Ok(HookOutcome::NoTty7Hooks);
|
||||
};
|
||||
let marker = HookAgent::Jcode.marker();
|
||||
let mut removed = false;
|
||||
for (key, _) in JCODE_HOOK_EVENTS {
|
||||
let Some(item) = table.get(*key) else {
|
||||
continue;
|
||||
};
|
||||
let mut commands = jcode_commands(item)?;
|
||||
let before = commands.len();
|
||||
commands.retain(|command| !command.contains(&marker));
|
||||
if commands.len() != before {
|
||||
set_jcode_commands(table, key, &commands);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
if !removed {
|
||||
return Ok(HookOutcome::NoTty7Hooks);
|
||||
}
|
||||
target.write(path, doc.to_string().as_bytes())?;
|
||||
Ok(HookOutcome::Removed)
|
||||
}
|
||||
|
||||
fn toml_hooks_state(
|
||||
target: &HookTarget,
|
||||
path: &Path,
|
||||
@@ -1873,7 +2280,10 @@ fn owned_file_content(target: &HookTarget, agent: HookAgent) -> Option<String> {
|
||||
| HookAgent::CodeBuddy
|
||||
| HookAgent::Cursor
|
||||
| HookAgent::Antigravity
|
||||
| HookAgent::Kimi => None,
|
||||
| HookAgent::Kimi
|
||||
| HookAgent::Empryo
|
||||
| HookAgent::Jcode
|
||||
| HookAgent::Muse => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2732,6 +3142,8 @@ mod tests {
|
||||
(HookAgent::Crush, "/home/me/.config/crush/crush.json"),
|
||||
(HookAgent::CodeBuddy, "/home/me/.codebuddy/settings.json"),
|
||||
(HookAgent::Cursor, "/home/me/.cursor/hooks.json"),
|
||||
(HookAgent::Empryo, "/home/me/.empryo/hooks.json"),
|
||||
(HookAgent::Jcode, "/home/me/.jcode/config.toml"),
|
||||
(
|
||||
HookAgent::PrimeAgent,
|
||||
"/home/me/.prime/agent/extensions/tty7/index.ts",
|
||||
@@ -3553,6 +3965,8 @@ mod tests {
|
||||
(HookAgent::Crush, "/home/me/.config/crush/crush.json"),
|
||||
(HookAgent::CodeBuddy, "/home/me/.codebuddy/settings.json"),
|
||||
(HookAgent::Cursor, "/home/me/.cursor/hooks.json"),
|
||||
(HookAgent::Empryo, "/home/me/.empryo/hooks.json"),
|
||||
(HookAgent::Jcode, "/home/me/.jcode/config.toml"),
|
||||
] {
|
||||
assert_eq!(
|
||||
agent.target_path(&target),
|
||||
@@ -3590,6 +4004,244 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
fn installed_muse_fixture(target: &HookTarget) -> serde_json::Value {
|
||||
let mut hooks = muse_plugin_manifest(target)["capabilities"]["hooks"].clone();
|
||||
for hook in hooks.as_array_mut().unwrap() {
|
||||
let timeout = hook.as_object_mut().unwrap().remove("timeoutMs").unwrap();
|
||||
hook["timeout_ms"] = timeout;
|
||||
}
|
||||
let runtime: Vec<_> = MUSE_HOOK_EVENTS
|
||||
.iter()
|
||||
.map(|(id, _, _)| {
|
||||
serde_json::json!({
|
||||
"candidate": {"kind": "hook", "capability_id": id}, "status": "trusted_enabled"
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
serde_json::json!({"active": true, "valid": true, "plugin": {"id": MUSE_PLUGIN_ID,
|
||||
"capabilities": {"hooks": hooks}}, "runtime_capabilities": runtime})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn muse_install_uses_native_cli_and_requires_approved_hooks() {
|
||||
let host = local_host();
|
||||
let dir = std::env::temp_dir().join(format!("tty7-muse-install-{}", std::process::id()));
|
||||
let target = HookTarget {
|
||||
host: &*host,
|
||||
home: dir.clone(),
|
||||
exe: PathBuf::from("/tty7 bin/tty7-app"),
|
||||
};
|
||||
let path = HookAgent::Muse.target_path(&target);
|
||||
let fixture = installed_muse_fixture(&target);
|
||||
let mut calls = Vec::new();
|
||||
let outcome = muse_install_with(&target, &path, |args| {
|
||||
calls.push(args.to_vec());
|
||||
Ok(if args[0] == "inspect" {
|
||||
fixture.clone()
|
||||
} else {
|
||||
serde_json::json!({})
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(outcome, HookOutcome::Installed);
|
||||
let bundle = path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
assert_eq!(
|
||||
calls,
|
||||
vec![
|
||||
vec![
|
||||
"install".to_owned(),
|
||||
bundle,
|
||||
"--scope".into(),
|
||||
"user".into()
|
||||
],
|
||||
vec!["approve".into(), "tty7-presence".into()],
|
||||
vec!["enable".into(), "tty7-presence".into()],
|
||||
vec!["inspect".into(), "tty7-presence".into()],
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
muse_inspected_state(&target, &fixture),
|
||||
HooksState::Installed
|
||||
);
|
||||
let mut unapproved = fixture.clone();
|
||||
unapproved["runtime_capabilities"][0]["status"] = "needs_review".into();
|
||||
assert_eq!(
|
||||
muse_inspected_state(&target, &unapproved),
|
||||
HooksState::Outdated
|
||||
);
|
||||
assert!(
|
||||
muse_install_with(&target, &path, |args| {
|
||||
if args[0] == "approve" {
|
||||
anyhow::bail!("approval failed");
|
||||
}
|
||||
Ok(serde_json::json!({}))
|
||||
})
|
||||
.is_err(),
|
||||
"a failed approval is not reported as installed"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn muse_remote_bundle_requires_manual_install_and_preserves_session_ids() {
|
||||
let host = FakeRemote::shared();
|
||||
let dir = std::env::temp_dir().join(format!("tty7-muse-remote-{}", std::process::id()));
|
||||
let target = HookTarget::remote(&*host, dir.clone());
|
||||
let path = HookAgent::Muse.target_path(&target);
|
||||
let outcome = muse_install_with(&target, &path, |_| {
|
||||
panic!("must not run local Muse for a remote")
|
||||
})
|
||||
.unwrap();
|
||||
let HookOutcome::MuseInstallManually(command) = outcome else {
|
||||
panic!("manual installation required");
|
||||
};
|
||||
assert!(command.contains("muse plugins install"));
|
||||
assert!(command.contains("muse plugins approve tty7-presence"));
|
||||
assert_eq!(
|
||||
hooks_state(&target, HookAgent::Muse),
|
||||
HooksState::NotInstalled
|
||||
);
|
||||
let manifest: serde_json::Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
let hooks = manifest["capabilities"]["hooks"].as_array().unwrap();
|
||||
assert_eq!(hooks.len(), 8);
|
||||
let interrupt = hooks
|
||||
.iter()
|
||||
.find(|hook| hook["event"] == "Interrupt")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
interrupt["async"], true,
|
||||
"Muse admits only asynchronous cancellation hooks"
|
||||
);
|
||||
let permission = hooks
|
||||
.iter()
|
||||
.find(|hook| hook["event"] == "PermissionRequest")
|
||||
.unwrap();
|
||||
assert_eq!(permission["command"][3], "permission-request");
|
||||
let event = round_trip(
|
||||
"muse",
|
||||
"session-start",
|
||||
r#"{"session_id":"muse-session-42","cwd":"/workspace"}"#,
|
||||
);
|
||||
assert_eq!(event.agent, Some(CLIAgent::Muse));
|
||||
assert_eq!(event.session_id.as_deref(), Some("muse-session-42"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empryo_hooks_merge_with_existing_lifecycle_hooks() {
|
||||
let dir = std::env::temp_dir().join(format!("tty7-empryo-hooks-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let host = FakeRemote::shared();
|
||||
let target = HookTarget::remote(&*host, dir.clone());
|
||||
let path = HookAgent::Empryo.target_path(&target);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&path, r#"{"hooks":{"SessionStart":[]}}"#).unwrap();
|
||||
assert_eq!(
|
||||
hooks_state(&target, HookAgent::Empryo),
|
||||
HooksState::NotInstalled
|
||||
);
|
||||
install_hooks(&target, HookAgent::Empryo).expect("install succeeds");
|
||||
assert_eq!(
|
||||
hooks_state(&target, HookAgent::Empryo),
|
||||
HooksState::Installed
|
||||
);
|
||||
let root: serde_json::Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert!(root["hooks"]["SessionStart"].as_array().unwrap().len() >= 1);
|
||||
assert!(
|
||||
root["hooks"]["SessionEnd"][0]["hooks"][0]["command"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.ends_with("agent-hook empryo session-end")
|
||||
);
|
||||
install_hooks(&target, HookAgent::Empryo).expect("reinstall succeeds");
|
||||
assert_eq!(
|
||||
hooks_state(&target, HookAgent::Empryo),
|
||||
HooksState::Installed
|
||||
);
|
||||
assert_eq!(
|
||||
uninstall_hooks(&target, HookAgent::Empryo).unwrap(),
|
||||
HookOutcome::Removed
|
||||
);
|
||||
assert_eq!(
|
||||
uninstall_hooks(&target, HookAgent::Empryo).unwrap(),
|
||||
HookOutcome::NoTty7Hooks
|
||||
);
|
||||
assert!(path.exists(), "uninstall preserves the shared config file");
|
||||
let root: serde_json::Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(root, serde_json::json!({"hooks": {}}));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_jcode_report_reads_its_session_from_the_environment() {
|
||||
let env = |k: &str| {
|
||||
(k == "JCODE_HOOK_PAYLOAD").then(|| {
|
||||
r#"{"event":"session_start","session_id":"j-7","cwd":"/work"}"#.to_string()
|
||||
})
|
||||
};
|
||||
let input = hook_payload("jcode", String::new(), env);
|
||||
let event = round_trip("jcode", "session-start", &input);
|
||||
assert_eq!(event.agent, Some(CLIAgent::Jcode));
|
||||
assert_eq!(event.session_id.as_deref(), Some("j-7"));
|
||||
assert_eq!(event.cwd.as_deref(), Some(Path::new("/work")));
|
||||
// Only jcode is read from there, and stdin still wins when it has
|
||||
// something to say.
|
||||
assert_eq!(hook_payload("claude", String::new(), env), "");
|
||||
assert_eq!(hook_payload("jcode", "{}".into(), env), "{}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jcode_hooks_use_native_config_lifecycle_keys() {
|
||||
let dir = std::env::temp_dir().join(format!("tty7-jcode-hooks-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let host = FakeRemote::shared();
|
||||
let target = HookTarget::remote(&*host, dir.clone());
|
||||
let path = HookAgent::Jcode.target_path(&target);
|
||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
std::fs::write(&path, "[hooks]\nturn_start = \"user-hook\"\n").unwrap();
|
||||
install_hooks(&target, HookAgent::Jcode).expect("install succeeds");
|
||||
assert_eq!(
|
||||
hooks_state(&target, HookAgent::Jcode),
|
||||
HooksState::Installed
|
||||
);
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
let doc: toml_edit::DocumentMut = text.parse().unwrap();
|
||||
let commands = jcode_commands(&doc["hooks"]["turn_start"]).unwrap();
|
||||
assert_eq!(
|
||||
commands,
|
||||
vec![
|
||||
"user-hook".to_owned(),
|
||||
target.hook_command(HookAgent::Jcode, "prompt-submit")
|
||||
]
|
||||
);
|
||||
install_hooks(&target, HookAgent::Jcode).expect("reinstall succeeds");
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(&path).unwrap(),
|
||||
text,
|
||||
"reinstall adds no duplicates"
|
||||
);
|
||||
assert!(text.contains("agent-hook jcode prompt-submit"));
|
||||
assert_eq!(
|
||||
uninstall_hooks(&target, HookAgent::Jcode).unwrap(),
|
||||
HookOutcome::Removed
|
||||
);
|
||||
assert!(
|
||||
std::fs::read_to_string(&path)
|
||||
.unwrap()
|
||||
.contains("user-hook")
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn antigravity_owns_a_named_hook_set_beside_the_users() {
|
||||
let dir = std::env::temp_dir().join(format!("tty7-agy-hooks-{}", std::process::id()));
|
||||
|
||||
@@ -32,10 +32,12 @@ pub enum CLIAgent {
|
||||
Empryo,
|
||||
PrimeAgent,
|
||||
QoderCLICn,
|
||||
Muse,
|
||||
Jcode,
|
||||
}
|
||||
|
||||
impl CLIAgent {
|
||||
pub const ALL: [CLIAgent; 26] = [
|
||||
pub const ALL: [CLIAgent; 28] = [
|
||||
CLIAgent::Claude,
|
||||
CLIAgent::Codex,
|
||||
CLIAgent::TraeCode,
|
||||
@@ -62,6 +64,8 @@ impl CLIAgent {
|
||||
CLIAgent::CodeBuddy,
|
||||
CLIAgent::Empryo,
|
||||
CLIAgent::PrimeAgent,
|
||||
CLIAgent::Muse,
|
||||
CLIAgent::Jcode,
|
||||
];
|
||||
|
||||
fn aliases(self) -> &'static [&'static str] {
|
||||
@@ -119,6 +123,8 @@ impl CLIAgent {
|
||||
// avatar until it exits is the cost of catching the short name.
|
||||
CLIAgent::CodeBuddy => &["codebuddy", "codebuddy-code", "cbc"],
|
||||
CLIAgent::Empryo => &["empryo"],
|
||||
CLIAgent::Muse => &["muse"],
|
||||
CLIAgent::Jcode => &["jcode"],
|
||||
CLIAgent::PrimeAgent => &["prime-agent"],
|
||||
}
|
||||
}
|
||||
@@ -149,6 +155,8 @@ impl CLIAgent {
|
||||
CLIAgent::Crush => "crush",
|
||||
CLIAgent::CodeBuddy => "codebuddy",
|
||||
CLIAgent::Empryo => "empryo",
|
||||
CLIAgent::Muse => "muse",
|
||||
CLIAgent::Jcode => "jcode",
|
||||
CLIAgent::PrimeAgent => "prime-agent",
|
||||
CLIAgent::QoderCLICn => "qoderclicn",
|
||||
}
|
||||
@@ -185,6 +193,8 @@ impl CLIAgent {
|
||||
CLIAgent::Crush => "Crush",
|
||||
CLIAgent::CodeBuddy => "CodeBuddy",
|
||||
CLIAgent::Empryo => "Empryo",
|
||||
CLIAgent::Muse => "Muse Code",
|
||||
CLIAgent::Jcode => "jcode",
|
||||
CLIAgent::PrimeAgent => "Prime Agent",
|
||||
CLIAgent::QoderCLICn => "Qoder CN CLI",
|
||||
}
|
||||
@@ -226,6 +236,9 @@ impl CLIAgent {
|
||||
CLIAgent::Crush => Some(format!("crush{flags} --session {session_id}")),
|
||||
CLIAgent::CodeBuddy => Some(format!("codebuddy{flags} --resume {session_id}")),
|
||||
CLIAgent::Empryo => Some(format!("empryo{flags} --session {session_id}")),
|
||||
// Root options may sit on either side of `resume`.
|
||||
CLIAgent::Muse => Some(format!("muse{flags} resume {session_id}")),
|
||||
CLIAgent::Jcode => Some(format!("jcode{flags} --resume {session_id}")),
|
||||
CLIAgent::PrimeAgent => Some(format!("prime-agent{flags} --resume {session_id}")),
|
||||
_ => None,
|
||||
}
|
||||
@@ -417,7 +430,7 @@ impl CLIAgent {
|
||||
let named = argv.iter().position(|t| names_self(t))?;
|
||||
let mut tail: Vec<&str> = argv[named + 1..].iter().map(String::as_str).collect();
|
||||
|
||||
if matches!(self, CLIAgent::Codex | CLIAgent::TraeCode)
|
||||
if matches!(self, CLIAgent::Codex | CLIAgent::TraeCode | CLIAgent::Muse)
|
||||
&& matches!(tail.first(), Some(&"resume") | Some(&"fork"))
|
||||
{
|
||||
tail.remove(0);
|
||||
@@ -517,6 +530,9 @@ impl CLIAgent {
|
||||
// empryo resumes with `--session <id>`; `--save-session` only
|
||||
// applies to headless runs and is not a session selector.
|
||||
CLIAgent::Empryo => &["--session"],
|
||||
// `muse resume --last` picks the newest session instead of the id.
|
||||
CLIAgent::Muse => &["--last"],
|
||||
CLIAgent::Jcode => &["--resume"],
|
||||
// `--resume`/`-r` is Kimi's hidden alias for `--session`/`-S`.
|
||||
// `--agent`/`--agent-file` bind the main agent at session creation
|
||||
// and Kimi rejects either next to `--session` outright; resuming
|
||||
@@ -635,6 +651,8 @@ impl CLIAgent {
|
||||
// The blue-violet field Charm ships the Crush heart on.
|
||||
CLIAgent::Crush => 0x6B50FF,
|
||||
CLIAgent::Empryo => 0xE8663D,
|
||||
CLIAgent::Muse => 0x0866FF,
|
||||
CLIAgent::Jcode => 0x111111,
|
||||
CLIAgent::PrimeAgent => 0x111111,
|
||||
// The near-black field CodeBuddy's own app icon sits on.
|
||||
CLIAgent::CodeBuddy => 0x1F1F1F,
|
||||
@@ -688,11 +706,20 @@ impl CLIAgent {
|
||||
| CLIAgent::Vibe
|
||||
| CLIAgent::Antigravity
|
||||
| CLIAgent::Empryo
|
||||
| CLIAgent::PrimeAgent => "icons/bot.svg",
|
||||
| CLIAgent::PrimeAgent
|
||||
| CLIAgent::Muse
|
||||
| CLIAgent::Jcode => "icons/bot.svg",
|
||||
}
|
||||
}
|
||||
|
||||
fn match_token(token: &str) -> Option<CLIAgent> {
|
||||
// Muse Code's `muse` launcher execs a versioned binary,
|
||||
// `muse-bin-<version>`, so that is the name the process runs under.
|
||||
let token = if token.starts_with("muse-bin-") {
|
||||
"muse"
|
||||
} else {
|
||||
token
|
||||
};
|
||||
CLIAgent::ALL
|
||||
.into_iter()
|
||||
.find(|a| a.aliases().contains(&token))
|
||||
@@ -1535,7 +1562,9 @@ mod tests {
|
||||
"vibe",
|
||||
"antigravity",
|
||||
"empryo",
|
||||
"prime-agent"
|
||||
"prime-agent",
|
||||
"muse",
|
||||
"jcode"
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
@@ -1552,6 +1581,54 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn muse_and_jcode_are_detected_and_resume() {
|
||||
assert_eq!(
|
||||
CLIAgent::detect_from_argv(&argv(&["muse", "--yolo"])),
|
||||
Some(CLIAgent::Muse)
|
||||
);
|
||||
// The `muse` launcher execs a versioned binary.
|
||||
assert_eq!(
|
||||
CLIAgent::detect_from_argv(&argv(&[
|
||||
"/home/me/.local/share/muse/muse-bin-1.4.2",
|
||||
"--yolo"
|
||||
])),
|
||||
Some(CLIAgent::Muse)
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::detect_from_argv(&argv(&["/home/me/.jcode/builds/stable/jcode"])),
|
||||
Some(CLIAgent::Jcode)
|
||||
);
|
||||
assert_eq!(CLIAgent::from_slug("muse"), Some(CLIAgent::Muse));
|
||||
assert_eq!(CLIAgent::from_slug("jcode"), Some(CLIAgent::Jcode));
|
||||
assert_eq!(
|
||||
CLIAgent::Muse
|
||||
.resume_command("s-1", Some(&argv(&["muse", "--yolo"])))
|
||||
.as_deref(),
|
||||
Some("muse --yolo resume s-1")
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::Muse
|
||||
.resume_command("s-1", Some(&argv(&["muse", "resume", "old", "--yolo"])))
|
||||
.as_deref(),
|
||||
Some("muse --yolo resume s-1")
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::Muse
|
||||
.resume_command("s-1", Some(&argv(&["muse", "resume", "--last", "--yolo"])))
|
||||
.as_deref(),
|
||||
Some("muse --yolo resume s-1")
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::Jcode
|
||||
.resume_command("s-1", Some(&argv(&["jcode", "--resume", "old"])))
|
||||
.as_deref(),
|
||||
Some("jcode --resume s-1")
|
||||
);
|
||||
assert_eq!(CLIAgent::Muse.fork_command("s-1", None), None);
|
||||
assert_eq!(CLIAgent::Jcode.fork_command("s-1", None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empryo_and_prime_agent_are_detected_and_resume() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -2563,12 +2563,13 @@ impl DaemonPane {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This pane as a Codex report sees it, for [`codex_report_target`].
|
||||
pub fn codex_candidate(&self) -> CodexCandidate {
|
||||
/// This pane as a report from `agent`'s shared server sees it, for
|
||||
/// [`codex_report_target`].
|
||||
pub fn codex_candidate(&self, agent: crate::core::cli_agent::CLIAgent) -> CodexCandidate {
|
||||
let st = self.state.lock().unwrap();
|
||||
CodexCandidate {
|
||||
pane: self.id,
|
||||
runs_codex: st.alive && st.agent == Some(crate::core::cli_agent::CLIAgent::Codex),
|
||||
runs_codex: st.alive && st.agent == Some(agent),
|
||||
cwd: st.cwd.clone(),
|
||||
session: st.agent_session.as_ref().and_then(|s| s.session_id.clone()),
|
||||
}
|
||||
@@ -3614,6 +3615,11 @@ pub struct CodexCandidate {
|
||||
/// started (`cleared`) goes to one that had a session before and any other to
|
||||
/// one yet to report, and the named pane breaks a tie that still stands.
|
||||
/// `None` is no pane, or no telling which.
|
||||
///
|
||||
/// jcode is built the same way — its sessions share one background server
|
||||
/// that runs every session's hooks with the environment of whichever client
|
||||
/// started it — so its reports take the same road, with `runs_codex` meaning
|
||||
/// "runs jcode".
|
||||
pub fn codex_report_target(
|
||||
panes: &[CodexCandidate],
|
||||
named: u64,
|
||||
|
||||
@@ -126,21 +126,22 @@ impl crate::host::server::PaneDirectory for Registry {
|
||||
/// so being up to a minute late to notice costs nothing.
|
||||
const AGENT_STALE_SWEEP_INTERVAL: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
|
||||
/// The pane a Codex hook's report is about, found from the report itself —
|
||||
/// where a hook runs says nothing for Codex (see
|
||||
/// [`crate::daemon::pane::codex_report_target`]). `None` for any other agent's
|
||||
/// report, which goes to the pane the hook named.
|
||||
/// The pane a Codex or jcode hook's report is about, found from the report
|
||||
/// itself — where a hook runs says nothing for an agent that runs its hooks in
|
||||
/// a shared server (see [`crate::daemon::pane::codex_report_target`]). `None`
|
||||
/// for any other agent's report, which goes to the pane the hook named.
|
||||
fn codex_report_pane(
|
||||
registry: &Registry,
|
||||
named: u64,
|
||||
body: &str,
|
||||
) -> Option<(Arc<DaemonPane>, crate::core::cli_agent::AgentEvent)> {
|
||||
let event = crate::core::cli_agent::parse_agent_event_body(body.as_bytes())?;
|
||||
if event.agent != Some(crate::core::cli_agent::CLIAgent::Codex) {
|
||||
return None;
|
||||
}
|
||||
use crate::core::cli_agent::CLIAgent;
|
||||
let agent = event
|
||||
.agent
|
||||
.filter(|a| matches!(a, CLIAgent::Codex | CLIAgent::Jcode))?;
|
||||
let panes = registry.all();
|
||||
let candidates: Vec<_> = panes.iter().map(|p| p.codex_candidate()).collect();
|
||||
let candidates: Vec<_> = panes.iter().map(|p| p.codex_candidate(agent)).collect();
|
||||
let target = crate::daemon::pane::codex_report_target(
|
||||
&candidates,
|
||||
named,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Coding agents"
|
||||
description: "What tty7 does around Claude Code, Codex, TraeCode, and 21 others — without ever wrapping them."
|
||||
description: "What tty7 does around Claude Code, Codex, TraeCode, and 25 others — without ever wrapping them."
|
||||
---
|
||||
|
||||
tty7 recognises coding agents running in a pane and builds around them. It does
|
||||
@@ -15,7 +15,7 @@ need, and what changed.
|
||||
|
||||
## Which agents
|
||||
|
||||
Twenty-six CLIs are recognised on sight, by the command running in the pane:
|
||||
Twenty-eight CLIs are recognised on sight, by the command running in the pane:
|
||||
|
||||
| Agent | Command |
|
||||
|---|---|
|
||||
@@ -45,6 +45,8 @@ Twenty-six CLIs are recognised on sight, by the command running in the pane:
|
||||
| Oh My Pi | `omp` |
|
||||
| Prime Agent | `prime-agent` |
|
||||
| Empryo | `empryo` |
|
||||
| Muse Code | `muse`, `muse-bin-<version>` |
|
||||
| jcode | `jcode` |
|
||||
|
||||
Detection sees through the usual disguises: a full path, a `.cmd` or `.exe` on
|
||||
Windows, leading environment assignments, and an interpreter in front
|
||||
@@ -67,7 +69,7 @@ If you launch agents through a wrapper script, map its name to an agent in
|
||||
The key is your command's name; the value is one of the slugs above (`claude`,
|
||||
`codex`, `traecli`, `gemini`, `aider`, `amp`, `opencode`, `copilot`, `cursor`, `goose`,
|
||||
`droid`, `pi`, `auggie`, `hermes`, `vibe`, `antigravity`, `grok`, `qwen`,
|
||||
`omp`, `kimi`, `qodercli`, `qoderclicn`, `crush`, `codebuddy`, `empryo`, `prime-agent`).
|
||||
`omp`, `kimi`, `qodercli`, `qoderclicn`, `crush`, `codebuddy`, `empryo`, `prime-agent`, `muse`, `jcode`).
|
||||
|
||||
## Quick launch
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ the agent say which one it is.
|
||||
|
||||
| Agent | |
|
||||
|---|---|
|
||||
| Claude Code · Codex · TraeCode · Copilot CLI · OpenCode · Pi · Grok Build · Oh My Pi · Gemini · Droid · Qwen Code · Goose · Kimi Code · Qoder CLI · Qoder CN CLI · Crush · CodeBuddy · Cursor CLI · Prime Agent · Antigravity | Hooks available |
|
||||
| Aider · Amp · Auggie · Hermes · Vibe · Empryo | Detected and labelled, but no status channel yet |
|
||||
| Claude Code · Codex · TraeCode · Copilot CLI · OpenCode · Pi · Grok Build · Oh My Pi · Gemini · Droid · Qwen Code · Goose · Kimi Code · Qoder CLI · Qoder CN CLI · Crush · CodeBuddy · Cursor CLI · Prime Agent · Antigravity · Empryo · Muse Code · jcode | Hooks available |
|
||||
| Aider · Amp · Auggie · Hermes · Vibe | Detected and labelled, but no status channel yet |
|
||||
|
||||
Installing writes into that agent's own configuration directory. Once installed
|
||||
the row grows a second **Uninstall** button beside the first, which itself
|
||||
|
||||
+1
-1
@@ -436,7 +436,7 @@ export const FEATURES = [
|
||||
p: "Ghost suggestions from your history, tab completion that explains each flag, syntax highlighting, click-to-place caret, real multi-line editing." },
|
||||
{ t: "Agent-aware", href: "/agents/overview",
|
||||
d: "M8 8h8a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-5a3 3 0 0 1 3-3zM12 4v4M9.5 13h.01M14.5 13h.01",
|
||||
p: "26 coding CLIs recognised on sight. Per-pane status dots, a notification when one needs you, and session resume after a reboot." },
|
||||
p: "28 coding CLIs recognised on sight. Per-pane status dots, a notification when one needs you, and session resume after a reboot." },
|
||||
{ t: "Remote work that feels local", href: "/remote/workspaces",
|
||||
d: "M5 4h14a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM5 13h14a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zM7 7.5h.01M7 16.5h.01",
|
||||
p: "A native Rust SSH stack with profiles, SFTP, and port forwarding — plus remote workspaces where files, repos, and panes stay on the far machine." },
|
||||
|
||||
@@ -8590,6 +8590,10 @@ impl Tty7App {
|
||||
use crate::core::agent_hooks::HookOutcome as O;
|
||||
match outcome {
|
||||
O::Installed => t(L10nKey::AppAgentHooksInstalled).to_string(),
|
||||
O::MuseInstallManually(command) => t_fmt(
|
||||
L10nKey::AppAgentHooksMuseManualInstall,
|
||||
&[("command", command)],
|
||||
),
|
||||
O::InstalledEnableCodexThere => {
|
||||
t(L10nKey::AppAgentHooksInstalledEnableCodexThere).to_string()
|
||||
}
|
||||
|
||||
@@ -924,6 +924,11 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::SettingsAgentCursorCli => "Cursor CLI",
|
||||
L10nKey::SettingsAgentPrimeAgent => "Prime Agent",
|
||||
L10nKey::SettingsAgentAntigravity => "Antigravity",
|
||||
L10nKey::SettingsAgentEmpryo => "Empryo",
|
||||
L10nKey::SettingsAgentJcode => "jcode",
|
||||
L10nKey::SettingsAgentMuse => "Muse Code",
|
||||
L10nKey::SettingsMuseManualInstall => "Manual install required",
|
||||
L10nKey::AppAgentHooksMuseManualInstall => "Run on the remote host: {command}",
|
||||
L10nKey::SettingsAgentQoderCn => "Qoder CN CLI",
|
||||
L10nKey::SettingsSearchAboutKeywords => "version license credits build update check github",
|
||||
L10nKey::SettingsSearchAppHttpProxyKeywords => {
|
||||
@@ -1040,6 +1045,9 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::SettingsSearchPrimeAgentKeywords => {
|
||||
"agent integration extension install prime prime-agent primeintellect"
|
||||
}
|
||||
L10nKey::SettingsSearchEmpryoKeywords => "agent integration hooks install empryo",
|
||||
L10nKey::SettingsSearchJcodeKeywords => "agent integration hooks install jcode",
|
||||
L10nKey::SettingsSearchMuseKeywords => "agent integration hooks plugins install muse meta",
|
||||
L10nKey::SettingsSearchAntigravityKeywords => {
|
||||
"agent integration hooks install antigravity agy google"
|
||||
}
|
||||
|
||||
@@ -927,6 +927,11 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsAgentCursorCli => "Cursor CLI",
|
||||
L10nKey::SettingsAgentPrimeAgent => "Prime Agent",
|
||||
L10nKey::SettingsAgentAntigravity => "Antigravity",
|
||||
L10nKey::SettingsAgentEmpryo => "Empryo",
|
||||
L10nKey::SettingsAgentJcode => "jcode",
|
||||
L10nKey::SettingsAgentMuse => "Muse Code",
|
||||
L10nKey::SettingsMuseManualInstall => "手動インストールが必要",
|
||||
L10nKey::AppAgentHooksMuseManualInstall => "対象のマシンで実行: {command}",
|
||||
L10nKey::SettingsAgentQoderCn => "Qoder CN CLI",
|
||||
L10nKey::SettingsSearchAboutKeywords => {
|
||||
"バージョン ライセンス クレジット ビルド 更新 確認 github about version license credits update check"
|
||||
@@ -1089,6 +1094,15 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsSearchPrimeAgentKeywords => {
|
||||
"エージェント 統合 拡張 インストール prime prime-agent primeintellect agent integration extension install"
|
||||
}
|
||||
L10nKey::SettingsSearchEmpryoKeywords => {
|
||||
"エージェント 統合 フック インストール empryo agent integration hooks install"
|
||||
}
|
||||
L10nKey::SettingsSearchJcodeKeywords => {
|
||||
"エージェント 統合 フック インストール jcode agent integration hooks install"
|
||||
}
|
||||
L10nKey::SettingsSearchMuseKeywords => {
|
||||
"エージェント 統合 フック プラグイン インストール muse meta agent integration hooks plugins install"
|
||||
}
|
||||
L10nKey::SettingsSearchAntigravityKeywords => {
|
||||
"エージェント 統合 フック インストール antigravity agy google agent integration hooks install"
|
||||
}
|
||||
|
||||
@@ -773,6 +773,10 @@ l10n_keys! {
|
||||
SettingsAgentCursorCli,
|
||||
SettingsAgentPrimeAgent,
|
||||
SettingsAgentAntigravity,
|
||||
SettingsAgentEmpryo,
|
||||
SettingsAgentJcode,
|
||||
SettingsAgentMuse,
|
||||
SettingsMuseManualInstall,
|
||||
SettingsAgentQoderCn,
|
||||
SettingsSearchAppHttpProxyKeywords,
|
||||
SettingsSearchAboutKeywords,
|
||||
@@ -837,6 +841,9 @@ l10n_keys! {
|
||||
SettingsSearchCursorCliKeywords,
|
||||
SettingsSearchPrimeAgentKeywords,
|
||||
SettingsSearchAntigravityKeywords,
|
||||
SettingsSearchEmpryoKeywords,
|
||||
SettingsSearchJcodeKeywords,
|
||||
SettingsSearchMuseKeywords,
|
||||
SettingsSearchQoderCnKeywords,
|
||||
SettingsSearchRememberWindowSizeKeywords,
|
||||
SettingsSearchReportMouseToAppsKeywords,
|
||||
@@ -1722,6 +1729,7 @@ l10n_keys! {
|
||||
AppAgentHooksOpFailed,
|
||||
AppAgentHooksInstalled,
|
||||
AppAgentHooksInstalledEnableCodexThere,
|
||||
AppAgentHooksMuseManualInstall,
|
||||
AppAgentHooksInstalledCodexEnableFailed,
|
||||
AppAgentHooksRemoved,
|
||||
AppAgentHooksNothingInstalled,
|
||||
@@ -2059,6 +2067,9 @@ mod tests {
|
||||
L10nKey::SettingsAgentCursorCli,
|
||||
L10nKey::SettingsAgentPrimeAgent,
|
||||
L10nKey::SettingsAgentAntigravity,
|
||||
L10nKey::SettingsAgentEmpryo,
|
||||
L10nKey::SettingsAgentJcode,
|
||||
L10nKey::SettingsAgentMuse,
|
||||
L10nKey::SettingsAgentQoderCn,
|
||||
// Windows names its backdrop materials, and Japanese Windows keeps
|
||||
// those names in Latin script — so does this list. Chinese does
|
||||
|
||||
@@ -982,6 +982,11 @@ pub fn translate_ru(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsAgentCursorCli => "Cursor CLI",
|
||||
L10nKey::SettingsAgentPrimeAgent => "Prime Agent",
|
||||
L10nKey::SettingsAgentAntigravity => "Antigravity",
|
||||
L10nKey::SettingsAgentEmpryo => "Empryo",
|
||||
L10nKey::SettingsAgentJcode => "jcode",
|
||||
L10nKey::SettingsAgentMuse => "Muse Code",
|
||||
L10nKey::SettingsMuseManualInstall => "Требуется ручная установка",
|
||||
L10nKey::AppAgentHooksMuseManualInstall => "Выполните на удалённом компьютере: {command}",
|
||||
L10nKey::SettingsAgentQoderCn => "Qoder CN CLI",
|
||||
L10nKey::SettingsSearchAppHttpProxyKeywords => {
|
||||
"прокси сеть загрузка обновление proxy http https socks socks5 clash v2ray network download update"
|
||||
@@ -1162,6 +1167,15 @@ pub fn translate_ru(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsSearchAntigravityKeywords => {
|
||||
"агент интеграция хуки установка agent integration hooks install antigravity agy google"
|
||||
}
|
||||
L10nKey::SettingsSearchEmpryoKeywords => {
|
||||
"агент интеграция хуки установка empryo agent integration hooks install"
|
||||
}
|
||||
L10nKey::SettingsSearchJcodeKeywords => {
|
||||
"агент интеграция хуки установка jcode agent integration hooks install"
|
||||
}
|
||||
L10nKey::SettingsSearchMuseKeywords => {
|
||||
"агент интеграция хуки плагины установка muse meta agent integration hooks plugins install"
|
||||
}
|
||||
L10nKey::SettingsSearchQoderCnKeywords => {
|
||||
"агент интеграция хуки установка китайский agent integration hooks install qodercn qoderclicn qoder-cn qoder china"
|
||||
}
|
||||
|
||||
@@ -805,6 +805,11 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsAgentCursorCli => "Cursor CLI",
|
||||
L10nKey::SettingsAgentPrimeAgent => "Prime Agent",
|
||||
L10nKey::SettingsAgentAntigravity => "Antigravity",
|
||||
L10nKey::SettingsAgentEmpryo => "Empryo",
|
||||
L10nKey::SettingsAgentJcode => "jcode",
|
||||
L10nKey::SettingsAgentMuse => "Muse Code",
|
||||
L10nKey::SettingsMuseManualInstall => "需要手动安装",
|
||||
L10nKey::AppAgentHooksMuseManualInstall => "请在目标机器上运行: {command}",
|
||||
L10nKey::SettingsAgentQoderCn => "Qoder CN CLI",
|
||||
L10nKey::SettingsSearchAboutKeywords => {
|
||||
"关于 版本 许可证 致谢 构建 更新 检查 github about version license credits update"
|
||||
@@ -961,6 +966,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsSearchPrimeAgentKeywords => {
|
||||
"Prime Agent agent 集成 扩展 安装 prime prime-agent primeintellect"
|
||||
}
|
||||
L10nKey::SettingsSearchEmpryoKeywords => "Empryo agent 集成 钩子 安装 empryo",
|
||||
L10nKey::SettingsSearchJcodeKeywords => "jcode agent 集成 钩子 安装 jcode",
|
||||
L10nKey::SettingsSearchMuseKeywords => "Muse Code agent 集成 钩子 插件 安装 muse meta",
|
||||
L10nKey::SettingsSearchAntigravityKeywords => {
|
||||
"Antigravity agent 集成 钩子 安装 antigravity agy google"
|
||||
}
|
||||
|
||||
@@ -654,6 +654,21 @@ fn settings_search_entries() -> &'static [SearchEntry] {
|
||||
title: SettingsAgentAntigravity,
|
||||
keywords: SettingsSearchAntigravityKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: Agents,
|
||||
title: SettingsAgentEmpryo,
|
||||
keywords: SettingsSearchEmpryoKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: Agents,
|
||||
title: SettingsAgentJcode,
|
||||
keywords: SettingsSearchJcodeKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: Agents,
|
||||
title: SettingsAgentMuse,
|
||||
keywords: SettingsSearchMuseKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: General,
|
||||
title: SettingsStartupWindow,
|
||||
|
||||
@@ -31,6 +31,9 @@ fn cli_agent(agent: HookAgent) -> crate::core::cli_agent::CLIAgent {
|
||||
HookAgent::Cursor => C::Cursor,
|
||||
HookAgent::PrimeAgent => C::PrimeAgent,
|
||||
HookAgent::Antigravity => C::Antigravity,
|
||||
HookAgent::Empryo => C::Empryo,
|
||||
HookAgent::Jcode => C::Jcode,
|
||||
HookAgent::Muse => C::Muse,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,8 +348,14 @@ impl Tty7App {
|
||||
let tk = Tk::of(cx);
|
||||
let agent = row.agent;
|
||||
let mark = cli_agent(agent);
|
||||
let manual_muse = agent == HookAgent::Muse
|
||||
&& self
|
||||
.active_settings()
|
||||
.is_some_and(|s| !s.agent_hooks_host.is_local());
|
||||
let (status, dot) = if busy {
|
||||
(Some(t(L10nKey::SettingsWorking)), tk.k3)
|
||||
} else if manual_muse {
|
||||
(Some(t(L10nKey::SettingsMuseManualInstall)), tk.warn)
|
||||
} else {
|
||||
match row.state {
|
||||
HooksState::Installed => (Some(t(L10nKey::SettingsStatusInstalled)), tk.ok),
|
||||
|
||||
Reference in New Issue
Block a user