feat(frontend): adapter web HTTP+WebSocket derrière les ports (#13)
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>
This commit is contained in:
400
frontend/src/adapters/http/requestResponseGateways.ts
Normal file
400
frontend/src/adapters/http/requestResponseGateways.ts
Normal file
@ -0,0 +1,400 @@
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user