Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Antigravity CLI joins the provider list. Install it with `curl -fsSL https://antigravity.google/cli/install.sh | bash`, then run `agy` to sign in. MonoCode uses its official streaming protocol for live sessions, model discovery, persistent conversations, skills, and staged image, video, audio, document, and file attachments.
- Settings → General → Diff view: Editor or Unified. Unified stacks every working-tree change in one **Changes** tab — GitHub-style review, editor syntax colours, sticky file headers and line numbers, and a single horizontal scroll that stops at the end of the line. Editor keeps the previous per-file working-tree tabs.

### Fixed

- Long Antigravity turns can run for up to 30 minutes instead of being cut off by the CLI's five-minute print-mode default, and a failed turn is shown only once in the transcript.

## [0.1.29] - 2026-09-02

### Added
Expand Down
2 changes: 1 addition & 1 deletion NOTICE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
MonoCode is not affiliated with, endorsed by, or sponsored by the makers of the
agent harnesses it can drive.

Provider marks that appear in the UI (including Claude, Codex, Cursor, Grok, OpenCode, Pi, omp, and fx) are trademarks of their respective owners and are used only to identify those products.
Provider marks that appear in the UI (including Claude, Codex, Cursor, Grok, Antigravity, OpenCode, Pi, omp, and fx) are trademarks of their respective owners and are used only to identify those products.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<img src="docs/screenshot.jpg" alt="MonoCode with sessions, agent chat, diffs, terminal, and editor" width="920" />
</p>

Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCode, Pi, omp, and fx. If they’re installed and logged in, MonoCode can run them. Tabs are sessions. The composer is the input. MonoCode does not sell tokens.
Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, Antigravity, OpenCode, Pi, omp, and fx. If they’re installed and logged in, MonoCode can run them. Tabs are sessions. The composer is the input. MonoCode does not sell tokens.

## Install

Expand All @@ -22,6 +22,7 @@ Works with your subscriptions on Claude Code, Codex, Cursor, Grok Build, OpenCod
> - [Codex](https://developers.openai.com/codex/cli) - `codex login`
> - [Cursor CLI](https://cursor.com/cli) - `agent login`
> - [Grok Build](https://docs.x.ai/build/overview) - `curl -fsSL https://x.ai/cli/install.sh | bash` then `grok login`
> - [Antigravity CLI](https://antigravity.google/docs/cli/overview) - `curl -fsSL https://antigravity.google/cli/install.sh | bash` then run `agy` to sign in
> - [OpenCode](https://opencode.ai) - `opencode auth login`
> - [Pi](https://pi.dev/) - `npm install -g @earendil-works/pi-coding-agent`
> - [omp](https://omp.sh) - `curl -fsSL https://omp.sh/install | sh`
Expand Down
98 changes: 92 additions & 6 deletions src-tauri/src/harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,19 @@ pub fn harness_resolve_grok() -> Result<CursorBinary, String> {
})
}

/// Resolve the Google Antigravity CLI (`agy`).
#[tauri::command(async)]
pub fn harness_resolve_antigravity() -> Result<CursorBinary, String> {
resolve_antigravity()
.map(|path| CursorBinary {
path: path.to_string_lossy().into_owned(),
})
.ok_or_else(|| {
"Antigravity CLI not found. Install it with `curl -fsSL https://antigravity.google/cli/install.sh | bash` and run `agy login`, then retry."
.into()
})
}

/// Bind an ephemeral loopback port for `opencode serve`.
#[tauri::command]
pub fn harness_free_port() -> Result<u16, String> {
Expand Down Expand Up @@ -634,6 +647,10 @@ const EXEC_ALLOWED_ARGS: &[&[&str]] = &[
&["models"],
&["status", "--json"],
&["agent", "list"],
&["--output-format", "json", "models"],
&["--output-format", "json", "agents"],
&["--output-format", "json", "-p", "/usage"],
&["-p", "/usage"],
];

fn exec_args_allowed(args: &[String]) -> bool {
Expand All @@ -655,6 +672,7 @@ fn is_resolved_harness_binary(command: &str) -> bool {
resolve_omp(),
resolve_fx(),
resolve_grok(),
resolve_antigravity(),
]
.into_iter()
.flatten()
Expand Down Expand Up @@ -688,11 +706,15 @@ fn exec_capture(command: &str, args: &[String], cwd: Option<&str>) -> Result<Str
.stdout(Stdio::piped())
.stderr(Stdio::piped());
prepare_child(&mut cmd, command);
if let Some(dir) = cwd {
let workdir = expand_home(dir);
if workdir.is_dir() {
cmd.current_dir(workdir);
}
let workdir = cwd
.map(expand_home)
.filter(|path| path.is_dir())
.or_else(|| dirs_home().map(PathBuf::from).filter(|path| path.is_dir()));
if let Some(workdir) = workdir {
// One-shot probes must never inherit the GUI process directory. On
// macOS that can be `/`, which lets agy treat the whole machine as its
// workspace when a caller forgot to provide a cwd.
cmd.current_dir(workdir);
}

let child = cmd
Expand All @@ -704,7 +726,7 @@ fn exec_capture(command: &str, args: &[String], cwd: Option<&str>) -> Result<Str
let _ = tx.send(child.wait_with_output());
});

match rx.recv_timeout(Duration::from_secs(15)) {
match rx.recv_timeout(Duration::from_secs(90)) {
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
if output.status.success() || !stdout.trim().is_empty() {
Expand Down Expand Up @@ -935,6 +957,7 @@ fn is_harness_argv_token(part: &str) -> bool {
| "codex"
| "opencode"
| "grok"
| "agy"
| "omp"
| "fx"
| "pi"
Expand Down Expand Up @@ -1353,6 +1376,51 @@ fn resolve_grok() -> Option<PathBuf> {
candidates.into_iter().find(|path| is_grok_agent(path))
}

fn resolve_antigravity() -> Option<PathBuf> {
let home = dirs_home().map(PathBuf::from);
let mut candidates: Vec<PathBuf> = Vec::new();

// The official installer uses ~/.local/bin. Keep this stable shim ahead
// of PATH so upgrades do not turn into a new macOS TCC identity.
if let Some(home) = &home {
for name in antigravity_binary_names() {
candidates.push(home.join(".local/bin").join(name));
candidates.push(home.join(".gemini/bin").join(name));
candidates.push(home.join(".antigravity/bin").join(name));
}
#[cfg(target_os = "windows")]
for name in antigravity_binary_names() {
candidates.push(home.join("AppData/Local/Antigravity/bin").join(name));
}
}
#[cfg(target_os = "macos")]
for name in antigravity_binary_names() {
candidates.push(PathBuf::from("/opt/homebrew/bin").join(name));
}
for name in antigravity_binary_names() {
candidates.push(PathBuf::from("/usr/local/bin").join(name));
candidates.push(PathBuf::from("/usr/bin").join(name));
candidates.push(PathBuf::from("/snap/bin").join(name));
}
for name in antigravity_binary_names() {
if let Some(from_shell) = which_via_login_shell(name) {
candidates.push(from_shell);
}
}

candidates.into_iter().find(|path| path.is_file())
}

#[cfg(target_os = "windows")]
fn antigravity_binary_names() -> &'static [&'static str] {
&["agy.exe", "agy"]
}

#[cfg(not(target_os = "windows"))]
fn antigravity_binary_names() -> &'static [&'static str] {
&["agy"]
}

fn is_pi_coding_agent(path: &Path) -> bool {
if !path.is_file() {
return false;
Expand Down Expand Up @@ -2308,6 +2376,11 @@ mod tests {
assert_eq!(command_basename("/Users/me/.grok/bin/grok"), "grok");
}

#[test]
fn antigravity_uses_the_official_cli_name() {
assert_eq!(antigravity_binary_names(), &["agy"]);
}

#[test]
fn passwd_identity_resolves_the_current_user() {
let id = passwd_identity().expect("passwd");
Expand All @@ -2333,6 +2406,16 @@ mod exec_allowlist_tests {
assert!(exec_args_allowed(&args(&["models"])));
assert!(exec_args_allowed(&args(&["status", "--json"])));
assert!(exec_args_allowed(&args(&["agent", "list"])));
assert!(exec_args_allowed(&args(&[
"--output-format",
"json",
"models"
])));
assert!(exec_args_allowed(&args(&[
"--output-format",
"json",
"agents"
])));
}

#[test]
Expand Down Expand Up @@ -2449,6 +2532,9 @@ mod reap_logic_tests {
"/opt/homebrew/bin/node /Users/n/.local/share/cursor-agent/versions/x/index.js worker-server"
));
assert!(looks_like_harness_argv("/Users/n/.local/bin/claude --help"));
assert!(looks_like_harness_argv(
"/Users/n/.local/bin/agy --input-format stream-json"
));
assert!(!looks_like_harness_argv("tmux new -s work"));
assert!(!looks_like_harness_argv("npm start"));
assert!(!looks_like_harness_argv(
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub fn run() {
harness::harness_resolve_pi,
harness::harness_resolve_fx,
harness::harness_resolve_grok,
harness::harness_resolve_antigravity,
harness::harness_free_port,
harness::harness_spawn,
harness::harness_write,
Expand Down
47 changes: 47 additions & 0 deletions src-tauri/src/skills.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ pub(crate) fn list_skills_from(project: &Path, home: Option<&Path>) -> Vec<Disco

// Highest priority first so later roots cannot replace a name.
add_root(project.join(".agents/skills"), "project", "agents");
add_root(project.join(".agent/skills"), "project", "agents");
if let Some(home) = home {
add_root(home.join(".agents/skills"), "user", "agents");
add_root(home.join(".agent/skills"), "user", "agents");
}

for (dir, source) in [
Expand All @@ -63,6 +65,8 @@ pub(crate) fn list_skills_from(project: &Path, home: Option<&Path>) -> Vec<Disco
(".omp/skills", "omp"),
(".fx/skills", "fx"),
(".grok/skills", "grok"),
(".antigravity/skills", "antigravity"),
(".gemini/skills", "antigravity"),
] {
add_root(project.join(dir), "project", source);
if let Some(home) = home {
Expand All @@ -72,6 +76,26 @@ pub(crate) fn list_skills_from(project: &Path, home: Option<&Path>) -> Vec<Disco
if let Some(home) = home {
add_root(home.join(".pi/agent/skills"), "user", "pi");
add_root(home.join(".omp/agent/skills"), "user", "omp");
// Antigravity CLI keeps global skills alongside its Gemini-compatible
// configuration and builtin skills. Workspace skills are discovered via
// `.agents/skills`, `.agent/skills`, `.antigravity/skills`, and `.gemini/skills`.
add_root(home.join(".gemini/config/skills"), "user", "antigravity");
add_root(
home.join(".gemini/antigravity-cli/skills"),
"user",
"antigravity",
);
add_root(
home.join(".gemini/antigravity-cli/builtin/skills"),
"builtin",
"antigravity",
);
add_root(
home.join(".gemini/antigravity/builtin/skills"),
"builtin",
"antigravity",
);
add_root(home.join(".antigravity/skills"), "user", "antigravity");
for (root, scope, namespace) in claude_plugin_skill_roots(home, project) {
add_namespaced_root(&mut by_name, root, scope, "claude", &namespace);
}
Expand Down Expand Up @@ -634,6 +658,29 @@ mod tests {
assert_eq!(user_skill.scope, "user");
}

#[test]
fn discovers_antigravity_global_skill_roots() {
let project = tmp("proj-antigravity");
let home = tmp("home-antigravity");
write_skill(
&home.0.join(".gemini/config/skills"),
"review-code",
"---\nname: review-code\ndescription: Review code\n---\n",
);
write_skill(
&home.0.join(".gemini/antigravity-cli/skills"),
"ship-feature",
"---\nname: ship-feature\ndescription: Ship a feature\n---\n",
);

let skills = list_skills_from(&project.0, Some(&home.0));
for name in ["review-code", "ship-feature"] {
let skill = skills.iter().find(|skill| skill.name == name).unwrap();
assert_eq!(skill.source, "antigravity");
assert_eq!(skill.scope, "user");
}
}

#[test]
fn discovers_installed_claude_plugin_skills() {
let project = tmp("proj-claude-plugin");
Expand Down
10 changes: 7 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ export default function App({
const harnesses = [
...new Set(sessionsRef.current.map((session) => session.harness)),
];
void refreshHarnessCatalogs(harnesses).then(() => {
void refreshHarnessCatalogs(harnesses, projectCwd).then(() => {
setSessions((prev) =>
prev.map((session) => {
if (!isLiveHarness(session.harness)) return session;
Expand All @@ -700,7 +700,7 @@ export default function App({
}),
);
});
}, []);
}, [projectCwd]);

const activeTab = tabs.find((t) => t.id === activeTabId) ?? tabs[0];
const active =
Expand Down Expand Up @@ -756,7 +756,11 @@ export default function App({
const busySessionIds = busySessionIdsRef.current;

const usageProviders = useMemo(() => {
if (active?.harness === "claude" || active?.harness === "codex") {
if (
active?.harness === "claude" ||
active?.harness === "codex" ||
active?.harness === "antigravity"
) {
return [active.harness];
}
return [];
Expand Down
12 changes: 12 additions & 0 deletions src/assets/providers/antigravity.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/chrome/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,7 @@ export function Composer({
<ModelPicker
harness={harness}
model={model}
cwd={executionCwd}
hotkeys={hotkeys && enabled}
onChange={onModelChange}
onClose={() => ref.current?.focus()}
Expand Down
2 changes: 2 additions & 0 deletions src/chrome/HarnessIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
import antigravity from "../assets/providers/antigravity.svg";
import claude from "../assets/providers/claude.svg";
import codex from "../assets/providers/codex.svg";
import cursor from "../assets/providers/cursor.svg";
Expand All @@ -14,6 +15,7 @@ export const HARNESS_ICONS: Record<HarnessId, string> = {
codex,
cursor,
grok,
antigravity,
opencode,
pi,
omp,
Expand Down
6 changes: 4 additions & 2 deletions src/chrome/ModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { MOD } from "../lib/platform";
type Props = {
harness: HarnessId;
model: string;
cwd?: string;
hotkeys?: boolean;
onChange: (harness: HarnessId, model: string) => void;
onClose?: () => void;
Expand All @@ -61,6 +62,7 @@ const MENU_MAX_HEIGHT = 340;
export function ModelPicker({
harness,
model,
cwd,
hotkeys = false,
onChange,
onClose,
Expand Down Expand Up @@ -143,8 +145,8 @@ export function ModelPicker({

useEffect(() => {
if (!open || visibleTab === "favorites") return;
void refreshHarnessCatalogs([visibleTab]);
}, [open, visibleTab]);
void refreshHarnessCatalogs([visibleTab], cwd);
}, [cwd, open, visibleTab]);

useEffect(() => {
const inBlockingUi = (target: EventTarget | null) => {
Expand Down
Loading