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
3 changes: 3 additions & 0 deletions changelog.d/copilot-agent-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Added
- Installed GitHub Copilot CLI is offered in the agent picker for new workspaces, tabs and worktrees
([#1650](https://github.com/devswha/ovis/pull/1650) by @YuuuXie).
1 change: 1 addition & 0 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ Every agent herdr runs shows up with its live status, terminal and alerts. The c
| **Hermes** | Native SQLite session store | — use Terminal |
| **Antigravity (agy)** | **Experimental; macOS untested.** Native ledger, selected by the running process’s held conversation lock. Linux native-tested; Windows uses terminal text. See [Agy behavior and limits](agy-chat.md). | — use Terminal |
| **Letta Code** | The terminal's text. Choose **Letta Code** in **New workspace**, **New tab with agent…** or **New worktree** where `letta` is installed, or run `letta` in a herdr pane's **Terminal**. | — use Terminal |
| **GitHub Copilot CLI** | The terminal's text. Choose **GitHub Copilot** in **New workspace**, **New tab with agent…** or **New worktree** where `copilot` is installed, or run `copilot` in a herdr pane's **Terminal**. | — use Terminal |
| **Anything else** | The terminal's text | — use Terminal |

GJC chat and GJC done alerts use a native extension rather than SDK session queries. From
Expand Down
2 changes: 1 addition & 1 deletion server/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ describe("daily active message", () => {

test("(h) every supported agent kind is a telemetry harness, or other until the Worker lists it", () => {
// the deployed Worker refuses a harness it does not list, so these count as other until a deploy adds them
const undeployed = new Set(["letta"]);
const undeployed = new Set(["letta", "copilot"]);
for (const kind of undeployed) expect(harnessOf(kind)).toBe("other");
const missing = [...SUPPORTED_AGENT_KINDS].filter((kind) => !undeployed.has(kind) && !(TELEMETRY_HARNESSES as readonly string[]).includes(kind));
if (missing.length > 0) throw new Error(`${missing.join(", ")}: add it to TELEMETRY_HARNESSES and deploy the Worker`);
Expand Down
1 change: 1 addition & 0 deletions shared/skill-syntax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const SKILL_SYNTAX: Readonly<Record<string, SkillSyntax>> = {
// `/prompts:x`, `/plugin:x` and `/skill:x` keep the menu open past their `:`
codex: { skillTriggers: ["$"], inline: ["/", "$"], namespaced: true, chipForms: [] },
claude: { skillTriggers: ["/"], inline: ["/"], namespaced: true, chipForms: [] },
copilot: DEFAULT_SKILL_SYNTAX,
pi: { skillTriggers: ["/"], inline: [], namespaced: true, chipForms: [] },
omp: { skillTriggers: ["/"], inline: ["/"], namespaced: true, chipForms: [] },
gjc: { skillTriggers: ["/"], inline: [], namespaced: true, chipForms: [] },
Expand Down
3 changes: 2 additions & 1 deletion shared/supported-agents.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/**
* The agent kinds this app offers to start, where they are installed: the ones docs/guide.md lists
* under Supported agents. herdr's manifests name more; any of those still runs from a pane's shell.
* Picker availability does not promise native Chat or a command catalog: Copilot uses terminal text.
*
* Its own file: the browser imports the value, and shared/protocol.ts reads `process.env` as it loads.
*/
export const SUPPORTED_AGENT_KINDS: ReadonlySet<string> = new Set(["claude", "codex", "omp", "omo", "gjc", "pi", "opencode", "devin", "grok", "hermes", "agy", "letta"]);
export const SUPPORTED_AGENT_KINDS: ReadonlySet<string> = new Set(["claude", "codex", "copilot", "omp", "omo", "gjc", "pi", "opencode", "devin", "grok", "hermes", "agy", "letta"]);
2 changes: 2 additions & 0 deletions src/lib/agentLaunchMode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ it("starts Codex with a sandbox and an approval policy together", () => {
it("leaves other agents and a plain shell alone", () => {
const set = { claudeLaunchMode: "plan", codexLaunchMode: "fullAccess" } as const;
expect(launchArgs("omp", set)).toBeUndefined();
expect(launchAgent("copilot", set, { prompt: "Fix it" })).toEqual({ kind: "copilot" });
expect(firstPromptDelivery("copilot", "Fix it")).toBe("pending");
expect(launchAgent("gemini", set)).toEqual({ kind: "gemini" });
expect(launchAgent("", set)).toBeNull();
});
Expand Down
26 changes: 26 additions & 0 deletions src/lib/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,32 @@ afterEach(() => {
jest.useRealTimers();
});

it("offers installed Copilot in agent menus while filtering missing and unsupported agents", async () => {
const agents = [
{ kind: "copilot", label: "GitHub Copilot", installed: true },
{ kind: "hermes", label: "Hermes", installed: false },
{ kind: "unknown", label: "Unknown", installed: true },
{ kind: "claude", label: "Claude Code" },
];
globalThis.fetch = (async (_url: string) => Response.json({ agents })) as typeof fetch;

expect(await fetchAgentKinds()).toEqual([agents[0]!, agents[3]!]);
agents[0]!.installed = false;
expect(await fetchAgentKinds()).toEqual([agents[3]!]);
});

it("offers Copilot from an older remote bridge without installation metadata", async () => {
const requests: string[] = [];
const copilot = { kind: "copilot", label: "GitHub Copilot" };
globalThis.fetch = (async (url: string) => {
requests.push(url);
return Response.json({ agents: [copilot, { kind: "hermes", label: "Hermes", installed: false }] });
}) as typeof fetch;

expect(await fetchAgentKinds("remote-pc")).toEqual([copilot]);
expect(requests).toEqual(["/api/machines/remote-pc/agents"]);
});

it("refuses a file over the attachment limit before any of it is sent, and sends one at the limit", async () => {
const requests: string[] = [];
globalThis.fetch = (async (url: string) => {
Expand Down
Loading