- Tauri : commande change_agent_profile + ChangeAgentProfileRequestDto/Dto (camelCase, relaunchedSession omis si None), câblage state.rs par composition. - Front : gateway changeAgentProfile (adapters Tauri+mock), sélecteur de profil par agent, dialog de confirmation FR (« Changer le moteur abandonne l'historique de conversation… »), refresh sur event agentProfileChanged. Tests : app-tauri dto 5/5 ; vitest 314/314 (mapping, payload, dialog gating, libellé FR, refresh). 0 régression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
178 lines
5.8 KiB
TypeScript
178 lines
5.8 KiB
TypeScript
/**
|
|
* Tauri adapter for {@link AgentGateway} (L6).
|
|
*
|
|
* NOTE: The Tauri commands wired here (`list_agents`, `create_agent`, …) are
|
|
* defined in the backend `app-tauri` crate and will be registered in a
|
|
* subsequent lot. This adapter is complete on the frontend side; the mock
|
|
* gateway covers tests and offline dev today. The real mode will work
|
|
* transparently once the commands are registered.
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
import { Channel, invoke } from "@tauri-apps/api/core";
|
|
|
|
import type { Agent, TerminalSession } from "@/domain";
|
|
import type {
|
|
AgentGateway,
|
|
ConversationDetails,
|
|
CreateAgentInput,
|
|
LiveAgent,
|
|
OpenTerminalOptions,
|
|
ReattachResult,
|
|
TerminalHandle,
|
|
} from "@/ports";
|
|
import { makeTerminalHandle } from "./terminal";
|
|
|
|
/** Wire shape returned by the `launch_agent` command (mirrors `open_terminal`). */
|
|
interface LaunchAgentResponse {
|
|
sessionId: string;
|
|
cwd: string;
|
|
rows: number;
|
|
cols: number;
|
|
/** Conversation id minted by this launch (omitted when nothing was assigned). */
|
|
assignedConversationId?: string;
|
|
}
|
|
|
|
export class TauriAgentGateway implements AgentGateway {
|
|
listAgents(projectId: string): Promise<Agent[]> {
|
|
return invoke<Agent[]>("list_agents", { projectId });
|
|
}
|
|
|
|
listLiveAgents(projectId: string): Promise<LiveAgent[]> {
|
|
return invoke<LiveAgent[]>("list_live_agents", { projectId });
|
|
}
|
|
|
|
attachLiveAgent(
|
|
projectId: string,
|
|
agentId: string,
|
|
nodeId: string,
|
|
): Promise<LiveAgent> {
|
|
return invoke<LiveAgent[]>("attach_live_agent", {
|
|
request: { projectId, agentId, nodeId },
|
|
}).then((list) => {
|
|
const attached = list[0];
|
|
if (!attached) {
|
|
throw new Error(`running session for agent ${agentId} was not returned`);
|
|
}
|
|
return attached;
|
|
});
|
|
}
|
|
|
|
createAgent(projectId: string, input: CreateAgentInput): Promise<Agent> {
|
|
// The `create_agent` command takes a single `request` DTO; `projectId` must
|
|
// live *inside* it (camelCase), not at the top level.
|
|
return invoke<Agent>("create_agent", {
|
|
request: {
|
|
projectId,
|
|
name: input.name,
|
|
profileId: input.profileId,
|
|
initialContent: input.initialContent ?? null,
|
|
},
|
|
});
|
|
}
|
|
|
|
changeAgentProfile(
|
|
projectId: string,
|
|
agentId: string,
|
|
profileId: string,
|
|
rows: number,
|
|
cols: number,
|
|
): Promise<{ agent: Agent; relaunchedSession?: TerminalSession }> {
|
|
// `change_agent_profile` takes a single `request` DTO; the camelCase keys
|
|
// match the backend `ChangeAgentProfileRequestDto`. The response carries the
|
|
// mutated agent and, when a live session was hot-swapped, the relaunched one
|
|
// (omitted otherwise — `relaunchedSession` stays `undefined`).
|
|
return invoke<{ agent: Agent; relaunchedSession?: TerminalSession }>(
|
|
"change_agent_profile",
|
|
{ request: { projectId, agentId, profileId, rows, cols } },
|
|
);
|
|
}
|
|
|
|
readContext(projectId: string, agentId: string): Promise<string> {
|
|
return invoke<string>("read_agent_context", { projectId, agentId });
|
|
}
|
|
|
|
async updateContext(
|
|
projectId: string,
|
|
agentId: string,
|
|
content: string,
|
|
): Promise<void> {
|
|
// `update_agent_context` takes a single `request` DTO.
|
|
await invoke("update_agent_context", {
|
|
request: { projectId, agentId, content },
|
|
});
|
|
}
|
|
|
|
async deleteAgent(projectId: string, agentId: string): Promise<void> {
|
|
await invoke("delete_agent", { projectId, agentId });
|
|
}
|
|
|
|
async launchAgent(
|
|
projectId: string,
|
|
agentId: string,
|
|
options: OpenTerminalOptions,
|
|
onData: (bytes: Uint8Array) => void,
|
|
): Promise<TerminalHandle> {
|
|
// Per-session output channel. The backend serialises chunks as byte arrays.
|
|
const channel = new Channel<number[]>();
|
|
channel.onmessage = (chunk) => onData(Uint8Array.from(chunk));
|
|
|
|
const res = await invoke<LaunchAgentResponse>("launch_agent", {
|
|
request: {
|
|
projectId,
|
|
agentId,
|
|
rows: options.rows,
|
|
cols: options.cols,
|
|
// Resume id: the leaf's persisted conversation id, when any (T4b). The
|
|
// backend resumes it; absent ⇒ a fresh cell that may get a new id.
|
|
conversationId: options.conversationId ?? null,
|
|
// Hosting cell: lets the backend enforce one-live-session-per-agent.
|
|
nodeId: options.nodeId ?? null,
|
|
},
|
|
onOutput: channel,
|
|
});
|
|
|
|
const handle = makeTerminalHandle(res.sessionId, channel);
|
|
// Surface the id assigned by this launch so the caller persists it on the
|
|
// leaf (`setCellConversation`) and resumes next time.
|
|
return res.assignedConversationId
|
|
? { ...handle, assignedConversationId: res.assignedConversationId }
|
|
: handle;
|
|
}
|
|
|
|
async reattach(
|
|
sessionId: string,
|
|
onData: (bytes: Uint8Array) => void,
|
|
): Promise<ReattachResult> {
|
|
// Agent sessions reattach through the same session-based `reattach_terminal`
|
|
// command as plain terminals (the PTY is identified by its session id).
|
|
const channel = new Channel<number[]>();
|
|
channel.onmessage = (chunk) => onData(Uint8Array.from(chunk));
|
|
|
|
const res = await invoke<{ sessionId: string; scrollback: number[] }>(
|
|
"reattach_terminal",
|
|
{ sessionId, onOutput: channel },
|
|
);
|
|
|
|
return {
|
|
handle: makeTerminalHandle(res.sessionId, channel),
|
|
scrollback: Uint8Array.from(res.scrollback),
|
|
};
|
|
}
|
|
|
|
inspectConversation(
|
|
projectId: string,
|
|
agentId: string,
|
|
conversationId: string,
|
|
): Promise<ConversationDetails> {
|
|
// `inspect_conversation` takes a single `request` DTO; absent fields are
|
|
// omitted from the response (best-effort), so an empty object is valid.
|
|
return invoke<ConversationDetails>("inspect_conversation", {
|
|
request: { projectId, agentId, conversationId },
|
|
});
|
|
}
|
|
}
|