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
2 changes: 1 addition & 1 deletion src/components/automations/composer-invocations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export interface ComposerInvocations {
* `isExternalMenuOpen`/`onExternalMenuKeyDown` escape hatch; a selected item
* becomes a badge that serializes to its literal `/cmd` / `$skill` token on save.
* Antigravity's and OpenCode's skills, which their servers never advertise,
* join their commands under `/`.
* and a custom agent's declared skills join their commands under `/`.
*/
export function useComposerInvocations({
editorRef,
Expand Down
10 changes: 9 additions & 1 deletion src/components/chat/composer/invocation-reference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,16 @@ describe("diskSkillTrigger", () => {
expect(diskSkillTrigger("open_code")).toBe("/")
})

// Custom ACP agents (e.g. user-configured Antigravity, or agents with declared
// skill directories) do not advertise skills as ACP commands; their skills
// live on disk and are invoked via `/`.
it("lists custom ACP agents' disk skills under `/`", () => {
expect(diskSkillTrigger("custom:antigravity-b")).toBe("/")
expect(diskSkillTrigger("custom:my-agent")).toBe("/")
})

it("scans nothing for an agent that advertises its skills as commands", () => {
for (const agent of ["claude_code", "kimi_code", "gemini", "custom:x"]) {
for (const agent of ["claude_code", "kimi_code", "gemini"]) {
expect(diskSkillTrigger(agent)).toBeNull()
}
expect(diskSkillTrigger(null)).toBeNull()
Expand Down
26 changes: 21 additions & 5 deletions src/components/chat/composer/invocation-reference.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { KnownInvocations } from "@/lib/invocation-token"
import type {
AgentSkillItem,
AgentType,
AvailableCommandInfo,
import {
isCustomAgentType,
type AgentSkillItem,
type AgentType,
type AvailableCommandInfo,
} from "@/lib/types"

import type { ReferenceAttrs } from "./types"
Expand Down Expand Up @@ -36,12 +37,27 @@ export type InvocationPrefix = "/" | "$"
* configured and MCP-prompt ones), while it still loads every skill, names
* each to the model and gives the model a `skill` tool to load one by id. A
* `/<skill>` the user sends reaches the model as written.
* - Custom ACP agents (`custom:<id>`): `/`. codeg cannot tell whether an
* arbitrary ACP server advertises its skills (Antigravity's
* `agy_acp_server` run as a custom agent never does), so the scan follows the
* user's own declaration instead: a custom agent that declares a dedicated
* skills directory or the shared `.agents/skills` store lists those skills,
* and one that declares neither gets `supported: false` and an empty list. A
* declared skill the agent also advertises as a command is dropped by
* {@link skillsForMenu}, so it is not listed twice.
*/
export function diskSkillTrigger(
agentType: AgentType | null | undefined
): InvocationPrefix | null {
if (!agentType) return null
if (agentType === "codex") return "$"
if (agentType === "antigravity" || agentType === "open_code") return "/"
if (
agentType === "antigravity" ||
agentType === "open_code" ||
isCustomAgentType(agentType)
) {
return "/"
}
return null
}

Expand Down
21 changes: 21 additions & 0 deletions src/components/chat/message-input.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1970,6 +1970,27 @@ describe("MessageInput slash menu for skills an agent does not advertise", () =>
expect(agentSkills).toHaveBeenCalledWith("antigravity", null)
})

it("lists custom ACP agents' skills from disk after their commands", async () => {
agentSkills.mockReturnValue([DEPLOY, SHADOWED])
await mountAndType({
agentType: "custom:antigravity-b",
availableCommands: COMMANDS,
})

const menu = await screen.findByTestId("slash-menu")
const rows = within(menu)
.getAllByRole("button")
.map((row) => row.textContent)
expect(rows).toEqual([
expect.stringMatching(/^\/plan/),
expect.stringMatching(/^\/logout/),
expect.stringMatching(/^\/usage/),
"DeployShip the build",
])
expect(within(menu).queryByText("Plan skill")).toBeNull()
expect(agentSkills).toHaveBeenCalledWith("custom:antigravity-b", null)
})

it("inserts a picked Antigravity skill as its `/` token", async () => {
agentSkills.mockReturnValue([DEPLOY])
const { editor } = await mountAndType(
Expand Down
8 changes: 5 additions & 3 deletions src/components/chat/message-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,11 @@ export function MessageInput({
// Skills read from disk feed the menu only for an agent that does not
// advertise them itself: Codex under `$` (its native command set is very
// small), Antigravity and OpenCode under `/` beside their commands (their
// ACP servers list only commands, never a skill). Every other agent surfaces
// its skills through ACP `availableCommands`, so a disk scan would only
// repeat them — skip it.
// ACP servers list only commands, never a skill), and custom agents under `/`
// from the skill directories their user declared (codeg cannot know what an
// arbitrary ACP server advertises). Every other built-in agent surfaces its
// skills through ACP `availableCommands`, so a disk scan would only repeat
// them — skip it.
const skillTrigger = diskSkillTrigger(agentType)
// Pass the working dir so we see both global skills and folder-scoped
// project skills (e.g. `{folder}/.codex/skills`). Without this, users
Expand Down
Loading