mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat(agents): recognize Oh My Pi and install its status hooks
Issue #376 asked for `omp`. Oh My Pi is a fork of Pi (can1357/oh-my-pi, descended from badlogic/pi-mono), but the fork is where the similarity stops for our purposes: it ships one binary of its own — `omp`, the only `bin` in `@oh-my-pi/pi-coding-agent`, and it never installs a `pi` — and it keeps its config under `~/.omp`. A pane running it was therefore not detected at all, and aliasing `omp` onto `CLIAgent::Pi` would have been worse than nothing: the status bridge would land in `~/.pi`, and Resume Session would offer `pi --session <id>` to a binary that spells that flag `--resume`. So it gets its own variant, wired the whole way through: | | | |---|---| | Detection | argv stem `omp`, distinct from `pi` in both directions | | Avatar | its own mark, normalized from the project's `assets/icon.svg` | | Resume | `omp --resume <id>`, opting out on `--no-session` | | Fork | `omp --fork <id>` — a verified fork command, so the menu item appears | | Hooks | Settings → Agents, at `~/.omp/agent/extensions/tty7/index.ts` | The status bridge is the one piece the fork did not change. Oh My Pi inherited Pi's extension contract intact — same default-exported factory, same `session_start` / `agent_start` / `agent_end` / `session_shutdown`, same `ctx.sessionManager.getSessionId()` — so `pi_extension_ts` now takes the agent and substitutes two things, the package it imports the type from and the slug it calls the emitter with. Pi's generated file is byte-identical to before, so no installed bridge goes stale. `--resume`, `-r` and `--session` are three spellings of one flag in Oh My Pi; all three shed when a session command is rebuilt, while `--session-dir` is a different flag and rides along. `fork_command` now honors the same `--no-session` opt-out `resume_command` already did — Oh My Pi rejects `--fork` outright under it, and no existing agent declares an opt-out.
This commit is contained in:
@@ -114,6 +114,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
dragging a selection past the edge, clearing the scrollback, the keyboard and
|
||||
mouse-reporting paths — cancels what is in flight first.
|
||||
|
||||
- **Oh My Pi (`omp`) is a first-class agent** — the eighteenth CLI tty7
|
||||
recognizes in a pane, with the full set: brand avatar, status dot, session
|
||||
resume (`omp --resume <id>`), and **Fork Session** (`omp --fork <id>`).
|
||||
**Settings → Agents** installs its hooks like the others.
|
||||
|
||||
Oh My Pi is a fork of Pi, but it is its own binary with its own
|
||||
`~/.omp` config directory, so it gets its own entry rather than sharing
|
||||
Pi's — a pane running `omp` was previously not detected at all, and
|
||||
aliasing it onto Pi would have written the bridge to `~/.pi` and offered
|
||||
`pi --session <id>` to a binary that spells it `--resume`. The status
|
||||
bridge is the Pi extension the fork inherited (same default-exported
|
||||
factory, same four lifecycle events), so one template now serves both,
|
||||
landing at `~/.omp/agent/extensions/tty7/index.ts`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Pasting a screenshot into a remote pane works on macOS too** — the
|
||||
|
||||
@@ -45,7 +45,7 @@ Native builds for each platform on [**Releases**](https://github.com/l0ng-ai/tty
|
||||
|---|---|
|
||||
| **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> palette · <kbd>⌘ F</kbd> scrollback search · nine themes · IME |
|
||||
| **Agent-aware** | per-pane detection (~17 CLIs): status dot · notifications · branch + diff · resume after reboot · tray icon when input is needed |
|
||||
| **Agent-aware** | per-pane detection (~18 CLIs): status dot · notifications · branch + diff · resume after reboot · tray icon when input is needed |
|
||||
| **Remote workspaces** | remote files, repos, changes, diffs, worktrees, tabs, and panes · reconnect from any client and continue where you left off |
|
||||
| **CLI + Skills** | bundled `tty7` CLI · [agent skill](skills/tty7/SKILL.md) · pane/workspace control · real PTY commands · output, process, port, and agent status |
|
||||
| **SSH** | native russh stack: profiles with keychain secrets · SFTP panel · port forwarding · jump hosts · one-time, unprivileged `tty7-server` install |
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@
|
||||
|---|---|
|
||||
| **编辑器级输入** | 历史影子建议 · 带说明的 Tab 补全 · 语法高亮 · 多行编辑 · 点击定位光标 · <kbd>⌃ R</kbd> 模糊历史搜索 |
|
||||
| **窗口** | 标签页与分屏 · <kbd>⌘ P</kbd> 命令面板 · <kbd>⌘ F</kbd> 回滚搜索 · 9 套主题 · 输入法 |
|
||||
| **Agent-aware** | 按 pane 识别约 17 个 CLI agent:状态点 · 通知 · 分支 + diff · 重启后续上会话 · 托盘图标提醒需要输入 |
|
||||
| **Agent-aware** | 按 pane 识别约 18 个 CLI agent:状态点 · 通知 · 分支 + diff · 重启后续上会话 · 托盘图标提醒需要输入 |
|
||||
| **远程工作区** | 远端文件、仓库、Changes、diff、worktree、标签页和 pane · 任意客户端重连后原地继续 |
|
||||
| **CLI + Skills** | 安装包自带 `tty7` CLI · [agent skill](skills/tty7/SKILL.md) · pane/工作区控制 · 真实 PTY 命令 · 输出、进程、端口和 agent 状态 |
|
||||
| **SSH** | 原生 russh 栈:profile 凭据进 keychain · SFTP 面板 · 端口转发 · 跳板机 · 一次无 sudo 安装 `tty7-server` |
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<!-- Oh My Pi's own brand mark, from can1357/oh-my-pi assets/icon.svg (MIT).
|
||||
Normalized to tty7's 24x24 icon format: the source is a 120x90 box whose
|
||||
art spans x[10,110] y[8,82], scaled by 0.2 and centred, so every edge lands
|
||||
on a tenth of a unit and the proportions are unchanged. The source paints
|
||||
the glyph white with an orange plug and two dark prongs; the avatars are
|
||||
tinted flat by the app, so the multi-colour split collapses to the single
|
||||
sentinel fill the rest of the set uses, and the prongs — a quarter of a
|
||||
unit wide here — are dropped rather than rendered as invisible slivers.
|
||||
The plug itself stays: it is what separates this mark from a plain pi. -->
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="4.6" width="20" height="2.4" rx="0.4" fill="#FF0000"/>
|
||||
<rect x="5" y="7" width="2.4" height="12.4" rx="0.4" fill="#FF0000"/>
|
||||
<rect x="15" y="7" width="2.4" height="9" rx="0.4" fill="#FF0000"/>
|
||||
<rect x="14.2" y="14" width="4" height="3.2" rx="0.6" fill="#FF0000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -213,16 +213,18 @@ pub enum HookAgent {
|
||||
OpenCode,
|
||||
Pi,
|
||||
Grok,
|
||||
OhMyPi,
|
||||
}
|
||||
|
||||
impl HookAgent {
|
||||
pub const ALL: [HookAgent; 6] = [
|
||||
pub const ALL: [HookAgent; 7] = [
|
||||
HookAgent::Claude,
|
||||
HookAgent::Codex,
|
||||
HookAgent::Copilot,
|
||||
HookAgent::OpenCode,
|
||||
HookAgent::Pi,
|
||||
HookAgent::Grok,
|
||||
HookAgent::OhMyPi,
|
||||
];
|
||||
|
||||
/// The hooks behind a detected agent process, if it has any.
|
||||
@@ -238,6 +240,7 @@ impl HookAgent {
|
||||
CLIAgent::OpenCode => Some(HookAgent::OpenCode),
|
||||
CLIAgent::Pi => Some(HookAgent::Pi),
|
||||
CLIAgent::Grok => Some(HookAgent::Grok),
|
||||
CLIAgent::OhMyPi => Some(HookAgent::OhMyPi),
|
||||
CLIAgent::Gemini
|
||||
| CLIAgent::Aider
|
||||
| CLIAgent::Amp
|
||||
@@ -260,6 +263,7 @@ impl HookAgent {
|
||||
HookAgent::OpenCode => "opencode",
|
||||
HookAgent::Pi => "pi",
|
||||
HookAgent::Grok => "grok",
|
||||
HookAgent::OhMyPi => "omp",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,6 +275,7 @@ impl HookAgent {
|
||||
HookAgent::OpenCode => "OpenCode",
|
||||
HookAgent::Pi => "Pi",
|
||||
HookAgent::Grok => "Grok Build",
|
||||
HookAgent::OhMyPi => "Oh My Pi",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,6 +294,9 @@ impl HookAgent {
|
||||
),
|
||||
HookAgent::Pi => target.under_home(&[".pi", "agent", "extensions", "tty7", "index.ts"]),
|
||||
HookAgent::Grok => target.under_home(&[".grok", "hooks", OWNED_FILE_STEM_JSON]),
|
||||
HookAgent::OhMyPi => {
|
||||
target.under_home(&[".omp", "agent", "extensions", "tty7", "index.ts"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +452,11 @@ pub fn hooks_state(target: &HookTarget, agent: HookAgent) -> HooksState {
|
||||
match agent {
|
||||
HookAgent::Claude => hook_map_state(target, &path, agent, CLAUDE_HOOK_EVENTS),
|
||||
HookAgent::Codex => hook_map_state(target, &path, agent, CODEX_HOOK_EVENTS),
|
||||
HookAgent::Copilot | HookAgent::OpenCode | HookAgent::Pi | HookAgent::Grok => {
|
||||
HookAgent::Copilot
|
||||
| HookAgent::OpenCode
|
||||
| HookAgent::Pi
|
||||
| HookAgent::Grok
|
||||
| HookAgent::OhMyPi => {
|
||||
let Some(expected) = owned_file_content(target, agent) else {
|
||||
return HooksState::NotInstalled;
|
||||
};
|
||||
@@ -475,7 +487,11 @@ pub fn install_hooks(target: &HookTarget, agent: HookAgent) -> anyhow::Result<St
|
||||
),
|
||||
})
|
||||
}
|
||||
HookAgent::Copilot | HookAgent::OpenCode | HookAgent::Pi | HookAgent::Grok => {
|
||||
HookAgent::Copilot
|
||||
| HookAgent::OpenCode
|
||||
| HookAgent::Pi
|
||||
| HookAgent::Grok
|
||||
| HookAgent::OhMyPi => {
|
||||
let content = owned_file_content(target, agent)
|
||||
.ok_or_else(|| anyhow::anyhow!("{agent:?} has no owned file"))?;
|
||||
owned_file_install(target, &path, &content, &agent.marker())?;
|
||||
@@ -488,9 +504,11 @@ pub fn uninstall_hooks(target: &HookTarget, agent: HookAgent) -> anyhow::Result<
|
||||
let path = agent.target_path(target);
|
||||
match agent {
|
||||
HookAgent::Claude | HookAgent::Codex => hook_map_uninstall(target, &path, agent),
|
||||
HookAgent::Copilot | HookAgent::OpenCode | HookAgent::Pi | HookAgent::Grok => {
|
||||
owned_file_uninstall(target, &path, &agent.marker())
|
||||
}
|
||||
HookAgent::Copilot
|
||||
| HookAgent::OpenCode
|
||||
| HookAgent::Pi
|
||||
| HookAgent::Grok
|
||||
| HookAgent::OhMyPi => owned_file_uninstall(target, &path, &agent.marker()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -772,7 +790,7 @@ fn owned_file_content(target: &HookTarget, agent: HookAgent) -> Option<String> {
|
||||
match agent {
|
||||
HookAgent::Copilot => copilot_hooks_json(target),
|
||||
HookAgent::OpenCode => opencode_plugin_js(target),
|
||||
HookAgent::Pi => pi_extension_ts(target),
|
||||
HookAgent::Pi | HookAgent::OhMyPi => pi_extension_ts(target, agent),
|
||||
HookAgent::Grok => grok_hooks_json(target),
|
||||
HookAgent::Claude | HookAgent::Codex => None,
|
||||
}
|
||||
@@ -911,11 +929,23 @@ export const Tty7Presence = async ({{ $ }}) => {{
|
||||
))
|
||||
}
|
||||
|
||||
fn pi_extension_ts(target: &HookTarget) -> Option<String> {
|
||||
/// The Pi extension bridge, shared with Oh My Pi.
|
||||
///
|
||||
/// Oh My Pi is a fork of Pi and kept the extension contract intact — same
|
||||
/// default-exported factory, same four lifecycle events, same
|
||||
/// `ctx.sessionManager.getSessionId()`. Only the package it is imported from
|
||||
/// and the slug the emitter is called with differ, so one template serves both
|
||||
/// rather than two copies drifting apart.
|
||||
fn pi_extension_ts(target: &HookTarget, agent: HookAgent) -> Option<String> {
|
||||
let (slug, package) = match agent {
|
||||
HookAgent::Pi => ("pi", "@mariozechner/pi-coding-agent"),
|
||||
HookAgent::OhMyPi => ("omp", "@oh-my-pi/pi-coding-agent"),
|
||||
_ => return None,
|
||||
};
|
||||
let exe = serde_json::to_string(&target.exe.display().to_string()).ok()?;
|
||||
Some(format!(
|
||||
r#"/* tty7 agent-hook pi bridge — generated by tty7, do not edit. */
|
||||
import type {{ ExtensionAPI }} from "@mariozechner/pi-coding-agent";
|
||||
r#"/* tty7 agent-hook {slug} bridge — generated by tty7, do not edit. */
|
||||
import type {{ ExtensionAPI }} from "{package}";
|
||||
import {{ spawnSync }} from "node:child_process";
|
||||
|
||||
const EXE = {exe};
|
||||
@@ -931,7 +961,7 @@ function emit(event: string, ctx?: SessionCtx): void {{
|
||||
const id = ctx?.sessionManager?.getSessionId?.();
|
||||
if (id) payload = JSON.stringify({{ session_id: id }});
|
||||
}} catch {{}}
|
||||
const args = ["agent-hook", "pi", event];
|
||||
const args = ["agent-hook", "{slug}", event];
|
||||
// Nothing to send → leave stdin closed rather than handing the emitter a
|
||||
// pipe it has to read to EOF.
|
||||
if (payload) {{
|
||||
@@ -1229,6 +1259,10 @@ mod tests {
|
||||
),
|
||||
(HookAgent::Pi, "/home/me/.pi/agent/extensions/tty7/index.ts"),
|
||||
(HookAgent::Grok, "/home/me/.grok/hooks/tty7.json"),
|
||||
(
|
||||
HookAgent::OhMyPi,
|
||||
"/home/me/.omp/agent/extensions/tty7/index.ts",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
agent.target_path(&target),
|
||||
@@ -1274,7 +1308,7 @@ mod tests {
|
||||
let host = FakeRemote::shared();
|
||||
let target = HookTarget::remote(&*host, dir.clone());
|
||||
|
||||
for agent in [HookAgent::Claude, HookAgent::Grok] {
|
||||
for agent in [HookAgent::Claude, HookAgent::Grok, HookAgent::OhMyPi] {
|
||||
assert_eq!(hooks_state(&target, agent), HooksState::NotInstalled);
|
||||
install_hooks(&target, agent).expect("install succeeds");
|
||||
assert_eq!(hooks_state(&target, agent), HooksState::Installed);
|
||||
@@ -1331,14 +1365,36 @@ mod tests {
|
||||
assert!(opencode.contains(hook_exe));
|
||||
assert!(opencode.contains(r#"process.env["TTY7"]"#));
|
||||
|
||||
let pi = pi_extension_ts(&target).expect("pi content builds");
|
||||
assert!(pi.contains("agent-hook pi"));
|
||||
assert!(pi.contains(&exe));
|
||||
assert!(pi.contains(r#"process.env["TTY7"]"#));
|
||||
assert!(pi.contains("getSessionId"));
|
||||
assert!(pi.contains("session_id"));
|
||||
assert!(pi.contains(r#"stdio: ["pipe", "ignore", "ignore"]"#));
|
||||
assert!(pi.contains(r#"pi.on("session_start""#));
|
||||
for (agent, slug, package) in [
|
||||
(HookAgent::Pi, "pi", "@mariozechner/pi-coding-agent"),
|
||||
(HookAgent::OhMyPi, "omp", "@oh-my-pi/pi-coding-agent"),
|
||||
] {
|
||||
let bridge =
|
||||
pi_extension_ts(&target, agent).unwrap_or_else(|| panic!("{slug} content builds"));
|
||||
assert!(bridge.contains(&format!("agent-hook {slug}")));
|
||||
assert!(bridge.contains(&format!(r#"["agent-hook", "{slug}", event]"#)));
|
||||
assert!(bridge.contains(&format!(r#"from "{package}""#)));
|
||||
assert!(bridge.contains(&exe));
|
||||
assert!(bridge.contains(r#"process.env["TTY7"]"#));
|
||||
assert!(bridge.contains("getSessionId"));
|
||||
assert!(bridge.contains("session_id"));
|
||||
assert!(bridge.contains(r#"stdio: ["pipe", "ignore", "ignore"]"#));
|
||||
for event in [
|
||||
"session_start",
|
||||
"agent_start",
|
||||
"agent_end",
|
||||
"session_shutdown",
|
||||
] {
|
||||
assert!(
|
||||
bridge.contains(&format!(r#"pi.on("{event}""#)),
|
||||
"{slug} bridge subscribes to {event}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
pi_extension_ts(&target, HookAgent::Claude).is_none(),
|
||||
"only the two Pi-shaped agents get this bridge"
|
||||
);
|
||||
|
||||
let grok = grok_hooks_json(&target).expect("grok content builds");
|
||||
let parsed: serde_json::Value = serde_json::from_str(&grok).expect("valid JSON");
|
||||
|
||||
@@ -21,10 +21,11 @@ pub enum CLIAgent {
|
||||
Antigravity,
|
||||
Grok,
|
||||
Qwen,
|
||||
OhMyPi,
|
||||
}
|
||||
|
||||
impl CLIAgent {
|
||||
pub const ALL: [CLIAgent; 17] = [
|
||||
pub const ALL: [CLIAgent; 18] = [
|
||||
CLIAgent::Claude,
|
||||
CLIAgent::Codex,
|
||||
CLIAgent::Gemini,
|
||||
@@ -42,6 +43,7 @@ impl CLIAgent {
|
||||
CLIAgent::Antigravity,
|
||||
CLIAgent::Grok,
|
||||
CLIAgent::Qwen,
|
||||
CLIAgent::OhMyPi,
|
||||
];
|
||||
|
||||
fn aliases(self) -> &'static [&'static str] {
|
||||
@@ -63,6 +65,9 @@ impl CLIAgent {
|
||||
CLIAgent::Antigravity => &["agy", "antigravity"],
|
||||
CLIAgent::Grok => &["grok"],
|
||||
CLIAgent::Qwen => &["qwen", "qwen-code"],
|
||||
// Oh My Pi is a fork of Pi, but it ships one binary of its own and
|
||||
// never installs a `pi`, so the two names stay disjoint.
|
||||
CLIAgent::OhMyPi => &["omp"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +90,7 @@ impl CLIAgent {
|
||||
CLIAgent::Antigravity => "antigravity",
|
||||
CLIAgent::Grok => "grok",
|
||||
CLIAgent::Qwen => "qwen",
|
||||
CLIAgent::OhMyPi => "omp",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +118,7 @@ impl CLIAgent {
|
||||
CLIAgent::Antigravity => "Antigravity",
|
||||
CLIAgent::Grok => "Grok",
|
||||
CLIAgent::Qwen => "Qwen Code",
|
||||
CLIAgent::OhMyPi => "Oh My Pi",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,19 +141,23 @@ impl CLIAgent {
|
||||
CLIAgent::Copilot => Some(format!("copilot{flags} --resume {session_id}")),
|
||||
CLIAgent::Grok => Some(format!("grok{flags} --resume {session_id}")),
|
||||
CLIAgent::Pi => Some(format!("pi{flags} --session {session_id}")),
|
||||
CLIAgent::OhMyPi => Some(format!("omp{flags} --resume {session_id}")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn opts_out_of_sessions(self, argv: &[String]) -> bool {
|
||||
let ephemeral: &[&str] = match self {
|
||||
CLIAgent::Pi => &["--no-session"],
|
||||
CLIAgent::Pi | CLIAgent::OhMyPi => &["--no-session"],
|
||||
_ => &[],
|
||||
};
|
||||
argv.iter().any(|t| ephemeral.contains(&t.as_str()))
|
||||
}
|
||||
|
||||
pub fn fork_command(self, session_id: &str, launch_argv: Option<&[String]>) -> Option<String> {
|
||||
if launch_argv.is_some_and(|argv| self.opts_out_of_sessions(argv)) {
|
||||
return None;
|
||||
}
|
||||
let flags = self.session_command_flags(session_id, launch_argv)?;
|
||||
match self {
|
||||
CLIAgent::Codex => Some(format!("codex fork {session_id}{flags}")),
|
||||
@@ -155,15 +166,18 @@ impl CLIAgent {
|
||||
)),
|
||||
CLIAgent::Grok => Some(format!("grok{flags} --resume {session_id} --fork-session")),
|
||||
CLIAgent::OpenCode => Some(format!("opencode{flags} --session {session_id} --fork")),
|
||||
CLIAgent::OhMyPi => Some(format!("omp{flags} --fork {session_id}")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fork_label(self) -> Option<&'static str> {
|
||||
match self {
|
||||
CLIAgent::Claude | CLIAgent::Codex | CLIAgent::Grok | CLIAgent::OpenCode => {
|
||||
Some("Fork Session")
|
||||
}
|
||||
CLIAgent::Claude
|
||||
| CLIAgent::Codex
|
||||
| CLIAgent::Grok
|
||||
| CLIAgent::OpenCode
|
||||
| CLIAgent::OhMyPi => Some("Fork Session"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -234,6 +248,9 @@ impl CLIAgent {
|
||||
"--continue",
|
||||
"-c",
|
||||
],
|
||||
// `--resume`, `-r` and `--session` are three spellings of one flag
|
||||
// in Oh My Pi; `--session-dir` is a different one and survives.
|
||||
CLIAgent::OhMyPi => &["--resume", "-r", "--session", "--fork", "--continue", "-c"],
|
||||
CLIAgent::Grok => &[
|
||||
"--resume",
|
||||
"-r",
|
||||
@@ -305,6 +322,7 @@ impl CLIAgent {
|
||||
CLIAgent::Antigravity => 0x2563EB,
|
||||
CLIAgent::Grok => 0x000000,
|
||||
CLIAgent::Qwen => 0x7C3AED,
|
||||
CLIAgent::OhMyPi => 0xF97316,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +339,7 @@ impl CLIAgent {
|
||||
CLIAgent::Droid => "icons/agents/droid.svg",
|
||||
CLIAgent::Grok => "icons/agents/grok.svg",
|
||||
CLIAgent::Pi => "icons/agents/pi.svg",
|
||||
CLIAgent::OhMyPi => "icons/agents/omp.svg",
|
||||
CLIAgent::Aider
|
||||
| CLIAgent::Auggie
|
||||
| CLIAgent::Hermes
|
||||
@@ -717,6 +736,10 @@ mod tests {
|
||||
fallback,
|
||||
["aider", "auggie", "hermes", "vibe", "antigravity", "qwen"]
|
||||
);
|
||||
assert!(
|
||||
!fallback.contains(&"omp"),
|
||||
"Oh My Pi ships its own mark and must not fall back"
|
||||
);
|
||||
for a in CLIAgent::ALL {
|
||||
let path = a.icon_path();
|
||||
assert!(
|
||||
@@ -737,11 +760,30 @@ mod tests {
|
||||
("/usr/local/bin/qwen", CLIAgent::Qwen),
|
||||
("pi", CLIAgent::Pi),
|
||||
("hermes", CLIAgent::Hermes),
|
||||
("omp", CLIAgent::OhMyPi),
|
||||
("/opt/homebrew/bin/omp", CLIAgent::OhMyPi),
|
||||
] {
|
||||
assert_eq!(CLIAgent::detect_from_argv(&argv(&[cmd])), Some(agent));
|
||||
}
|
||||
}
|
||||
|
||||
/// Oh My Pi is a Pi fork, but it is its own binary with its own config
|
||||
/// directory and its own flags — one must never be detected as the other.
|
||||
#[test]
|
||||
fn oh_my_pi_and_pi_stay_distinct() {
|
||||
assert_eq!(
|
||||
CLIAgent::detect_from_argv(&argv(&["omp"])),
|
||||
Some(CLIAgent::OhMyPi)
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::detect_from_argv(&argv(&["pi"])),
|
||||
Some(CLIAgent::Pi)
|
||||
);
|
||||
assert_eq!(CLIAgent::from_slug("omp"), Some(CLIAgent::OhMyPi));
|
||||
assert_eq!(CLIAgent::from_slug("oh-my-pi"), None);
|
||||
assert_ne!(CLIAgent::Pi.icon_path(), CLIAgent::OhMyPi.icon_path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_rules_map_wrappers_to_agents() {
|
||||
let custom: HashMap<String, String> = [("cc".to_string(), "claude".to_string())].into();
|
||||
@@ -1193,6 +1235,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oh_my_pi_resume_and_fork_use_its_own_flags() {
|
||||
let argv = |parts: &[&str]| parts.iter().map(|s| s.to_string()).collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
CLIAgent::OhMyPi.resume_command("s-1", None).as_deref(),
|
||||
Some("omp --resume s-1")
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::OhMyPi.fork_command("s-1", None).as_deref(),
|
||||
Some("omp --fork s-1")
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::OhMyPi
|
||||
.resume_command(
|
||||
"s-2",
|
||||
Some(&argv(&["omp", "--session", "s-1", "--model", "opus"]))
|
||||
)
|
||||
.as_deref(),
|
||||
Some("omp --model opus --resume s-2"),
|
||||
"--session is a third spelling of --resume and sheds with it"
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::OhMyPi
|
||||
.fork_command(
|
||||
"s-2",
|
||||
Some(&argv(&["omp", "--fork", "s-1", "-c", "--yolo"]))
|
||||
)
|
||||
.as_deref(),
|
||||
Some("omp --yolo --fork s-2")
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::OhMyPi
|
||||
.resume_command(
|
||||
"s-3",
|
||||
Some(&argv(&["omp", "--session-dir", "/w/.sessions"]))
|
||||
)
|
||||
.as_deref(),
|
||||
Some("omp --session-dir /w/.sessions --resume s-3"),
|
||||
"--session-dir is a different flag and rides along"
|
||||
);
|
||||
|
||||
// `--no-session` means the run never persisted one, so there is
|
||||
// nothing to resume from and Oh My Pi rejects `--fork` outright.
|
||||
for id in ["s-4"] {
|
||||
assert_eq!(
|
||||
CLIAgent::OhMyPi.resume_command(id, Some(&argv(&["omp", "--no-session"]))),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
CLIAgent::OhMyPi.fork_command(id, Some(&argv(&["omp", "--no-session"]))),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fork_commands_cover_exactly_the_agents_with_a_verified_fork() {
|
||||
assert_eq!(
|
||||
|
||||
+3
-3
@@ -53,15 +53,15 @@ one that advances 0.5em (Sarasa Mono SC, say) makes two columns exactly 1.0em.
|
||||
## Coding agents
|
||||
|
||||
tty7 recognizes third-party coding agents running in a pane (Claude Code,
|
||||
Codex, Gemini CLI, Aider, Amp, OpenCode, and ~10 more) and adds around them —
|
||||
Codex, Gemini CLI, Aider, Amp, OpenCode, and ~12 more) and adds around them —
|
||||
it never wraps or replaces the agent.
|
||||
|
||||
- **Brand avatars** — the tab chip / sidebar row shows which agent runs where; custom wrappers map in via `agent_commands` in `config.json`
|
||||
- **Status dot** — working (blue) / needs your input (amber) / done (green), driven by agent-reported events over an OSC channel; Settings → Agents installs the hooks that feed it (Claude Code, Codex, Copilot CLI, OpenCode, Pi, Grok Build)
|
||||
- **Status dot** — working (blue) / needs your input (amber) / done (green), driven by agent-reported events over an OSC channel; Settings → Agents installs the hooks that feed it (Claude Code, Codex, Copilot CLI, OpenCode, Pi, Grok Build, Oh My Pi)
|
||||
- **Notifications** — "needs your permission…" the moment an agent blocks on you, and "finished after Ns" per turn, honoring your notification policy
|
||||
- **Branch at a glance** — each sidebar row shows its pane's git branch and working-tree diff (`+N −M`), refreshed on `cd` and when a command finishes; clicking the counts opens the diff overlay, and turning that off (Settings → Window & Tabs, or `sidebar_diff_preview: false` in `config.json`) keeps the readout while making it non-clickable
|
||||
- **Session resume** — panes lost to a reboot re-launch their agent conversation on restore, carrying the original launch flags (`claude --dangerously-skip-permissions --resume …`) (`restore_agent_sessions`, on by default)
|
||||
- **Fork session** — branch a live agent conversation into a second, independent one by shelling the agent's own fork command (`codex fork <id>`, `claude --resume <id> --fork-session`, also OpenCode and Grok Build); the original is untouched and both continue separately. Right-click a pane to pick a split placement, or right-click the tab / sidebar row to open the fork in a new tab. Needs the agent's hooks installed, since the fork targets the session id they report; a remote pane can't fork, because the command would run against the local agent — and note a fork copies the whole transcript, so repeated forking costs real disk in the agent's own session store
|
||||
- **Fork session** — branch a live agent conversation into a second, independent one by shelling the agent's own fork command (`codex fork <id>`, `claude --resume <id> --fork-session`, also OpenCode, Grok Build, and Oh My Pi); the original is untouched and both continue separately. Right-click a pane to pick a split placement, or right-click the tab / sidebar row to open the fork in a new tab. Needs the agent's hooks installed, since the fork targets the session id they report; a remote pane can't fork, because the command would run against the local agent — and note a fork copies the whole transcript, so repeated forking costs real disk in the agent's own session store
|
||||
- **Copy Session ID** — put the agent's native session id on the clipboard, beside *Copy Working Directory*, for pasting into `codex resume`, a bug report, or another tool
|
||||
- **Context feed** — palette commands send the current selection or the repo's `git diff` to the running agent as a ready-made prompt
|
||||
- **Tray icon** — a system tray / menu bar item that flips to an attention state the moment any agent needs your input; its menu lists every agent pane (brand avatar + status dot, click to reveal), switches the notification policy, and offers *Quit and Stop Daemon* alongside the plain session-keeping quit (`show_tray_icon`, on by default)
|
||||
|
||||
@@ -51,14 +51,14 @@ fallback 只有在**汉字 advance 等于主字体西文 advance 的两倍**时
|
||||
## Coding agent
|
||||
|
||||
tty7 能识别 pane 里跑着的第三方 coding agent(Claude Code、Codex、Gemini CLI、
|
||||
Aider、Amp、OpenCode 等约 17 个)并在其外围加功能 —— 绝不包裹或替代 agent 本身。
|
||||
Aider、Amp、OpenCode 等约 18 个)并在其外围加功能 —— 绝不包裹或替代 agent 本身。
|
||||
|
||||
- **品牌头像** —— 标签 chip / 侧栏行显示每个 pane 跑的是哪个 agent;自定义包装命令可通过 `config.json` 的 `agent_commands` 映射
|
||||
- **状态点** —— 工作中(蓝)/ 等你输入(琥珀)/ 完成(绿),由 agent 自己上报的 OSC 事件驱动;在 设置 → Agents 一键装好对应 hooks(Claude Code、Codex、Copilot CLI、OpenCode、Pi、Grok Build)
|
||||
- **状态点** —— 工作中(蓝)/ 等你输入(琥珀)/ 完成(绿),由 agent 自己上报的 OSC 事件驱动;在 设置 → Agents 一键装好对应 hooks(Claude Code、Codex、Copilot CLI、OpenCode、Pi、Grok Build、Oh My Pi)
|
||||
- **通知** —— agent 卡在等你批准的那一刻弹 "needs your permission…",每轮结束弹 "finished after Ns",遵循你的通知策略
|
||||
- **一眼看分支** —— 侧栏每行显示该 pane 的 git 分支和工作区改动(`+N −M`),`cd` 或命令跑完时自动刷新;点改动数字会打开 diff 浮层,关掉它(设置 → 窗口与标签,或 `config.json` 的 `sidebar_diff_preview: false`)分支和数字照常显示,只是不再可点
|
||||
- **会话恢复** —— 重启后无法重连的 pane 会自动续上 agent 对话,并带上原始启动 flags(`claude --dangerously-skip-permissions --resume …`;`restore_agent_sessions`,默认开启)
|
||||
- **Fork 会话** —— 直接调 agent 自己的 fork 命令(`codex fork <id>`、`claude --resume <id> --fork-session`,OpenCode 和 Grok Build 同样支持),把当前对话分叉成一个独立会话;原会话原封不动,两边各自往下走。在 pane 上右键可选择分屏位置,在标签 / 侧栏行上右键则直接开新标签。需要先装好该 agent 的 hooks(fork 认的是 hooks 上报的 session id);远程 pane 不能 fork,因为命令会跑在本机的 agent 上;另外 fork 会整份复制对话历史,反复 fork 会在 agent 自己的会话目录里占掉不少磁盘
|
||||
- **Fork 会话** —— 直接调 agent 自己的 fork 命令(`codex fork <id>`、`claude --resume <id> --fork-session`,OpenCode、Grok Build 和 Oh My Pi 同样支持),把当前对话分叉成一个独立会话;原会话原封不动,两边各自往下走。在 pane 上右键可选择分屏位置,在标签 / 侧栏行上右键则直接开新标签。需要先装好该 agent 的 hooks(fork 认的是 hooks 上报的 session id);远程 pane 不能 fork,因为命令会跑在本机的 agent 上;另外 fork 会整份复制对话历史,反复 fork 会在 agent 自己的会话目录里占掉不少磁盘
|
||||
- **复制 Session ID** —— 把 agent 的原生 session id 复制到剪贴板,就在 *Copy Working Directory* 旁边,方便粘进 `codex resume`、bug 报告或别的工具
|
||||
- **上下文回填** —— 面板命令把当前选区或仓库 `git diff` 打包成 prompt 直接喂给正在跑的 agent
|
||||
- **托盘图标** —— 系统托盘 / 菜单栏常驻图标,任何 agent 等你输入时立即切换为提醒态;菜单列出所有 agent pane(品牌头像 + 状态点,点击直达)、可切换通知策略,并在保留会话的普通退出之外提供 *Quit and Stop Daemon*(`show_tray_icon`,默认开启)
|
||||
|
||||
@@ -53,6 +53,7 @@ fn agent_icon(path: &str) -> Option<&'static [u8]> {
|
||||
"icons/agents/droid.svg" => include_bytes!("../../assets/icons/agents/droid.svg"),
|
||||
"icons/agents/grok.svg" => include_bytes!("../../assets/icons/agents/grok.svg"),
|
||||
"icons/agents/pi.svg" => include_bytes!("../../assets/icons/agents/pi.svg"),
|
||||
"icons/agents/omp.svg" => include_bytes!("../../assets/icons/agents/omp.svg"),
|
||||
_ => return None,
|
||||
};
|
||||
Some(bytes)
|
||||
|
||||
@@ -560,6 +560,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::SettingsAgentOpencode => "OpenCode",
|
||||
L10nKey::SettingsAgentPi => "Pi",
|
||||
L10nKey::SettingsAgentGrokBuild => "Grok Build",
|
||||
L10nKey::SettingsAgentOhMyPi => "Oh My Pi",
|
||||
L10nKey::SettingsSearchAboutKeywords => "version license credits build update check github",
|
||||
L10nKey::SettingsSearchAppHttpProxyKeywords => {
|
||||
"proxy http https socks socks5 clash v2ray network download update"
|
||||
@@ -638,6 +639,7 @@ pub fn translate_en(key: L10nKey) -> &'static str {
|
||||
L10nKey::SettingsSearchOptionAsMetaKeywords => {
|
||||
"alt keyboard modifier escape macos option meta option acts as meta"
|
||||
}
|
||||
L10nKey::SettingsSearchOhMyPiKeywords => "agent integration extension install omp oh my pi",
|
||||
L10nKey::SettingsSearchPiKeywords => "agent integration extension install pi",
|
||||
L10nKey::SettingsSearchPortForwardingKeywords => {
|
||||
"ssh tunnel local remote dynamic socks forward rule"
|
||||
|
||||
@@ -560,6 +560,7 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsAgentOpencode => "OpenCode",
|
||||
L10nKey::SettingsAgentPi => "Pi",
|
||||
L10nKey::SettingsAgentGrokBuild => "Grok Build",
|
||||
L10nKey::SettingsAgentOhMyPi => "Oh My Pi",
|
||||
L10nKey::SettingsSearchAboutKeywords => {
|
||||
"バージョン ライセンス クレジット ビルド 更新 確認 github about version license credits update check"
|
||||
}
|
||||
@@ -672,6 +673,9 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsSearchOptionAsMetaKeywords => {
|
||||
"alt キーボード 修飾キー エスケープ macos option meta option acts as meta keyboard modifier"
|
||||
}
|
||||
L10nKey::SettingsSearchOhMyPiKeywords => {
|
||||
"エージェント 統合 拡張 インストール omp oh my pi agent integration extension install"
|
||||
}
|
||||
L10nKey::SettingsSearchPiKeywords => {
|
||||
"エージェント 統合 拡張 インストール pi agent integration extension install"
|
||||
}
|
||||
|
||||
@@ -456,6 +456,7 @@ pub enum L10nKey {
|
||||
SettingsAgentOpencode,
|
||||
SettingsAgentPi,
|
||||
SettingsAgentGrokBuild,
|
||||
SettingsAgentOhMyPi,
|
||||
SettingsSearchAppHttpProxyKeywords,
|
||||
SettingsSearchAboutKeywords,
|
||||
SettingsSearchAnsiColorsKeywords,
|
||||
@@ -492,6 +493,7 @@ pub enum L10nKey {
|
||||
SettingsSearchNewTabPositionKeywords,
|
||||
SettingsSearchNotifyOnCommandFinishKeywords,
|
||||
SettingsSearchNotifyThresholdKeywords,
|
||||
SettingsSearchOhMyPiKeywords,
|
||||
SettingsSearchOpacityKeywords,
|
||||
SettingsSearchOpenFilesWithKeywords,
|
||||
SettingsSearchOpencodeKeywords,
|
||||
@@ -1477,6 +1479,7 @@ mod tests {
|
||||
L10nKey::SettingsAgentOpencode,
|
||||
L10nKey::SettingsAgentPi,
|
||||
L10nKey::SettingsAgentGrokBuild,
|
||||
L10nKey::SettingsAgentOhMyPi,
|
||||
L10nKey::SettingsSearchAboutKeywords,
|
||||
L10nKey::SettingsSearchAppHttpProxyKeywords,
|
||||
L10nKey::SettingsSearchAnsiColorsKeywords,
|
||||
@@ -1513,6 +1516,7 @@ mod tests {
|
||||
L10nKey::SettingsSearchNewTabPositionKeywords,
|
||||
L10nKey::SettingsSearchNotifyOnCommandFinishKeywords,
|
||||
L10nKey::SettingsSearchNotifyThresholdKeywords,
|
||||
L10nKey::SettingsSearchOhMyPiKeywords,
|
||||
L10nKey::SettingsSearchOpacityKeywords,
|
||||
L10nKey::SettingsSearchOpenFilesWithKeywords,
|
||||
L10nKey::SettingsSearchOpencodeKeywords,
|
||||
|
||||
@@ -495,6 +495,7 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsAgentOpencode => "OpenCode",
|
||||
L10nKey::SettingsAgentPi => "Pi",
|
||||
L10nKey::SettingsAgentGrokBuild => "Grok Build",
|
||||
L10nKey::SettingsAgentOhMyPi => "Oh My Pi",
|
||||
L10nKey::SettingsSearchAboutKeywords => {
|
||||
"关于 版本 许可证 致谢 构建 更新 检查 github about version license credits update"
|
||||
}
|
||||
@@ -605,6 +606,9 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
|
||||
L10nKey::SettingsSearchOptionAsMetaKeywords => {
|
||||
"Option作为Meta 修饰键 alt option meta 转义 escape macos keyboard modifier"
|
||||
}
|
||||
L10nKey::SettingsSearchOhMyPiKeywords => {
|
||||
"Oh My Pi agent 集成 扩展 安装 omp oh my pi agent integration extension install"
|
||||
}
|
||||
L10nKey::SettingsSearchPiKeywords => {
|
||||
"Pi agent 集成 扩展 安装 pi agent integration extension install"
|
||||
}
|
||||
|
||||
@@ -311,6 +311,11 @@ fn settings_search_entries() -> &'static [SearchEntry] {
|
||||
title: SettingsAgentGrokBuild,
|
||||
keywords: SettingsSearchGrokBuildKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: Agents,
|
||||
title: SettingsAgentOhMyPi,
|
||||
keywords: SettingsSearchOhMyPiKeywords,
|
||||
},
|
||||
SearchEntry {
|
||||
section: WindowTabs,
|
||||
title: SettingsStartupWindow,
|
||||
|
||||
Reference in New Issue
Block a user