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
28 changes: 28 additions & 0 deletions packages/agents/opencode/src/__tests__/opencode-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ function createMockSdkClient() {
},
app: {
agents: vi.fn().mockResolvedValue({ data: [] }),
skills: vi.fn().mockResolvedValue({ data: [] }),
},
mcp: {
status: vi.fn().mockResolvedValue({ data: {} }),
Expand Down Expand Up @@ -366,6 +367,17 @@ describe("OpenCodeAgent", () => {
expect(call.parts[1]).toEqual({ type: "agent", name: "code-reviewer" });
});

it("should prepend synthetic skill command when skill is provided", async () => {
await agent.connect();

await agent.sendMessage("sess-1", "Hello", { skill: "coding-guidelines" } as never);

const call = mockClient.session.promptAsync.mock.calls[0][0];
expect(call.parts).toHaveLength(2);
expect(call.parts[0]).toEqual({ type: "text", text: "/coding-guidelines", synthetic: true });
expect(call.parts[1]).toEqual({ type: "text", text: "Hello" });
});

it("should include all parts when files and agent are provided", async () => {
await agent.connect();
agent.workspaceFolder = "/ws";
Expand Down Expand Up @@ -684,6 +696,22 @@ describe("OpenCodeAgent", () => {
});
});

describe("getSkills()", () => {
it("should call app.skills() and return mapped skills", async () => {
mockClient.app.skills.mockResolvedValue({
data: [{ name: "coding-guidelines", description: "desc", location: "/skills/coding-guidelines" }],
});
await agent.connect();

const result = await agent.getSkills();

expect(mockClient.app.skills).toHaveBeenCalled();
expect(result).toEqual([
{ name: "coding-guidelines", description: "desc", location: "/skills/coding-guidelines" },
]);
});
});

// ============================================================
// Config API
// ============================================================
Expand Down
9 changes: 9 additions & 0 deletions packages/agents/opencode/src/mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
McpStatus,
MessagePart,
ProviderInfo,
SkillInfo,
TodoItem,
ToolListItem,
} from "@opencodegui/core";
Expand Down Expand Up @@ -129,6 +130,14 @@ export function mapAgents(agents: Agent[]): AgentInfo[] {
return agents.map(mapAgent);
}

export function mapSkills(skills: Array<{ name: string; description: string; location: string }>): SkillInfo[] {
return skills.map((skill) => ({
name: skill.name,
description: skill.description,
location: skill.location,
}));
}

// ============================================================
// Config
// ============================================================
Expand Down
18 changes: 16 additions & 2 deletions packages/agents/opencode/src/opencode-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import type {
ProviderInfo,
QuestionAnswer,
SendMessageOptions,
SkillInfo,
TodoItem,
ToolListItem,
} from "@opencodegui/core";
Expand All @@ -44,6 +45,7 @@ import {
mapProviders,
mapSession,
mapSessions,
mapSkills,
mapTodos,
mapToolIds,
} from "./mappers";
Expand Down Expand Up @@ -239,10 +241,16 @@ export class OpenCodeAgent implements IAgent {
async sendMessage(sessionId: string, text: string, options?: SendMessageOptions): Promise<void> {
const client = this.requireClient();
const parts: Array<
| { type: "text"; text: string }
| { type: "text"; text: string; synthetic?: boolean }
| { type: "file"; mime: string; url: string; filename: string }
| { type: "agent"; name: string }
> = [{ type: "text", text }];
> = [];

if (options?.skill) {
parts.push({ type: "text", text: `/${options.skill}`, synthetic: true });
}

parts.push({ type: "text", text });

if (options?.files) {
for (const file of options.files) {
Expand Down Expand Up @@ -320,6 +328,12 @@ export class OpenCodeAgent implements IAgent {
return mapAgents(response.data!);
}

async getSkills(): Promise<SkillInfo[]> {
const client = this.requireClient();
const response = await client.app.skills();
return mapSkills(response.data!);
}

async getChildSessions(sessionId: string): Promise<ChatSession[]> {
const client = this.requireClient();
const response = await client.session.children({
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/agent.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
ProviderInfo,
QuestionAnswer,
SendMessageOptions,
SkillInfo,
TodoItem,
ToolListItem,
} from "./domain";
Expand Down Expand Up @@ -100,6 +101,7 @@ export interface IAgent {

// --- Agent list (capabilities.subAgent) ---
getAgents(): Promise<AgentInfo[]>;
getSkills(): Promise<SkillInfo[]>;
getChildSessions(sessionId: string): Promise<ChatSession[]>;

// --- Permissions (capabilities.permission) ---
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,12 @@ export type AgentInfo = {
color?: string;
};

export type SkillInfo = {
name: string;
description?: string;
location?: string;
};

// ============================================================
// App Config & Paths
// ============================================================
Expand All @@ -460,6 +466,7 @@ export type SendMessageOptions = {
files?: FileAttachment[];
agent?: string;
primaryAgent?: string;
skill?: string;
};

// ============================================================
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
PermissionResponse,
ProviderInfo,
QuestionAnswer,
SkillInfo,
TodoItem,
} from "./domain";

Expand Down Expand Up @@ -59,6 +60,7 @@ export type UIToHostMessage =
files?: FileAttachment[];
agent?: string;
primaryAgent?: string;
skill?: string;
}
| {
type: "editAndResend";
Expand Down Expand Up @@ -105,6 +107,7 @@ export type UIToHostMessage =
| { type: "getSessionTodos"; sessionId: string }
| { type: "getChildSessions"; sessionId: string }
| { type: "getAgents" }
| { type: "getSkills" }

// --- Model config ---
| { type: "setModel"; model: string }
Expand Down Expand Up @@ -193,6 +196,7 @@ export type HostToUIMessage =

// --- Agent list ---
| { type: "agents"; agents: AgentInfo[] }
| { type: "skills"; skills: SkillInfo[] }

// --- Platform data ---
| { type: "openEditors"; files: FileAttachment[] }
Expand Down
15 changes: 15 additions & 0 deletions packages/platforms/vscode/src/__tests__/chat-view-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function createMockAgent(): {
getProviders: vi.fn().mockResolvedValue({ providers: [], default: {} }),
listAllProviders: vi.fn().mockResolvedValue({ all: [], default: {}, connected: [] }),
getAgents: vi.fn().mockResolvedValue([]),
getSkills: vi.fn().mockResolvedValue([]),
getChildSessions: vi.fn().mockResolvedValue([]),
replyPermission: vi.fn().mockResolvedValue(undefined),
getSessionDiff: vi.fn().mockResolvedValue([]),
Expand Down Expand Up @@ -499,12 +500,14 @@ describe("ChatViewProvider", () => {
model: { providerID: "anthropic", modelID: "claude-4" },
files: [{ filePath: "a.ts", fileName: "a.ts" }],
agent: "reviewer",
skill: "coding-guidelines",
});

expect(mockAgent.sendMessage).toHaveBeenCalledWith("sess-1", "Hello", {
model: { providerID: "anthropic", modelID: "claude-4" },
files: [{ filePath: "a.ts", fileName: "a.ts" }],
agent: "reviewer",
skill: "coding-guidelines",
});
});
});
Expand Down Expand Up @@ -909,6 +912,18 @@ describe("ChatViewProvider", () => {
});
});

describe("getSkills", () => {
it("should send skills message", async () => {
const skills = [{ name: "coding-guidelines" }];
mockAgent.getSkills.mockResolvedValue(skills as never);

const { postMessage, sendMessage } = setupProvider(mockAgent);
await sendMessage({ type: "getSkills" });

expect(postMessage).toHaveBeenCalledWith({ type: "skills", skills });
});
});

// ============================================================
// shareSession
// ============================================================
Expand Down
6 changes: 6 additions & 0 deletions packages/platforms/vscode/src/chat-view-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
files: message.files,
agent: message.agent,
primaryAgent: message.primaryAgent,
skill: message.skill,
});
break;
}
Expand Down Expand Up @@ -266,6 +267,11 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
this.postMessage({ type: "agents", agents });
break;
}
case "getSkills": {
const skills = await this.agent.getSkills();
this.postMessage({ type: "skills", skills });
break;
}
case "shareSession": {
const session = await this.agent.shareSession(message.sessionId);
this.activeSession = session;
Expand Down
12 changes: 10 additions & 2 deletions packages/platforms/vscode/webview/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AgentEvent, AgentInfo, ChatSession, TodoItem } from "@opencodegui/core";
import type { AgentEvent, AgentInfo, ChatSession, SkillInfo, TodoItem } from "@opencodegui/core";
import { useCallback, useEffect, useRef, useState } from "react";
import { EmptyState } from "./components/molecules/EmptyState";
import { FileChangesHeader } from "./components/molecules/FileChangesHeader";
Expand Down Expand Up @@ -47,6 +47,7 @@ export function App() {
const [todos, setTodos] = useState<TodoItem[]>([]);
const [childSessions, setChildSessions] = useState<ChatSession[]>([]);
const [agents, setAgents] = useState<AgentInfo[]>([]);
const [skills, setSkills] = useState<SkillInfo[]>([]);
const [selectedPrimaryAgent, setSelectedPrimaryAgent] = useState<string | null>(null);
const [difitAvailable, setDifitAvailable] = useState(false);
const [openCodePaths, setOpenCodePaths] = useState<{
Expand Down Expand Up @@ -208,6 +209,10 @@ export function App() {
});
break;
}
case "skills": {
setSkills(data.skills);
break;
}
case "difitAvailable": {
setDifitAvailable(data.available);
break;
Expand All @@ -218,6 +223,7 @@ export function App() {
postMessage({ type: "ready" });
postMessage({ type: "getOpenEditors" });
postMessage({ type: "getAgents" });
postMessage({ type: "getSkills" });
return () => window.removeEventListener("message", handler);
}, [
session.activeSession?.id,
Expand All @@ -236,7 +242,7 @@ export function App() {
// Cross-cutting action handlers (span multiple hooks)

const handleSend = useCallback(
(text: string, files: FileAttachment[], agent?: string, primaryAgent?: string) => {
(text: string, files: FileAttachment[], agent?: string, primaryAgent?: string, skill?: string) => {
if (!session.activeSession) return;
postMessage({
type: "sendMessage",
Expand All @@ -246,6 +252,7 @@ export function App() {
files: files.length > 0 ? files : undefined,
agent,
primaryAgent,
skill,
});
},
[session.activeSession, prov.selectedModel],
Expand Down Expand Up @@ -538,6 +545,7 @@ export function App() {
soundSettings={sound.soundSettings}
onSoundSettingChange={sound.handleSoundSettingChange}
agents={agents}
skills={skills}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const defaultProps = {
selectedAgent: null,
onSelectAgent: vi.fn(),
onClearAgent: vi.fn(),
skills: [] as any[],
selectedSkill: null,
onSelectSkill: vi.fn(),
onClearSkill: vi.fn(),
isShellMode: false,
onToggleShellMode: vi.fn(),
onDisableShellMode: vi.fn(),
Expand Down Expand Up @@ -105,13 +109,20 @@ describe("FileAttachmentBar", () => {
expect(container.querySelector(".pickerDropdown")).toBeInTheDocument();
});

// renders three sections: Files, Agents, Shell Mode
it("3 つのセクション(Files, Agents, Shell Mode)を表示すること", () => {
// renders four sections: Files, Agents, Skills, Shell Mode
it("4 つのセクション(Files, Agents, Skills, Shell Mode)を表示すること", () => {
render(
<FileAttachmentBar {...defaultProps} showFilePicker={true} pickerFiles={[file1]} agents={[agent1, agent2]} />,
<FileAttachmentBar
{...defaultProps}
showFilePicker={true}
pickerFiles={[file1]}
agents={[agent1, agent2]}
skills={[{ name: "coding-guidelines", description: "guides" }] as any}
/>,
);
expect(screen.getByText("Files")).toBeInTheDocument();
expect(screen.getByText("Sub-agents")).toBeInTheDocument();
expect(screen.getByText("Skills")).toBeInTheDocument();
expect(screen.getByText("Shell Mode")).toBeInTheDocument();
});
});
Expand Down Expand Up @@ -161,7 +172,7 @@ describe("FileAttachmentBar", () => {
/>,
);
const disabledSections = container.querySelectorAll(".sectionDisabled");
expect(disabledSections.length).toBe(2);
expect(disabledSections.length).toBe(3);
});

// toggle track has toggleOn class
Expand Down
Loading