diff --git a/CHANGELOG.md b/CHANGELOG.md index 9994bc3f..e76768e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `), and **Fork Session** (`omp --fork `). + **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 ` 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 diff --git a/README.md b/README.md index 9accb3d5..672059e5 100644 --- a/README.md +++ b/README.md @@ -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 · ⌃ R fuzzy history | | **Window** | tabs & splits · ⌘ P palette · ⌘ F 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 | diff --git a/README.zh-CN.md b/README.zh-CN.md index ae575126..1dcc93a4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -45,7 +45,7 @@ |---|---| | **编辑器级输入** | 历史影子建议 · 带说明的 Tab 补全 · 语法高亮 · 多行编辑 · 点击定位光标 · ⌃ R 模糊历史搜索 | | **窗口** | 标签页与分屏 · ⌘ P 命令面板 · ⌘ F 回滚搜索 · 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` | diff --git a/assets/icons/agents/omp.svg b/assets/icons/agents/omp.svg new file mode 100644 index 00000000..8a5d6462 --- /dev/null +++ b/assets/icons/agents/omp.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/crates/tty7-core/src/core/agent_hooks.rs b/crates/tty7-core/src/core/agent_hooks.rs index b58710bc..46bd8015 100644 --- a/crates/tty7-core/src/core/agent_hooks.rs +++ b/crates/tty7-core/src/core/agent_hooks.rs @@ -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 { + 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 { 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 { +/// 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 { + 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"); diff --git a/crates/tty7-core/src/core/cli_agent.rs b/crates/tty7-core/src/core/cli_agent.rs index e2390160..9d62919a 100644 --- a/crates/tty7-core/src/core/cli_agent.rs +++ b/crates/tty7-core/src/core/cli_agent.rs @@ -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 { + 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 = [("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::>(); + + 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!( diff --git a/docs/features.md b/docs/features.md index d8672561..ae11dd33 100644 --- a/docs/features.md +++ b/docs/features.md @@ -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 `, `claude --resume --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 `, `claude --resume --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) diff --git a/docs/features.zh-CN.md b/docs/features.zh-CN.md index 982524a0..b5f50e57 100644 --- a/docs/features.zh-CN.md +++ b/docs/features.zh-CN.md @@ -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 `、`claude --resume --fork-session`,OpenCode 和 Grok Build 同样支持),把当前对话分叉成一个独立会话;原会话原封不动,两边各自往下走。在 pane 上右键可选择分屏位置,在标签 / 侧栏行上右键则直接开新标签。需要先装好该 agent 的 hooks(fork 认的是 hooks 上报的 session id);远程 pane 不能 fork,因为命令会跑在本机的 agent 上;另外 fork 会整份复制对话历史,反复 fork 会在 agent 自己的会话目录里占掉不少磁盘 +- **Fork 会话** —— 直接调 agent 自己的 fork 命令(`codex fork `、`claude --resume --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`,默认开启) diff --git a/src/ui/assets.rs b/src/ui/assets.rs index 6f9e464d..ff8d1749 100644 --- a/src/ui/assets.rs +++ b/src/ui/assets.rs @@ -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) diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index c1694e22..09656160 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -563,6 +563,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" @@ -641,6 +642,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" diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index de7107d9..07448ed3 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -563,6 +563,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" } @@ -675,6 +676,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" } diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index 51cb731c..3dd997b2 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -457,6 +457,7 @@ pub enum L10nKey { SettingsAgentOpencode, SettingsAgentPi, SettingsAgentGrokBuild, + SettingsAgentOhMyPi, SettingsSearchAppHttpProxyKeywords, SettingsSearchAboutKeywords, SettingsSearchAnsiColorsKeywords, @@ -493,6 +494,7 @@ pub enum L10nKey { SettingsSearchNewTabPositionKeywords, SettingsSearchNotifyOnCommandFinishKeywords, SettingsSearchNotifyThresholdKeywords, + SettingsSearchOhMyPiKeywords, SettingsSearchOpacityKeywords, SettingsSearchOpenFilesWithKeywords, SettingsSearchOpencodeKeywords, @@ -1479,6 +1481,7 @@ mod tests { L10nKey::SettingsAgentOpencode, L10nKey::SettingsAgentPi, L10nKey::SettingsAgentGrokBuild, + L10nKey::SettingsAgentOhMyPi, L10nKey::SettingsSearchAboutKeywords, L10nKey::SettingsSearchAppHttpProxyKeywords, L10nKey::SettingsSearchAnsiColorsKeywords, @@ -1515,6 +1518,7 @@ mod tests { L10nKey::SettingsSearchNewTabPositionKeywords, L10nKey::SettingsSearchNotifyOnCommandFinishKeywords, L10nKey::SettingsSearchNotifyThresholdKeywords, + L10nKey::SettingsSearchOhMyPiKeywords, L10nKey::SettingsSearchOpacityKeywords, L10nKey::SettingsSearchOpenFilesWithKeywords, L10nKey::SettingsSearchOpencodeKeywords, diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index f41f50ff..9dfaae89 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -498,6 +498,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" } @@ -608,6 +609,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" } diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 3a893306..71e694ef 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -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,