fix: fix some displays and features
This commit is contained in:
@ -19,6 +19,7 @@ import { TauriTerminalGateway } from "./terminal";
|
||||
import { TauriLayoutGateway } from "./layout";
|
||||
import { TauriProfileGateway } from "./profile";
|
||||
import { TauriTemplateGateway } from "./template";
|
||||
import { TauriSkillGateway } from "./skill";
|
||||
import { TauriGitGateway } from "./git";
|
||||
|
||||
function notImplemented(what: string): never {
|
||||
@ -47,6 +48,7 @@ export function createTauriGateways(): Gateways {
|
||||
remote: new TauriRemoteGateway(),
|
||||
profile: new TauriProfileGateway(),
|
||||
template: new TauriTemplateGateway(),
|
||||
skill: new TauriSkillGateway(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -58,5 +60,6 @@ export {
|
||||
TauriLayoutGateway,
|
||||
TauriProfileGateway,
|
||||
TauriTemplateGateway,
|
||||
TauriSkillGateway,
|
||||
TauriGitGateway,
|
||||
};
|
||||
|
||||
@ -23,12 +23,15 @@ import type {
|
||||
LayoutTree,
|
||||
Project,
|
||||
ProfileAvailability,
|
||||
Skill,
|
||||
SkillScope,
|
||||
Template,
|
||||
Unsubscribe,
|
||||
} from "@/domain";
|
||||
import type {
|
||||
AgentGateway,
|
||||
CreateAgentInput,
|
||||
CreateSkillInput,
|
||||
CreateTemplateInput,
|
||||
Gateways,
|
||||
GitGateway,
|
||||
@ -38,6 +41,7 @@ import type {
|
||||
ProjectGateway,
|
||||
ReattachResult,
|
||||
RemoteGateway,
|
||||
SkillGateway,
|
||||
SystemGateway,
|
||||
TemplateGateway,
|
||||
TerminalGateway,
|
||||
@ -191,6 +195,7 @@ export class MockAgentGateway implements AgentGateway {
|
||||
profileId: input.profileId,
|
||||
origin: { type: "scratch" },
|
||||
synchronized: false,
|
||||
skills: [],
|
||||
};
|
||||
list.push(agent);
|
||||
this.contexts.set(
|
||||
@ -282,6 +287,18 @@ export class MockAgentGateway implements AgentGateway {
|
||||
return this.getAgents(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an agent's assigned skills in-place.
|
||||
* Used by `MockSkillGateway.assignSkill` / `unassignSkill` so both gateways
|
||||
* share the same in-memory store.
|
||||
*/
|
||||
_setSkills(projectId: string, agentId: string, skills: Agent["skills"]): void {
|
||||
const list = this.getAgents(projectId);
|
||||
const idx = list.findIndex((a) => a.id === agentId);
|
||||
if (idx === -1) return;
|
||||
list[idx] = { ...list[idx], skills };
|
||||
}
|
||||
|
||||
async launchAgent(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
@ -931,6 +948,7 @@ export class MockTemplateGateway implements TemplateGateway {
|
||||
syncedTemplateVersion: template.version,
|
||||
},
|
||||
synchronized,
|
||||
skills: [],
|
||||
};
|
||||
|
||||
this.agentGateway._insertAgent(projectId, agent, template.contentMd);
|
||||
@ -990,6 +1008,130 @@ export class MockTemplateGateway implements TemplateGateway {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stateful in-memory skill gateway (L12).
|
||||
*
|
||||
* Skills are keyed by `${scope}` (project scope is further partitioned by
|
||||
* `projectId`, mirroring the disjoint on-disk roots). Assignment mutates the
|
||||
* agent record held by the injected {@link MockAgentGateway}, so both gateways
|
||||
* share one store — exactly like {@link MockTemplateGateway}.
|
||||
*/
|
||||
export class MockSkillGateway implements SkillGateway {
|
||||
// Global skills are project-independent; project skills live under their id.
|
||||
private global: Skill[] = [];
|
||||
private byProject = new Map<string, Skill[]>();
|
||||
private seq = 0;
|
||||
|
||||
constructor(private readonly agentGateway: MockAgentGateway) {}
|
||||
|
||||
private bucket(projectId: string, scope: SkillScope): Skill[] {
|
||||
if (scope === "global") return this.global;
|
||||
let list = this.byProject.get(projectId);
|
||||
if (!list) {
|
||||
list = [];
|
||||
this.byProject.set(projectId, list);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
async listSkills(projectId: string, scope: SkillScope): Promise<Skill[]> {
|
||||
return structuredClone(this.bucket(projectId, scope));
|
||||
}
|
||||
|
||||
async createSkill(input: CreateSkillInput): Promise<Skill> {
|
||||
this.seq += 1;
|
||||
const skill: Skill = {
|
||||
id: `mock-skill-${this.seq}`,
|
||||
name: input.name,
|
||||
contentMd: input.content,
|
||||
scope: input.scope,
|
||||
};
|
||||
this.bucket(input.projectId, input.scope).push(skill);
|
||||
return structuredClone(skill);
|
||||
}
|
||||
|
||||
async updateSkill(
|
||||
projectId: string,
|
||||
scope: SkillScope,
|
||||
skillId: string,
|
||||
content: string,
|
||||
): Promise<Skill> {
|
||||
const list = this.bucket(projectId, scope);
|
||||
const idx = list.findIndex((s) => s.id === skillId);
|
||||
if (idx === -1) {
|
||||
const err: GatewayError = {
|
||||
code: "NOT_FOUND",
|
||||
message: `skill ${skillId} not found`,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
list[idx] = { ...list[idx], contentMd: content };
|
||||
return structuredClone(list[idx]);
|
||||
}
|
||||
|
||||
async deleteSkill(
|
||||
projectId: string,
|
||||
scope: SkillScope,
|
||||
skillId: string,
|
||||
): Promise<void> {
|
||||
const list = this.bucket(projectId, scope);
|
||||
const idx = list.findIndex((s) => s.id === skillId);
|
||||
if (idx === -1) {
|
||||
const err: GatewayError = {
|
||||
code: "NOT_FOUND",
|
||||
message: `skill ${skillId} not found`,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
list.splice(idx, 1);
|
||||
}
|
||||
|
||||
async assignSkill(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
skillId: string,
|
||||
scope: SkillScope,
|
||||
): Promise<void> {
|
||||
const agent = this.agentGateway
|
||||
._rawAgents(projectId)
|
||||
.find((a) => a.id === agentId);
|
||||
if (!agent) {
|
||||
const err: GatewayError = {
|
||||
code: "NOT_FOUND",
|
||||
message: `agent ${agentId} not found in project ${projectId}`,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
if (agent.skills.some((s) => s.skillId === skillId)) return; // idempotent
|
||||
this.agentGateway._setSkills(projectId, agentId, [
|
||||
...agent.skills,
|
||||
{ skillId, scope },
|
||||
]);
|
||||
}
|
||||
|
||||
async unassignSkill(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
skillId: string,
|
||||
): Promise<void> {
|
||||
const agent = this.agentGateway
|
||||
._rawAgents(projectId)
|
||||
.find((a) => a.id === agentId);
|
||||
if (!agent) {
|
||||
const err: GatewayError = {
|
||||
code: "NOT_FOUND",
|
||||
message: `agent ${agentId} not found in project ${projectId}`,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
this.agentGateway._setSkills(
|
||||
projectId,
|
||||
agentId,
|
||||
agent.skills.filter((s) => s.skillId !== skillId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the full set of mock gateways. */
|
||||
export function createMockGateways(): Gateways {
|
||||
const agentGateway = new MockAgentGateway();
|
||||
@ -1003,6 +1145,7 @@ export function createMockGateways(): Gateways {
|
||||
remote: new MockRemoteGateway(),
|
||||
profile: new MockProfileGateway(),
|
||||
template: new MockTemplateGateway(agentGateway),
|
||||
skill: new MockSkillGateway(agentGateway),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import { createMockGateways, MockSystemGateway } from "./index";
|
||||
const gateways: Gateways = createMockGateways();
|
||||
|
||||
describe("createMockGateways", () => {
|
||||
it("exposes all nine gateways", () => {
|
||||
it("exposes all ten gateways", () => {
|
||||
expect(Object.keys(gateways).sort()).toEqual([
|
||||
"agent",
|
||||
"git",
|
||||
@ -20,6 +20,7 @@ describe("createMockGateways", () => {
|
||||
"profile",
|
||||
"project",
|
||||
"remote",
|
||||
"skill",
|
||||
"system",
|
||||
"template",
|
||||
"terminal",
|
||||
|
||||
70
frontend/src/adapters/skill.ts
Normal file
70
frontend/src/adapters/skill.ts
Normal file
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Tauri adapter for {@link SkillGateway} (L12).
|
||||
*
|
||||
* Commands use snake_case (Tauri convention); payload keys are camelCase
|
||||
* (matching the backend DTO `#[serde(rename_all = "camelCase")]`), consistent
|
||||
* with the other adapters in this directory. The `scope` value (`"global"` /
|
||||
* `"project"`) maps directly onto the backend `SkillScope` serde enum.
|
||||
*/
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import type { Skill, SkillScope } from "@/domain";
|
||||
import type { CreateSkillInput, SkillGateway } from "@/ports";
|
||||
|
||||
export class TauriSkillGateway implements SkillGateway {
|
||||
listSkills(projectId: string, scope: SkillScope): Promise<Skill[]> {
|
||||
return invoke<Skill[]>("list_skills", { projectId, scope });
|
||||
}
|
||||
|
||||
createSkill(input: CreateSkillInput): Promise<Skill> {
|
||||
return invoke<Skill>("create_skill", {
|
||||
request: {
|
||||
projectId: input.projectId,
|
||||
name: input.name,
|
||||
content: input.content,
|
||||
scope: input.scope,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateSkill(
|
||||
projectId: string,
|
||||
scope: SkillScope,
|
||||
skillId: string,
|
||||
content: string,
|
||||
): Promise<Skill> {
|
||||
return invoke<Skill>("update_skill", {
|
||||
request: { projectId, scope, skillId, content },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSkill(
|
||||
projectId: string,
|
||||
scope: SkillScope,
|
||||
skillId: string,
|
||||
): Promise<void> {
|
||||
await invoke("delete_skill", { projectId, scope, skillId });
|
||||
}
|
||||
|
||||
async assignSkill(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
skillId: string,
|
||||
scope: SkillScope,
|
||||
): Promise<void> {
|
||||
await invoke("assign_skill_to_agent", {
|
||||
request: { projectId, agentId, skillId, scope },
|
||||
});
|
||||
}
|
||||
|
||||
async unassignSkill(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
skillId: string,
|
||||
): Promise<void> {
|
||||
await invoke("unassign_skill_from_agent", {
|
||||
request: { projectId, agentId, skillId },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user