Skip to content
Merged
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Project conventions, architecture, commands and hard-won gotchas live in CLAUDE.md. Read it before changing anything.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ All notable changes to AngKorGit are documented here. The format follows

## [Unreleased]

### Added
- **GitHub Copilot CLI as an installed AI provider.** AngKorGit detects the
`copilot` binary, including WinGet installs on Windows, and runs prompts through
the user's existing Copilot login and quota with optional model overrides.

### Fixed
- **"Show in file manager" on Windows opens the file's folder again.** Explorer was
handed a path with forward slashes, quoted as a whole together with its `/select`
Expand Down
11 changes: 7 additions & 4 deletions CLAUDE.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ It's free, MIT licensed, and there is no account, no telemetry and no cloud. Eve

**Remotes and accounts.** Fetch, pull and push through the same credential chain git uses. SSH keys and access tokens, several accounts on one host, identity profiles stored per repository and never in your global gitconfig. Pull requests from GitHub, GitLab and Bitbucket: list, check out, create, pick reviewers. Commit signing through your existing git config.

**AI, if you want it.** A commit message from the staged diff, a plain explanation of a commit or a conflict, a review of what you are about to commit, a pull request description. It uses the AI CLI you already log into (Claude Code, Codex, Gemini CLI, OpenCode), or an API key, or Ollama on your own machine. Requests go straight from your computer to the provider you chose. Every one of them has a Stop button.
**AI, if you want it.** A commit message from the staged diff, a plain explanation of a commit or a conflict, a review of what you are about to commit, a pull request description. It uses the AI CLI you already log into (Claude Code, GitHub Copilot CLI, Codex, Gemini CLI, OpenCode, Antigravity), or an API key, or Ollama on your own machine. Requests go straight from your computer to the provider you chose. Every one of them has a Stop button.

**Keyboard first.** ⌘K opens a palette with every command in the app, shortcuts are printed next to menu items, Escape closes exactly one thing at a time.

Expand Down
7 changes: 4 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ For transparency, the app's security-relevant surface is:
app's local settings (the webview's local storage on your machine), **not** in
the OS keychain, and are sent only to the provider you configured. Prefer the
installed-CLI or local-model providers if you'd rather store no key at all.
- **AI CLIs** — if you select an installed AI CLI (Claude Code, Codex, Gemini
CLI, OpenCode), AngKorGit runs that binary as a local subprocess with your
user's permissions. Only a fixed allowlist of known CLI programs can be run.
- **AI CLIs** — if you select an installed AI CLI (Claude Code, GitHub Copilot
CLI, Codex, Gemini CLI, OpenCode, Antigravity), AngKorGit runs that binary as a
local subprocess with your user's permissions. Only a fixed allowlist of known
CLI programs can be run.
- **Network** — outbound only: git remotes you configure, Gravatar (avatar
lookup by email hash), the AI provider you explicitly configure, and the
updater, which checks GitHub Releases for a new signed build shortly after
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src-tauri/src/ai_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::error::{AppError, AppResult};

const AGENTS: &[(&str, &str, &str)] = &[
("claude", "Claude Code", "claude"),
("copilot", "GitHub Copilot CLI", "copilot"),
("codex", "Codex CLI", "codex"),
("gemini", "Gemini CLI", "gemini"),
("opencode", "OpenCode", "opencode"),
Expand Down Expand Up @@ -105,6 +106,12 @@ pub(crate) fn search_path(extra: Option<&Path>) -> std::ffi::OsString {
if let Some(appdata) = std::env::var_os("APPDATA") {
push(&mut dirs, PathBuf::from(appdata).join("npm"));
}
if let Some(local_appdata) = std::env::var_os("LOCALAPPDATA") {
push(
&mut dirs,
PathBuf::from(local_appdata).join("Microsoft/WinGet/Links"),
);
}
}
std::env::join_paths(dirs).unwrap_or_default()
}
Expand Down Expand Up @@ -366,6 +373,14 @@ mod tests {
assert!(err.is_err());
}

#[test]
fn accepts_github_copilot_cli_program() {
assert!(is_supported("copilot"));
assert!(is_supported("/usr/local/bin/copilot"));
#[cfg(windows)]
assert!(is_supported(r"C:\Program Files\GitHub Copilot\copilot.exe"));
}

#[cfg(unix)]
fn fake_agent(dir: &Path, body: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/features/settings/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ function CliAgentPicker() {
<SettingEmpty
icon={<SquareTerminal className="size-4" />}
title="No AI CLI found"
description="Install Claude Code, Codex CLI, Gemini CLI, OpenCode or Antigravity CLI, then scan again."
description="Install Claude Code, GitHub Copilot CLI, Codex CLI, Gemini CLI, OpenCode or Antigravity CLI, then scan again."
action={
<Button variant="secondary" size="sm" onClick={() => void scan()}>
<RefreshCw className="size-3.5" /> Scan again
Expand Down Expand Up @@ -1322,7 +1322,7 @@ export function SettingsDialog() {
title="Provider"
description={
settings.ai.provider === 'cli'
? 'Uses an AI CLI already installed on this machine — Claude Code, Codex, Gemini CLI, OpenCode or Antigravity — with its own login and quota. No API key needed.'
? 'Uses an AI CLI already installed on this machine — Claude Code, GitHub Copilot CLI, Codex, Gemini CLI, OpenCode or Antigravity — with its own login and quota. No API key needed.'
: 'Used for commit messages, diff explanations, conflict help and reviews. Local models via Ollama or LM Studio need no API key.'
}
action={
Expand Down
8 changes: 5 additions & 3 deletions apps/website/src/components/AiSection.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Commit from './Commit.astro';

const providers = [
{ name: 'Claude Code', kind: 'CLI you already log into' },
{ name: 'GitHub Copilot CLI', kind: 'CLI you already log into' },
{ name: 'Codex', kind: 'CLI you already log into' },
{ name: 'Gemini CLI', kind: 'CLI you already log into' },
{ name: 'OpenCode', kind: 'CLI you already log into' },
Expand All @@ -22,8 +23,9 @@ const providers = [
<h2 class="display mt-3 text-4xl leading-[1.02] sm:text-5xl">Uses the AI you <em>already</em> have.</h2>
<div class="mt-6 space-y-4 text-[17px] leading-relaxed text-muted">
<p>
AngKorGit doesn't ship a model or a token wallet. If you've got Claude Code, Codex, Gemini CLI or OpenCode
installed, it finds the binary and uses your own login and quota. No key to paste.
AngKorGit doesn't ship a model or a token wallet. If you've got Claude Code, GitHub Copilot CLI, Codex,
Gemini CLI, OpenCode or Antigravity installed, it finds the binary and uses your own login and quota. No
key to paste.
</p>
<p>
Or point it at an API key, or at Ollama on your own machine. Whatever you pick, requests go straight from
Expand All @@ -49,7 +51,7 @@ const providers = [
<p class="text-[10px] uppercase tracking-[0.12em] text-faint">Detected on this machine</p>
<ul class="mt-2 grid gap-1.5 sm:grid-cols-2">
{
providers.slice(0, 4).map((p, i) => (
providers.slice(0, 5).map((p, i) => (
<li class:list={['flex items-center gap-2 rounded-md border px-2.5 py-2', i === 0 ? 'border-primary/50 bg-primary/10' : 'border-border-subtle bg-background']}>
<span class="flex h-6 w-6 items-center justify-center rounded-md bg-surface-raised font-mono text-[10px] text-muted">&gt;_</span>
<span class="flex min-w-0 flex-col leading-tight">
Expand Down
2 changes: 1 addition & 1 deletion docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ AngKorGit follows Clean Architecture with feature-based folders. Dependencies po

**Conflict resolution as data.** Conflicted files are parsed into text/conflict blocks (`parseConflicts`), the resolver mutates block resolutions, and `serializeResolution` writes the result. Unresolved blocks re-emit their markers, so a half-finished session never destroys data.

**AI is an adapter registry.** Features call capabilities (`generateCommitMessage`, `explainConflict`, …) against the `AiProvider` interface. API providers (OpenAI, Anthropic, Gemini, Ollama, LM Studio) are created from config; HTTP goes through an injected transport implemented by a Rust proxy (no CORS, keys stay out of webview fetch). The `cli` provider is different: it runs an AI CLI already installed on the machine (Claude Code, Codex, Gemini CLI, OpenCode) as an allowlisted local subprocess via `ai_cli.rs` — the user's own login and quota, no API key. Adding an API provider touches one file; adding a CLI agent touches `cliAgents.ts` plus the `ai_cli.rs` allowlist.
**AI is an adapter registry.** Features call capabilities (`generateCommitMessage`, `explainConflict`, …) against the `AiProvider` interface. API providers (OpenAI, Anthropic, Gemini, Ollama, LM Studio) are created from config; HTTP goes through an injected transport implemented by a Rust proxy (no CORS, keys stay out of webview fetch). The `cli` provider is different: it runs an AI CLI already installed on the machine (Claude Code, GitHub Copilot CLI, Codex, Gemini CLI, OpenCode, Antigravity) as an allowlisted local subprocess via `ai_cli.rs` — the user's own login and quota, no API key. Adding an API provider touches one file; adding a CLI agent touches `cliAgents.ts` plus the `ai_cli.rs` allowlist.

**Credentials are layered, host-scoped, and never global.** App-managed accounts (tokens in the OS keyring under AngKorGit's own service, matched to remotes by host) come first, then SSH agent/keys, then the system `git credential` stack — so a GitLab token is never offered to GitHub. The same philosophy applies to committer identity: profiles apply to a repository's local config only, never the shared global gitconfig other tools fight over.

Expand Down
7 changes: 4 additions & 3 deletions docs/Development.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ The engine lives in `apps/desktop/src-tauri/src/core/`, one module per domain ar
2. Register it in `createAiProvider` and `AI_PROVIDER_PRESETS`.
3. Done — settings UI, capabilities and transport pick it up automatically.

Installed AI-CLI agents (Claude Code, Codex, Gemini CLI, OpenCode) follow a different
path: add the agent's argv/stdin shape in `packages/core/src/ai/cliAgents.ts` and its
binary to the allowlist in `apps/desktop/src-tauri/src/ai_cli.rs`.
Installed AI-CLI agents (Claude Code, GitHub Copilot CLI, Codex, Gemini CLI,
OpenCode, Antigravity) follow a different path: add the agent's argv/stdin shape
in `packages/core/src/ai/cliAgents.ts` and its binary to the allowlist in
`apps/desktop/src-tauri/src/ai_cli.rs`.

## Release

Expand Down
2 changes: 1 addition & 1 deletion docs/Roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ direction.
- [x] Diff: inline & side-by-side, syntax highlight, word diff, image diff, find in diff (⌘F), minimap, previous/next change and file navigation (N/P, [/]), opens directly at the first change (no scroll animation), reloads live as the file changes on disk, file history one click from the header; → / ↑ ↓ / ← walk from the graph into a commit's files and back
- [x] Settings: sixteen themes (Angkor Dusk default) with accents & zoom, identity profiles (repo-local) with linked accounts, SSH key management & generation, hosting accounts with verified tokens (Secret Service on Linux, missing tokens flagged), AI providers & commit style, keyboard reference
- [x] Sidebar: accordion sections with pinned headers and collapse-all, row menus on hover and right-click everywhere, empty-state cards; graph display options and column headers; welcome page with keyboard navigation and missing-folder detection
- [x] AI: provider-agnostic (OpenAI, Anthropic, Gemini, Ollama, LM Studio) plus installed AI CLIs (Claude Code, Codex, Gemini CLI, OpenCode, Antigravity) — commit messages, diff/conflict explanations, PR descriptions, staged-change review with team conventions (global + per-repo `.angkorgit/review.md`), background execution with stop, full-size reading views
- [x] AI: provider-agnostic (OpenAI, Anthropic, Gemini, Ollama, LM Studio) plus installed AI CLIs (Claude Code, GitHub Copilot CLI, Codex, Gemini CLI, OpenCode, Antigravity) — commit messages, diff/conflict explanations, PR descriptions, staged-change review with team conventions (global + per-repo `.angkorgit/review.md`), background execution with stop, full-size reading views
- [x] Undo/redo for recent operations; drag-and-drop merge/rebase
- [x] Auto-update: pull-based from GitHub releases, signature-verified
- [x] Commit signing: SSH and GPG, driven by existing git config (commit.gpgSign, gpg.format, user.signingKey) — covers commit, amend, merge
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/ai/cliAgents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ export const CLI_AGENTS: Record<CliAgentId, CliAgentSpec> = {
promptVia: 'stdin',
args: (model) => ['-p', '--output-format', 'text', ...(model ? ['--model', model] : [])],
},
copilot: {
id: 'copilot',
label: 'GitHub Copilot CLI',
binary: 'copilot',
promptVia: 'arg',
args: (model) => [
'--silent',
'--stream',
'off',
'--no-ask-user',
'--no-color',
'--output-format',
'text',
...(model ? ['--model', model] : []),
'--prompt',
],
},
codex: {
id: 'codex',
label: 'Codex CLI',
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/ai/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export const AI_PROVIDER_PRESETS: Record<
AiProviderKind,
{ label: string; defaultModel: string; needsApiKey: boolean; defaultBaseUrl: string }
> = {
cli: { label: 'Installed AI CLI (Claude Code, Codex…)', defaultModel: '', needsApiKey: false, defaultBaseUrl: '' },
cli: { label: 'Installed AI CLI (Claude, Copilot, Codex…)', defaultModel: '', needsApiKey: false, defaultBaseUrl: '' },
openai: { label: 'OpenAI', defaultModel: 'gpt-4o-mini', needsApiKey: true, defaultBaseUrl: 'https://api.openai.com/v1' },
anthropic: { label: 'Anthropic', defaultModel: 'claude-sonnet-5', needsApiKey: true, defaultBaseUrl: 'https://api.anthropic.com' },
gemini: { label: 'Google Gemini', defaultModel: 'gemini-2.0-flash', needsApiKey: true, defaultBaseUrl: 'https://generativelanguage.googleapis.com' },
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/ai/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export type AiProviderKind =
| 'ollama'
| 'lmstudio';

export type CliAgentId = 'claude' | 'codex' | 'gemini' | 'opencode' | 'antigravity';
export type CliAgentId = 'claude' | 'copilot' | 'codex' | 'gemini' | 'opencode' | 'antigravity';

export interface CliAgentInfo {
id: CliAgentId;
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/cliAgents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,21 @@ describe('cli agent specs', () => {
expect(CLI_AGENTS.opencode.promptVia).toBe('arg');
});

it('copilot runs one silent, non-interactive prompt without asking the user', () => {
expect(CLI_AGENTS.copilot.promptVia).toBe('arg');
expect(CLI_AGENTS.copilot.args('')).toEqual([
'--silent',
'--stream',
'off',
'--no-ask-user',
'--no-color',
'--output-format',
'text',
'--prompt',
]);
expect(CLI_AGENTS.copilot.args('gpt-5.4')).toContain('gpt-5.4');
});

it('antigravity puts -p last so the appended prompt becomes its value', () => {
expect(CLI_AGENTS.antigravity.promptVia).toBe('arg');
expect(CLI_AGENTS.antigravity.args('').at(-1)).toBe('-p');
Expand Down
Loading