feat: add main features
Agents for developpement added + frontend add + backend added. Git viewer created + agent and template creator + layout and project creator
This commit is contained in:
211
frontend/src/features/agents/useAgents.ts
Normal file
211
frontend/src/features/agents/useAgents.ts
Normal file
@ -0,0 +1,211 @@
|
||||
/**
|
||||
* `useAgents` — view-model hook for the agents feature (L6).
|
||||
*
|
||||
* Owns the agents feature state for a given project and exposes the actions the
|
||||
* UI triggers. Consumes {@link AgentGateway} and {@link ProfileGateway}
|
||||
* exclusively; never touches `invoke()` or `@tauri-apps/api`, keeping the
|
||||
* component layer testable with mock gateways (ARCHITECTURE §1.3).
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import type { Agent, AgentProfile, GatewayError } from "@/domain";
|
||||
import type { OpenTerminalOptions, TerminalHandle } from "@/ports";
|
||||
import { useGateways } from "@/app/di";
|
||||
|
||||
/** What the agents UI needs from this hook. */
|
||||
export interface AgentsViewModel {
|
||||
/** All agents known to the project. */
|
||||
agents: Agent[];
|
||||
/** Id of the currently selected agent, or `null`. */
|
||||
selectedAgentId: string | null;
|
||||
/** The `.md` context of the selected agent (loaded on select). */
|
||||
context: string;
|
||||
/** Profiles available for assigning to a new agent. */
|
||||
profiles: AgentProfile[];
|
||||
/** Last error message, or `null`. */
|
||||
error: string | null;
|
||||
/** Whether a request is in flight. */
|
||||
busy: boolean;
|
||||
/**
|
||||
* Id of the agent whose terminal is currently running, or `null`.
|
||||
* Set when the user clicks Launch; cleared when the terminal is stopped.
|
||||
*/
|
||||
runningAgentId: string | null;
|
||||
/** Reloads the agent list. */
|
||||
refresh: () => Promise<void>;
|
||||
/** Creates a new agent and refreshes the list. */
|
||||
createAgent: (name: string, profileId: string, initialContent?: string) => Promise<void>;
|
||||
/** Selects an agent and loads its context. */
|
||||
selectAgent: (agentId: string) => Promise<void>;
|
||||
/** Saves the context for the currently selected agent. */
|
||||
saveContext: (content: string) => Promise<void>;
|
||||
/** Deletes an agent; deselects if it was selected. */
|
||||
deleteAgent: (agentId: string) => Promise<void>;
|
||||
/**
|
||||
* Returns an opener compatible with `TerminalView`'s `open` prop for the
|
||||
* given agent. Calling it sets `runningAgentId`. The component unmounting
|
||||
* will call `handle.close()` automatically via the TerminalView cleanup.
|
||||
*/
|
||||
launchAgent: (
|
||||
agentId: string,
|
||||
options: OpenTerminalOptions,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
) => Promise<TerminalHandle>;
|
||||
/** Clears `runningAgentId` (called by the Stop button to unmount the terminal). */
|
||||
stopAgent: () => void;
|
||||
}
|
||||
|
||||
function describe(e: unknown): string {
|
||||
if (e && typeof e === "object" && "message" in e) {
|
||||
return String((e as GatewayError).message);
|
||||
}
|
||||
return String(e);
|
||||
}
|
||||
|
||||
export function useAgents(projectId: string): AgentsViewModel {
|
||||
const { agent, profile } = useGateways();
|
||||
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
|
||||
const [context, setContext] = useState<string>("");
|
||||
const [profiles, setProfiles] = useState<AgentProfile[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [runningAgentId, setRunningAgentId] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [agentList, profileList] = await Promise.all([
|
||||
agent.listAgents(projectId),
|
||||
profile.listProfiles(),
|
||||
]);
|
||||
setAgents(agentList);
|
||||
setProfiles(profileList);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [agent, profile, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const createAgent = useCallback(
|
||||
async (name: string, profileId: string, initialContent?: string) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const created = await agent.createAgent(projectId, {
|
||||
name,
|
||||
profileId,
|
||||
initialContent,
|
||||
});
|
||||
setAgents((prev) => [...prev, created]);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[agent, projectId],
|
||||
);
|
||||
|
||||
const selectAgent = useCallback(
|
||||
async (agentId: string) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const ctx = await agent.readContext(projectId, agentId);
|
||||
setSelectedAgentId(agentId);
|
||||
setContext(ctx);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[agent, projectId],
|
||||
);
|
||||
|
||||
const saveContext = useCallback(
|
||||
async (content: string) => {
|
||||
if (!selectedAgentId) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await agent.updateContext(projectId, selectedAgentId, content);
|
||||
setContext(content);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[agent, projectId, selectedAgentId],
|
||||
);
|
||||
|
||||
const deleteAgent = useCallback(
|
||||
async (agentId: string) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await agent.deleteAgent(projectId, agentId);
|
||||
setAgents((prev) => prev.filter((a) => a.id !== agentId));
|
||||
setSelectedAgentId((current) => (current === agentId ? null : current));
|
||||
if (selectedAgentId === agentId) setContext("");
|
||||
// Also stop the terminal if the running agent was just deleted.
|
||||
setRunningAgentId((current) => (current === agentId ? null : current));
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[agent, projectId, selectedAgentId],
|
||||
);
|
||||
|
||||
const launchAgent = useCallback(
|
||||
async (
|
||||
agentId: string,
|
||||
options: OpenTerminalOptions,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<TerminalHandle> => {
|
||||
setError(null);
|
||||
try {
|
||||
const handle = await agent.launchAgent(projectId, agentId, options, onData);
|
||||
setRunningAgentId(agentId);
|
||||
return handle;
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
[agent, projectId],
|
||||
);
|
||||
|
||||
const stopAgent = useCallback(() => {
|
||||
setRunningAgentId(null);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
agents,
|
||||
selectedAgentId,
|
||||
context,
|
||||
profiles,
|
||||
error,
|
||||
busy,
|
||||
runningAgentId,
|
||||
refresh,
|
||||
createAgent,
|
||||
selectAgent,
|
||||
saveContext,
|
||||
deleteAgent,
|
||||
launchAgent,
|
||||
stopAgent,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user