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:
l0ng-ai
2026-08-08 10:11:01 +08:00
parent c5aaf2b4f7
commit 0702d499fc
14 changed files with 236 additions and 33 deletions
+14
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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` |
+15
View File
@@ -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

+76 -20
View File
@@ -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");
+103 -5
View File
@@ -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
View File
@@ -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)
+3 -3
View File
@@ -51,14 +51,14 @@ fallback 只有在**汉字 advance 等于主字体西文 advance 的两倍**时
## Coding agent
tty7 能识别 pane 里跑着的第三方 coding agentClaude Code、Codex、Gemini CLI、
Aider、Amp、OpenCode 等约 17 个)并在其外围加功能 —— 绝不包裹或替代 agent 本身。
Aider、Amp、OpenCode 等约 18 个)并在其外围加功能 —— 绝不包裹或替代 agent 本身。
- **品牌头像** —— 标签 chip / 侧栏行显示每个 pane 跑的是哪个 agent;自定义包装命令可通过 `config.json``agent_commands` 映射
- **状态点** —— 工作中(蓝)/ 等你输入(琥珀)/ 完成(绿),由 agent 自己上报的 OSC 事件驱动;在 设置 → Agents 一键装好对应 hooksClaude Code、Codex、Copilot CLI、OpenCode、Pi、Grok Build
- **状态点** —— 工作中(蓝)/ 等你输入(琥珀)/ 完成(绿),由 agent 自己上报的 OSC 事件驱动;在 设置 → Agents 一键装好对应 hooksClaude 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`OpenCodeGrok Build 同样支持),把当前对话分叉成一个独立会话;原会话原封不动,两边各自往下走。在 pane 上右键可选择分屏位置,在标签 / 侧栏行上右键则直接开新标签。需要先装好该 agent 的 hooksfork 认的是 hooks 上报的 session id);远程 pane 不能 fork,因为命令会跑在本机的 agent 上;另外 fork 会整份复制对话历史,反复 fork 会在 agent 自己的会话目录里占掉不少磁盘
- **Fork 会话** —— 直接调 agent 自己的 fork 命令(`codex fork <id>``claude --resume <id> --fork-session`OpenCodeGrok Build 和 Oh My Pi 同样支持),把当前对话分叉成一个独立会话;原会话原封不动,两边各自往下走。在 pane 上右键可选择分屏位置,在标签 / 侧栏行上右键则直接开新标签。需要先装好该 agent 的 hooksfork 认的是 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`,默认开启)
+1
View File
@@ -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)
+2
View File
@@ -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"
+4
View File
@@ -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"
}
+4
View File
@@ -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,
+4
View File
@@ -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"
}
+5
View File
@@ -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,