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 NotchBuddy/Sources/App/PillCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct PillDefinition {
case "integration_claude": return "Claude Code"
case "agent_cursor": return "Cursor"
case "agent_codex": return "Codex"
case "agent_opencode": return "opencode"
default: return "Agent"
}
}
Expand All @@ -59,6 +60,10 @@ enum PillCatalog {
// ── Agents ───────────────────────────────────────────────────────────
.init(id: "agent_gemini", name: "Gemini CLI", color: "#8AB4F8",
category: .agent, subtitle: "Agent", source: .agent, githubOnly: true),
// No githubOnly: the bridge is an external plugin spawning the relay
// directly, so it works in every build with no hook installer.
.init(id: "agent_opencode", name: "opencode", color: "#EAB308",
category: .agent, subtitle: "Agent", source: .agent),
// ── AI for the chat ──────────────────────────────────────────────────
.init(id: "ai_anthropic", name: "Anthropic", color: ChatProvider.anthropic.accentHex,
category: .ai, subtitle: "Chat", source: .n8n),
Expand Down
2 changes: 1 addition & 1 deletion docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ A **declared pill** is a catalog entry (`PillCatalog.swift`) that has been enabl

A catalog pill that is not checked in Settings behaves like any other agent: it gets an automatic pill when a session starts, and that pill is removed when the session ends.

The GitHub build exposes Gemini CLI (`agent_gemini`) and Antigravity (`agent_antigravity`) in Settings → Active pills. Cursor (`agent_cursor`) and Codex (`agent_codex`, GitHub build only) are there too — their pills can be declared and set as the main pill; session support is coming in a future version.
The GitHub build exposes Gemini CLI (`agent_gemini`) and Antigravity (`agent_antigravity`) in Settings → Active pills. Cursor (`agent_cursor`) and Codex (`agent_codex`, GitHub build only) are there too — their pills can be declared and set as the main pill; session support is coming in a future version. opencode (`agent_opencode`, every build) is there as well — it needs no hook installer: its bridge is an external plugin that spawns the relay with `--agent opencode` directly.

## Real-world examples

Expand Down
21 changes: 21 additions & 0 deletions windows/src/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ const task = (
/** AgentTask.integrationAgents — same ids, names and colours as macOS. */
export const INTEGRATION_AGENTS: AgentTask[] = [
task("integration_claude", "VS Code", "#F5F6F8", "claudeCode"),
// Third-party coding agent bridged over the hook relay with coucou_agent.
// Declared so it can be pinned in Settings → Active pills; sessions still
// arrive as dynamic events exactly like undeclared agents.
task("agent_opencode", "opencode", "#EAB308", "agent"),
task("integration_resend", "Resend", "#22C55E", "n8n"),
task("integration_n8n", "n8n", "#F29B38", "n8n"),
task("integration_vercel", "Vercel", "#7C5CFF", "n8n"),
Expand All @@ -71,6 +75,7 @@ export const INTEGRATION_AGENTS: AgentTask[] = [
export const TOGGLEABLE_INTEGRATION_IDS = [
"integration_resend", "integration_n8n", "integration_vercel", "integration_github",
"integration_notion", "integration_calcom", "integration_stripe",
"agent_opencode",
];

/** What an integration poller last reported. */
Expand Down Expand Up @@ -231,6 +236,22 @@ class AppState {
removeTask(id: string) {
const idx = this.tasks.findIndex((t) => t.id === id);
if (idx < 0) return;
// Declared + active: reset, never remove (mirror of AppState.removeTask on
// macOS). The user pinned this pill in Settings → Active pills; a finished
// session parks it as idle instead of taking it away.
const isActiveDecl =
INTEGRATION_AGENTS.some((t) => t.id === id) && this.settings.activeIntegrations.includes(id);
if (isActiveDecl) {
const t = this.tasks[idx];
t.state = "idle";
t.steps = [];
t.stepIndex = 0;
t.pillBadge = null;
const catalogName = INTEGRATION_AGENTS.find((t) => t.id === id)?.name;
if (catalogName) t.name = catalogName;
this.notify();
return;
}
this.tasks.splice(idx, 1);
if (this.focusId === id) this.focusId = this.tasks[0]?.id ?? "integration_claude";
this.notify();
Expand Down
46 changes: 45 additions & 1 deletion windows/src/settings/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import "./settings.css";
import { Bridge, onEvent, type HookStatus } from "../core/bridge";
import { DEFAULT_SETTINGS, type Settings } from "../core/state";
import { DEFAULT_SETTINGS, INTEGRATION_AGENTS, type Settings } from "../core/state";
import { h, clear } from "../views/dom";

let settings: Settings = { ...DEFAULT_SETTINGS };
Expand Down Expand Up @@ -358,6 +358,49 @@ function integrationsSection(present: Record<string, boolean>): HTMLElement {
return h("section", {}, h("h2", {}, h("span", { text: "Integrations" })), note, list);
}

// ── Agents section ────────────────────────────────────────────────────────────

function agentsSection(): HTMLElement {
// Third-party coding agents get a pill automatically while a session sends
// hook events. Pinning one here keeps it after the session ends (idle)
// instead of removing it. Shares the 4-pill budget with Integrations.
const defs = INTEGRATION_AGENTS.filter((t) => t.source === "agent");
const list = h("div", { style: "display:flex;flex-direction:column;gap:14px" });

for (const def of defs) {
const active = settings.activeIntegrations.includes(def.id);
const sw = h("button", { class: active ? "switch on" : "switch" });
sw.addEventListener("click", () => {
const on = settings.activeIntegrations.includes(def.id);
if (on) {
settings.activeIntegrations = settings.activeIntegrations.filter((x) => x !== def.id);
} else {
if (settings.activeIntegrations.length >= MAX_ACTIVE) return;
settings.activeIntegrations = [...settings.activeIntegrations, def.id];
}
sw.classList.toggle("on", !on);
void save();
});

list.append(
h("div", { style: "display:flex;gap:12px;align-items:flex-start" },
h("div", { style: "display:flex;align-items:center;gap:8px;min-width:132px;padding-top:4px" },
sw,
h("i", { class: "dot", style: `background:${def.color}` }),
h("span", { style: "font-size:12.5px", text: def.name }),
),
h("div", { class: "hint", style: "padding-top:4px", text: "No Coucou setup needed — events arrive over the hook relay." }),
),
);
}

return h("section", {},
h("h2", {}, h("span", { text: "Agents" })),
h("div", { class: "hint", text: "Agent pills appear on their own while a session is active. Pin one to keep it afterwards." }),
list,
);
}

// ── General section ───────────────────────────────────────────────────────────

function generalSection(): HTMLElement {
Expand Down Expand Up @@ -444,6 +487,7 @@ async function main() {
claudeSection(status),
apiSection(hasKey),
integrationsSection(present),
agentsSection(),
generalSection(),
h("div", {
class: "hint",
Expand Down