Lot F1 du chantier server/client mode : nouvel adaptateur web branché derrière les ports d'invocation et de flux live, permettant au frontend de dialoguer avec le backend via HTTP + WebSocket en mode client/serveur. Le mode desktop (Tauri IPC) reste inchangé. - frontend/src/adapters/http : invoker HTTP, client live WebSocket, gateways request/response et stream, frames, garde unsupported (7 fichiers + 2 tests). - frontend/src/app : câblage DI (di.tsx) et son test, typage vite-env.d.ts. Validé : build vert, garde no-direct-invoke verte, 724 tests verts, desktop inchangé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
401 lines
16 KiB
TypeScript
401 lines
16 KiB
TypeScript
/**
|
|
* HTTP request/response gateways for the web transport — ticket #13, lot F1.
|
|
*
|
|
* Each class implements a UI port by forwarding the **exact** backend command
|
|
* name + camelCase argument envelope its Tauri sibling uses (the shared backend
|
|
* core, so identical contracts) through the generic {@link HttpInvoker}. No DTO
|
|
* shape is re-derived here; only the transport changes (Tauri `invoke` → HTTP
|
|
* `POST /api/invoke`). Normalizers that are transport-neutral (`workState`,
|
|
* `conversation`) are reused from the sibling adapters.
|
|
*
|
|
* Lives in `src/adapters/**`; touches no `@tauri-apps/api`.
|
|
*/
|
|
|
|
import type {
|
|
Agent,
|
|
AgentDrift,
|
|
AgentProfile,
|
|
EffectivePermissions,
|
|
EmbedderEngines,
|
|
EmbedderProfile,
|
|
FirstRunState,
|
|
GitBranches,
|
|
GitCommit,
|
|
GitFileStatus,
|
|
GraphCommit,
|
|
LayoutKind,
|
|
LayoutList,
|
|
LayoutOperation,
|
|
LayoutTree,
|
|
LocalModelServerConfig,
|
|
Memory,
|
|
MemoryIndexEntry,
|
|
MemoryLink,
|
|
MemoryType,
|
|
ModelServerCommandPreview,
|
|
PermissionSet,
|
|
Project,
|
|
ProjectPermissions,
|
|
ProjectWorkState,
|
|
ProfileAvailability,
|
|
Skill,
|
|
SkillScope,
|
|
Template,
|
|
TurnPage,
|
|
} from "@/domain";
|
|
import type {
|
|
CloneOpenCodeProfileFromSeedInput,
|
|
ConversationGateway,
|
|
ConversationPageRequest,
|
|
CreateMemoryInput,
|
|
CreateSkillInput,
|
|
CreateTemplateInput,
|
|
EmbedderGateway,
|
|
GitGateway,
|
|
InputGateway,
|
|
LayoutGateway,
|
|
MemoryGateway,
|
|
ModelServerGateway,
|
|
PermissionGateway,
|
|
ProfileGateway,
|
|
ProjectGateway,
|
|
SkillGateway,
|
|
TemplateGateway,
|
|
WorkStateGateway,
|
|
} from "@/ports";
|
|
import { normalizeProjectWorkState } from "../workStateNormalization";
|
|
import { normalizeTurnPage } from "../conversationNormalization";
|
|
import type { HttpInvoker } from "./httpInvoker";
|
|
|
|
export class HttpProjectGateway implements ProjectGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
listProjects(): Promise<Project[]> {
|
|
return this.http.invoke<Project[]>("list_projects");
|
|
}
|
|
createProject(name: string, root: string): Promise<Project> {
|
|
return this.http.invoke<Project>("create_project", { request: { name, root } });
|
|
}
|
|
openProject(projectId: string): Promise<Project> {
|
|
return this.http.invoke<Project>("open_project", { projectId });
|
|
}
|
|
async closeProject(projectId: string): Promise<void> {
|
|
await this.http.invoke("close_project", { projectId });
|
|
}
|
|
readProjectContext(projectId: string): Promise<string> {
|
|
return this.http.invoke<string>("read_project_context", { projectId });
|
|
}
|
|
async updateProjectContext(projectId: string, content: string): Promise<void> {
|
|
await this.http.invoke("update_project_context", { request: { projectId, content } });
|
|
}
|
|
}
|
|
|
|
export class HttpLayoutGateway implements LayoutGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
loadLayout(projectId: string, layoutId?: string): Promise<LayoutTree> {
|
|
return this.http.invoke<LayoutTree>("load_layout", { projectId, layoutId });
|
|
}
|
|
mutateLayout(
|
|
projectId: string,
|
|
operation: LayoutOperation,
|
|
layoutId?: string,
|
|
): Promise<LayoutTree> {
|
|
return this.http.invoke<LayoutTree>("mutate_layout", { projectId, layoutId, operation });
|
|
}
|
|
listLayouts(projectId: string): Promise<LayoutList> {
|
|
return this.http.invoke<LayoutList>("list_layouts", { projectId });
|
|
}
|
|
createLayout(projectId: string, name: string, kind?: LayoutKind): Promise<{ layoutId: string }> {
|
|
return this.http.invoke<{ layoutId: string }>("create_layout", { request: { projectId, name, kind } });
|
|
}
|
|
renameLayout(projectId: string, layoutId: string, name: string): Promise<void> {
|
|
return this.http.invoke<void>("rename_layout", { request: { projectId, layoutId, name } });
|
|
}
|
|
deleteLayout(projectId: string, layoutId: string): Promise<{ activeId: string }> {
|
|
return this.http.invoke<{ activeId: string }>("delete_layout", { request: { projectId, layoutId } });
|
|
}
|
|
setActiveLayout(projectId: string, layoutId: string): Promise<{ activeId: string }> {
|
|
return this.http.invoke<{ activeId: string }>("set_active_layout", { request: { projectId, layoutId } });
|
|
}
|
|
}
|
|
|
|
export class HttpGitGateway implements GitGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
status(projectId: string): Promise<GitFileStatus[]> {
|
|
return this.http.invoke<GitFileStatus[]>("git_status", { projectId });
|
|
}
|
|
async stage(projectId: string, path: string): Promise<void> {
|
|
await this.http.invoke("git_stage", { request: { projectId, path } });
|
|
}
|
|
async unstage(projectId: string, path: string): Promise<void> {
|
|
await this.http.invoke("git_unstage", { request: { projectId, path } });
|
|
}
|
|
commit(projectId: string, message: string): Promise<GitCommit> {
|
|
return this.http.invoke<GitCommit>("git_commit", { request: { projectId, message } });
|
|
}
|
|
branches(projectId: string): Promise<GitBranches> {
|
|
return this.http.invoke<GitBranches>("git_branches", { projectId });
|
|
}
|
|
async checkout(projectId: string, branch: string): Promise<void> {
|
|
await this.http.invoke("git_checkout", { request: { projectId, branch } });
|
|
}
|
|
log(projectId: string, limit: number): Promise<GitCommit[]> {
|
|
return this.http.invoke<GitCommit[]>("git_log", { projectId, limit });
|
|
}
|
|
async init(projectId: string): Promise<void> {
|
|
await this.http.invoke("git_init", { projectId });
|
|
}
|
|
graph(projectId: string, limit: number): Promise<GraphCommit[]> {
|
|
return this.http.invoke<GraphCommit[]>("git_graph", { projectId, limit });
|
|
}
|
|
}
|
|
|
|
export class HttpProfileGateway implements ProfileGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
firstRunState(): Promise<FirstRunState> {
|
|
return this.http.invoke<FirstRunState>("first_run_state");
|
|
}
|
|
referenceProfiles(): Promise<AgentProfile[]> {
|
|
return this.http.invoke<AgentProfile[]>("reference_profiles");
|
|
}
|
|
detectProfiles(candidates: AgentProfile[]): Promise<ProfileAvailability[]> {
|
|
return this.http.invoke<ProfileAvailability[]>("detect_profiles", { request: { candidates } });
|
|
}
|
|
listProfiles(): Promise<AgentProfile[]> {
|
|
return this.http.invoke<AgentProfile[]>("list_profiles");
|
|
}
|
|
saveProfile(profile: AgentProfile): Promise<AgentProfile> {
|
|
return this.http.invoke<AgentProfile>("save_profile", { request: { profile } });
|
|
}
|
|
async deleteProfile(profileId: string): Promise<void> {
|
|
await this.http.invoke("delete_profile", { profileId });
|
|
}
|
|
configureProfiles(profiles: AgentProfile[]): Promise<AgentProfile[]> {
|
|
return this.http.invoke<AgentProfile[]>("configure_profiles", { request: { profiles } });
|
|
}
|
|
cloneOpenCodeProfileFromSeed(
|
|
input: CloneOpenCodeProfileFromSeedInput = {},
|
|
): Promise<AgentProfile> {
|
|
return this.http.invoke<AgentProfile>("clone_opencode_profile_from_seed", {
|
|
request: { name: input.name, opencode: input.opencode },
|
|
});
|
|
}
|
|
}
|
|
|
|
export class HttpModelServerGateway implements ModelServerGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
listModelServers(): Promise<LocalModelServerConfig[]> {
|
|
return this.http.invoke<LocalModelServerConfig[]>("list_model_servers");
|
|
}
|
|
saveModelServer(config: LocalModelServerConfig): Promise<LocalModelServerConfig> {
|
|
return this.http.invoke<LocalModelServerConfig>("save_model_server", { request: { config } });
|
|
}
|
|
async deleteModelServer(serverId: string): Promise<void> {
|
|
await this.http.invoke("delete_model_server", { serverId });
|
|
}
|
|
previewModelServerCommand(config: LocalModelServerConfig): Promise<ModelServerCommandPreview> {
|
|
return this.http.invoke<ModelServerCommandPreview>("preview_model_server_command", { config });
|
|
}
|
|
}
|
|
|
|
export class HttpTemplateGateway implements TemplateGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
listTemplates(): Promise<Template[]> {
|
|
return this.http.invoke<Template[]>("list_templates");
|
|
}
|
|
createTemplate(input: CreateTemplateInput): Promise<Template> {
|
|
return this.http.invoke<Template>("create_template", {
|
|
request: { name: input.name, content: input.content, defaultProfileId: input.defaultProfileId },
|
|
});
|
|
}
|
|
updateTemplate(templateId: string, content: string): Promise<Template> {
|
|
return this.http.invoke<Template>("update_template", { request: { templateId, content } });
|
|
}
|
|
async deleteTemplate(templateId: string): Promise<void> {
|
|
await this.http.invoke("delete_template", { templateId });
|
|
}
|
|
createAgentFromTemplate(
|
|
projectId: string,
|
|
templateId: string,
|
|
opts?: { name?: string; synchronized?: boolean },
|
|
): Promise<Agent> {
|
|
return this.http.invoke<Agent>("create_agent_from_template", {
|
|
request: {
|
|
projectId,
|
|
templateId,
|
|
name: opts?.name ?? null,
|
|
synchronized: opts?.synchronized ?? true,
|
|
},
|
|
});
|
|
}
|
|
detectDrift(projectId: string): Promise<AgentDrift[]> {
|
|
return this.http.invoke<AgentDrift[]>("detect_agent_drift", { projectId });
|
|
}
|
|
syncAgent(projectId: string, agentId: string): Promise<{ synced: boolean; version: number | null }> {
|
|
return this.http.invoke<{ synced: boolean; version: number | null }>("sync_agent_with_template", {
|
|
request: { projectId, agentId },
|
|
});
|
|
}
|
|
}
|
|
|
|
export class HttpSkillGateway implements SkillGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
listSkills(projectId: string, scope: SkillScope): Promise<Skill[]> {
|
|
return this.http.invoke<Skill[]>("list_skills", { projectId, scope });
|
|
}
|
|
createSkill(input: CreateSkillInput): Promise<Skill> {
|
|
return this.http.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 this.http.invoke<Skill>("update_skill", { request: { projectId, scope, skillId, content } });
|
|
}
|
|
async deleteSkill(projectId: string, scope: SkillScope, skillId: string): Promise<void> {
|
|
await this.http.invoke("delete_skill", { projectId, scope, skillId });
|
|
}
|
|
async assignSkill(projectId: string, agentId: string, skillId: string, scope: SkillScope): Promise<void> {
|
|
await this.http.invoke("assign_skill_to_agent", { request: { projectId, agentId, skillId, scope } });
|
|
}
|
|
async unassignSkill(projectId: string, agentId: string, skillId: string): Promise<void> {
|
|
await this.http.invoke("unassign_skill_from_agent", { request: { projectId, agentId, skillId } });
|
|
}
|
|
}
|
|
|
|
export class HttpMemoryGateway implements MemoryGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
listMemories(projectId: string): Promise<Memory[]> {
|
|
return this.http.invoke<Memory[]>("list_memories", { projectId });
|
|
}
|
|
getMemory(projectId: string, slug: string): Promise<Memory> {
|
|
return this.http.invoke<Memory>("get_memory", { projectId, slug });
|
|
}
|
|
createMemory(input: CreateMemoryInput): Promise<Memory> {
|
|
return this.http.invoke<Memory>("create_memory", {
|
|
request: {
|
|
projectId: input.projectId,
|
|
name: input.name,
|
|
description: input.description,
|
|
type: input.type,
|
|
content: input.content,
|
|
},
|
|
});
|
|
}
|
|
updateMemory(
|
|
projectId: string,
|
|
slug: string,
|
|
description: string,
|
|
type: MemoryType,
|
|
content: string,
|
|
): Promise<Memory> {
|
|
return this.http.invoke<Memory>("update_memory", { request: { projectId, slug, description, type, content } });
|
|
}
|
|
async deleteMemory(projectId: string, slug: string): Promise<void> {
|
|
await this.http.invoke("delete_memory", { projectId, slug });
|
|
}
|
|
readIndex(projectId: string): Promise<MemoryIndexEntry[]> {
|
|
return this.http.invoke<MemoryIndexEntry[]>("read_memory_index", { projectId });
|
|
}
|
|
resolveLinks(projectId: string, slug: string): Promise<MemoryLink[]> {
|
|
return this.http.invoke<MemoryLink[]>("resolve_memory_links", { projectId, slug });
|
|
}
|
|
recall(projectId: string, text: string, tokenBudget: number): Promise<MemoryIndexEntry[]> {
|
|
return this.http.invoke<MemoryIndexEntry[]>("recall_memory", { request: { projectId, text, tokenBudget } });
|
|
}
|
|
}
|
|
|
|
export class HttpEmbedderGateway implements EmbedderGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
listEmbedderProfiles(): Promise<EmbedderProfile[]> {
|
|
return this.http.invoke<EmbedderProfile[]>("list_embedder_profiles");
|
|
}
|
|
saveEmbedderProfile(profile: EmbedderProfile): Promise<EmbedderProfile> {
|
|
return this.http.invoke<EmbedderProfile>("save_embedder_profile", { request: { profile } });
|
|
}
|
|
async deleteEmbedderProfile(embedderId: string): Promise<void> {
|
|
await this.http.invoke("delete_embedder_profile", { embedderId });
|
|
}
|
|
describeEmbedderEngines(): Promise<EmbedderEngines> {
|
|
return this.http.invoke<EmbedderEngines>("describe_embedder_engines");
|
|
}
|
|
}
|
|
|
|
export class HttpPermissionGateway implements PermissionGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
getProjectPermissions(projectId: string): Promise<ProjectPermissions> {
|
|
return this.http.invoke<ProjectPermissions>("get_project_permissions", { projectId });
|
|
}
|
|
updateProjectPermissions(
|
|
projectId: string,
|
|
permissions: PermissionSet | null,
|
|
): Promise<ProjectPermissions> {
|
|
return this.http.invoke<ProjectPermissions>("update_project_permissions", { request: { projectId, permissions } });
|
|
}
|
|
updateAgentPermissions(
|
|
projectId: string,
|
|
agentId: string,
|
|
permissions: PermissionSet | null,
|
|
): Promise<ProjectPermissions> {
|
|
return this.http.invoke<ProjectPermissions>("update_agent_permissions", {
|
|
request: { projectId, agentId, permissions },
|
|
});
|
|
}
|
|
resolveAgentPermissions(projectId: string, agentId: string): Promise<EffectivePermissions | null> {
|
|
return this.http.invoke<EffectivePermissions | null>("resolve_agent_permissions", {
|
|
request: { projectId, agentId },
|
|
});
|
|
}
|
|
}
|
|
|
|
export class HttpWorkStateGateway implements WorkStateGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
async getProjectWorkState(projectId: string): Promise<ProjectWorkState> {
|
|
const state = await this.http.invoke<unknown>("get_project_work_state", { projectId });
|
|
return normalizeProjectWorkState(state);
|
|
}
|
|
async cancelBackgroundTask(taskId: string): Promise<void> {
|
|
await this.http.invoke<unknown>("cancel_background_task", { taskId });
|
|
}
|
|
async retryBackgroundTask(taskId: string): Promise<void> {
|
|
await this.http.invoke<unknown>("retry_background_task", { taskId });
|
|
}
|
|
}
|
|
|
|
export class HttpConversationGateway implements ConversationGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
async readPage(
|
|
projectId: string,
|
|
conversationId: string,
|
|
request?: ConversationPageRequest,
|
|
): Promise<TurnPage> {
|
|
const page = await this.http.invoke<unknown>("read_conversation_page", {
|
|
request: {
|
|
projectId,
|
|
conversationId,
|
|
anchor: request?.anchor,
|
|
direction: request?.direction ?? "backward",
|
|
limit: request?.limit,
|
|
},
|
|
});
|
|
return normalizeTurnPage(page);
|
|
}
|
|
}
|
|
|
|
export class HttpInputGateway implements InputGateway {
|
|
constructor(private readonly http: HttpInvoker) {}
|
|
async interrupt(projectId: string, agentId: string): Promise<void> {
|
|
await this.http.invoke("interrupt_agent", { request: { projectId, agentId } });
|
|
}
|
|
async delegationDelivered(projectId: string, agentId: string, ticket: string): Promise<void> {
|
|
await this.http.invoke("delegation_delivered", { request: { projectId, agentId, ticket } });
|
|
}
|
|
async setFrontAttached(agentId: string, attached: boolean): Promise<void> {
|
|
await this.http.invoke("set_front_attached", { request: { agentId, attached } });
|
|
}
|
|
async cancelResume(agentId: string): Promise<boolean> {
|
|
return this.http.invoke<boolean>("cancel_resume", { agentId });
|
|
}
|
|
async setResumeAt(agentId: string, resetsAtMs: number): Promise<void> {
|
|
await this.http.invoke("set_resume_at", { agentId, resetsAtMs });
|
|
}
|
|
}
|