Files
IdeaSDK/frontend/src/adapters/http/requestResponseGateways.ts
Blomios e0cdb4aa56 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>
2026-07-15 12:50:38 +02:00

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 });
}
}