fix(web): passe le nodeId réel à la custom CLI au lieu d'un id fabriqué — #225
This commit is contained in:
@ -44,7 +44,7 @@ export interface CustomAgentChatViewProps {
|
|||||||
profile: AgentProfile;
|
profile: AgentProfile;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
layoutId?: string;
|
layoutId?: string;
|
||||||
nodeId: string;
|
nodeId?: string;
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
conversationId: string | null;
|
conversationId: string | null;
|
||||||
composerHeightPx?: number;
|
composerHeightPx?: number;
|
||||||
@ -125,6 +125,11 @@ function describe(e: unknown): string {
|
|||||||
return String(e);
|
return String(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function describeOpenFailure(e: unknown, mobile: boolean): string {
|
||||||
|
if (!mobile) return describe(e);
|
||||||
|
return "Le CLI custom n'a pas pu démarrer. Vérifiez la configuration du profil ou utilisez la TUI native.";
|
||||||
|
}
|
||||||
|
|
||||||
function instrumentationError(e: unknown): Record<string, unknown> {
|
function instrumentationError(e: unknown): Record<string, unknown> {
|
||||||
if (e && typeof e === "object") {
|
if (e && typeof e === "object") {
|
||||||
const err = e as GatewayError & { name?: string };
|
const err = e as GatewayError & { name?: string };
|
||||||
@ -803,6 +808,7 @@ export function CustomAgentChatView({
|
|||||||
onSessionIdRef.current = onSessionId;
|
onSessionIdRef.current = onSessionId;
|
||||||
const onConversationIdRef = useRef(onConversationId);
|
const onConversationIdRef = useRef(onConversationId);
|
||||||
onConversationIdRef.current = onConversationId;
|
onConversationIdRef.current = onConversationId;
|
||||||
|
const domId = nodeId ?? `agent-${agentId}`;
|
||||||
|
|
||||||
const clampComposerHeight = useCallback(
|
const clampComposerHeight = useCallback(
|
||||||
(heightPx: number): number => {
|
(heightPx: number): number => {
|
||||||
@ -891,7 +897,7 @@ export function CustomAgentChatView({
|
|||||||
const slashPrefix = slashCommandPrefix(draft);
|
const slashPrefix = slashCommandPrefix(draft);
|
||||||
const draftScope = useMemo<ChatDraftScope | null>(() => {
|
const draftScope = useMemo<ChatDraftScope | null>(() => {
|
||||||
if (explicitDraftScope) return explicitDraftScope;
|
if (explicitDraftScope) return explicitDraftScope;
|
||||||
return layoutId ? { projectId, layoutId, nodeId } : null;
|
return layoutId && nodeId ? { projectId, layoutId, nodeId } : null;
|
||||||
}, [explicitDraftScope, layoutId, nodeId, projectId]);
|
}, [explicitDraftScope, layoutId, nodeId, projectId]);
|
||||||
const currentConversationCacheKey = useMemo(
|
const currentConversationCacheKey = useMemo(
|
||||||
() => conversationCacheKey(projectId, agentId, conversationId),
|
() => conversationCacheKey(projectId, agentId, conversationId),
|
||||||
@ -1173,7 +1179,7 @@ export function CustomAgentChatView({
|
|||||||
options.conversationId === undefined
|
options.conversationId === undefined
|
||||||
? conversationId ?? undefined
|
? conversationId ?? undefined
|
||||||
: options.conversationId ?? undefined,
|
: options.conversationId ?? undefined,
|
||||||
nodeId,
|
...(nodeId ? { nodeId } : {}),
|
||||||
};
|
};
|
||||||
console.debug("[ticket149] recoverStructuredSession:launchAgentChat:start", {
|
console.debug("[ticket149] recoverStructuredSession:launchAgentChat:start", {
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@ -1365,7 +1371,7 @@ export function CustomAgentChatView({
|
|||||||
sessionId: externalSessionId,
|
sessionId: externalSessionId,
|
||||||
error: instrumentationError(e),
|
error: instrumentationError(e),
|
||||||
});
|
});
|
||||||
if (!cancelled) setError(describe(e));
|
if (!cancelled) setError(describeOpenFailure(e, mobile));
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) setOpening(false);
|
if (!cancelled) setOpening(false);
|
||||||
}
|
}
|
||||||
@ -1384,6 +1390,7 @@ export function CustomAgentChatView({
|
|||||||
applyChatViewState,
|
applyChatViewState,
|
||||||
currentConversationCacheKey,
|
currentConversationCacheKey,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
mobile,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const canSend = useMemo(
|
const canSend = useMemo(
|
||||||
@ -1822,7 +1829,7 @@ export function CustomAgentChatView({
|
|||||||
<div
|
<div
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby={`profile-command-title-${nodeId}`}
|
aria-labelledby={`profile-command-title-${domId}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border border-border bg-surface p-3 shadow-lg",
|
"border border-border bg-surface p-3 shadow-lg",
|
||||||
mobile
|
mobile
|
||||||
@ -1833,7 +1840,7 @@ export function CustomAgentChatView({
|
|||||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h2
|
<h2
|
||||||
id={`profile-command-title-${nodeId}`}
|
id={`profile-command-title-${domId}`}
|
||||||
className="text-sm font-semibold text-content"
|
className="text-sm font-semibold text-content"
|
||||||
>
|
>
|
||||||
Changer de profil
|
Changer de profil
|
||||||
@ -1979,7 +1986,7 @@ export function CustomAgentChatView({
|
|||||||
)}
|
)}
|
||||||
{slashMenuOpen && slashCommands.length > 0 && (
|
{slashMenuOpen && slashCommands.length > 0 && (
|
||||||
<div
|
<div
|
||||||
id={`slash-command-menu-${nodeId}`}
|
id={`slash-command-menu-${domId}`}
|
||||||
role="listbox"
|
role="listbox"
|
||||||
aria-label="suggestions commandes slash"
|
aria-label="suggestions commandes slash"
|
||||||
className="max-h-48 overflow-y-auto rounded-md border border-border bg-surface py-1 shadow-lg"
|
className="max-h-48 overflow-y-auto rounded-md border border-border bg-surface py-1 shadow-lg"
|
||||||
@ -1991,7 +1998,7 @@ export function CustomAgentChatView({
|
|||||||
<button
|
<button
|
||||||
key={command.name}
|
key={command.name}
|
||||||
type="button"
|
type="button"
|
||||||
id={`slash-command-${nodeId}-${index}`}
|
id={`slash-command-${domId}-${index}`}
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={selected}
|
aria-selected={selected}
|
||||||
aria-disabled={!available}
|
aria-disabled={!available}
|
||||||
@ -2019,10 +2026,10 @@ export function CustomAgentChatView({
|
|||||||
<div className="relative flex min-w-0 flex-1">
|
<div className="relative flex min-w-0 flex-1">
|
||||||
<textarea
|
<textarea
|
||||||
ref={composerRef}
|
ref={composerRef}
|
||||||
aria-label={`message CLI custom ${nodeId}`}
|
aria-label={`message CLI custom ${domId}`}
|
||||||
aria-controls={slashMenuOpen ? `slash-command-menu-${nodeId}` : undefined}
|
aria-controls={slashMenuOpen ? `slash-command-menu-${domId}` : undefined}
|
||||||
aria-activedescendant={
|
aria-activedescendant={
|
||||||
slashMenuOpen ? `slash-command-${nodeId}-${slashActiveIndex}` : undefined
|
slashMenuOpen ? `slash-command-${domId}-${slashActiveIndex}` : undefined
|
||||||
}
|
}
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-h-10 flex-1 resize-none rounded-md border border-border bg-surface p-2 pr-7 text-sm text-content outline-none",
|
"min-h-10 flex-1 resize-none rounded-md border border-border bg-surface p-2 pr-7 text-sm text-content outline-none",
|
||||||
@ -2069,7 +2076,7 @@ export function CustomAgentChatView({
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label={`redimensionner le composer ${nodeId}`}
|
aria-label={`redimensionner le composer ${domId}`}
|
||||||
title="Redimensionner le composer"
|
title="Redimensionner le composer"
|
||||||
className="absolute right-1.5 top-1.5 h-4 w-4 rounded-sm border border-border bg-raised text-[9px] leading-3 text-muted hover:text-content focus:border-primary focus:outline-none"
|
className="absolute right-1.5 top-1.5 h-4 w-4 rounded-sm border border-border bg-raised text-[9px] leading-3 text-muted hover:text-content focus:border-primary focus:outline-none"
|
||||||
style={{ cursor: "ns-resize" }}
|
style={{ cursor: "ns-resize" }}
|
||||||
|
|||||||
@ -113,10 +113,26 @@ describe("WebAgentCell", () => {
|
|||||||
|
|
||||||
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||||
expect(screen.queryByTestId("terminal-key-bar")).toBeNull();
|
expect(screen.queryByTestId("terminal-key-bar")).toBeNull();
|
||||||
|
await waitFor(() => expect(launchChatSpy).toHaveBeenCalledTimes(1));
|
||||||
|
expect(launchChatSpy.mock.calls[0][2]).toMatchObject({ cwd: "/srv/demo" });
|
||||||
|
expect(launchChatSpy.mock.calls[0][2]).not.toHaveProperty("nodeId");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("propagates the real leaf node id to the mobile custom CLI when provided", async () => {
|
||||||
|
const { agent, profile, agentId } = await seededAgentGateway(structuredProfile);
|
||||||
|
const launchChatSpy = vi.spyOn(agent, "launchAgentChat");
|
||||||
|
const leafNodeId = "1efdd2dd-981d-431b-915c-1dd5e976fdd5";
|
||||||
|
|
||||||
|
renderCell(agent, profile, agentId, {
|
||||||
|
fullHeight: true,
|
||||||
|
nodeId: leafNodeId,
|
||||||
|
});
|
||||||
|
fireEvent.click(await screen.findByRole("button", { name: "CLI custom" }));
|
||||||
|
|
||||||
await waitFor(() => expect(launchChatSpy).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(launchChatSpy).toHaveBeenCalledTimes(1));
|
||||||
expect(launchChatSpy.mock.calls[0][2]).toMatchObject({
|
expect(launchChatSpy.mock.calls[0][2]).toMatchObject({
|
||||||
cwd: "/srv/demo",
|
cwd: "/srv/demo",
|
||||||
nodeId: `web-mobile-${agentId}`,
|
nodeId: leafNodeId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -149,10 +165,8 @@ describe("WebAgentCell", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(launchChatSpy).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(launchChatSpy).toHaveBeenCalledTimes(1));
|
||||||
await waitFor(() => expect(reattachChatSpy).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(reattachChatSpy).toHaveBeenCalledTimes(1));
|
||||||
expect(launchChatSpy.mock.calls[0][2]).toMatchObject({
|
expect(launchChatSpy.mock.calls[0][2]).toMatchObject({ cwd: "/srv/demo" });
|
||||||
cwd: "/srv/demo",
|
expect(launchChatSpy.mock.calls[0][2]).not.toHaveProperty("nodeId");
|
||||||
nodeId: `web-mobile-${agentId}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "TUI native" }));
|
fireEvent.click(screen.getByRole("button", { name: "TUI native" }));
|
||||||
await waitFor(() => expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull());
|
await waitFor(() => expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull());
|
||||||
@ -183,11 +197,11 @@ describe("WebAgentCell", () => {
|
|||||||
expect(launchChatSpy).toHaveBeenCalledTimes(1);
|
expect(launchChatSpy).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("surfaces a mobile custom CLI launch failure when the runtime cannot find node", async () => {
|
it("surfaces a generic mobile custom CLI launch failure", async () => {
|
||||||
const { agent, profile, agentId } = await seededAgentGateway(structuredProfile);
|
const { agent, profile, agentId } = await seededAgentGateway(structuredProfile);
|
||||||
const launchChatSpy = vi.spyOn(agent, "launchAgentChat").mockRejectedValueOnce({
|
const launchChatSpy = vi.spyOn(agent, "launchAgentChat").mockRejectedValueOnce({
|
||||||
code: "PROCESS",
|
code: "PROCESS",
|
||||||
message: "spawn node ENOENT: node is not installed on this runtime",
|
message: "invalid node id: web-mobile-agent-1",
|
||||||
});
|
});
|
||||||
const reattachChatSpy = vi.spyOn(agent, "reattachAgentChat");
|
const reattachChatSpy = vi.spyOn(agent, "reattachAgentChat");
|
||||||
|
|
||||||
@ -195,12 +209,12 @@ describe("WebAgentCell", () => {
|
|||||||
fireEvent.click(await screen.findByRole("button", { name: "CLI custom" }));
|
fireEvent.click(await screen.findByRole("button", { name: "CLI custom" }));
|
||||||
|
|
||||||
await waitFor(() => expect(launchChatSpy).toHaveBeenCalledTimes(1));
|
await waitFor(() => expect(launchChatSpy).toHaveBeenCalledTimes(1));
|
||||||
expect(launchChatSpy.mock.calls[0][2]).toMatchObject({
|
expect(launchChatSpy.mock.calls[0][2]).toMatchObject({ cwd: "/srv/demo" });
|
||||||
cwd: "/srv/demo",
|
expect(launchChatSpy.mock.calls[0][2]).not.toHaveProperty("nodeId");
|
||||||
nodeId: `web-mobile-${agentId}`,
|
|
||||||
});
|
|
||||||
expect(reattachChatSpy).not.toHaveBeenCalled();
|
expect(reattachChatSpy).not.toHaveBeenCalled();
|
||||||
expect((await screen.findByRole("alert")).textContent).toContain("spawn node ENOENT");
|
const alertText = (await screen.findByRole("alert")).textContent ?? "";
|
||||||
|
expect(alertText).toContain("Le CLI custom n'a pas pu démarrer");
|
||||||
|
expect(alertText).not.toContain("invalid node id");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to native TUI when the mobile agent loses custom CLI support", async () => {
|
it("falls back to native TUI when the mobile agent loses custom CLI support", async () => {
|
||||||
|
|||||||
@ -203,7 +203,7 @@ export function WebAgentCell({
|
|||||||
agentName={resolvedAgent.name}
|
agentName={resolvedAgent.name}
|
||||||
profile={resolvedProfile}
|
profile={resolvedProfile}
|
||||||
cwd={cwd}
|
cwd={cwd}
|
||||||
nodeId={`web-mobile-${agentId}`}
|
nodeId={nodeId}
|
||||||
sessionId={customSessionId}
|
sessionId={customSessionId}
|
||||||
conversationId={customConversationId}
|
conversationId={customConversationId}
|
||||||
draftScope={draftScope}
|
draftScope={draftScope}
|
||||||
|
|||||||
Reference in New Issue
Block a user