Skip to content

Commit be6220a

Browse files
committed
chore: update use-acp to version 0.2.4 and enhance chat UI
- Added `white-space: pre-wrap` to code blocks in markdown renderer CSS for better text formatting. - Introduced a new `ReadyToChatBlock` component to display a message when the agent is connected. - Better loading state when a new session is being creating - Clear session when connectin gto a new agent
1 parent a35648c commit be6220a

File tree

8 files changed

+233
-95
lines changed

8 files changed

+233
-95
lines changed

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
"thememirror": "^2.0.1",
165165
"timestring": "^7.0.0",
166166
"typescript-memoize": "^1.1.1",
167-
"use-acp": "^0.2.3",
167+
"use-acp": "0.2.4",
168168
"use-resize-observer": "^9.1.0",
169169
"vega-lite": "^5.23.0",
170170
"vega-loader": "^4.5.3",

frontend/src/components/chat/acp/__tests__/state.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,62 @@ describe("state utility functions", () => {
171171
`);
172172
});
173173

174+
it("should clear externalAgentSessionId when switching between different agents", () => {
175+
// Start with a Claude session that has an active external session ID
176+
const claudeSession: AgentSession = {
177+
agentId: "claude",
178+
tabId: "tab_claude" as TabId,
179+
title: "Claude session",
180+
createdAt: 1_735_689_600_000,
181+
lastUsedAt: 1_735_689_600_000,
182+
externalAgentSessionId: "claude-session-123" as ExternalAgentSessionId,
183+
};
184+
const initialState: AgentSessionState = {
185+
sessions: [claudeSession],
186+
activeTabId: claudeSession.tabId,
187+
};
188+
189+
// Switch to Gemini
190+
const newState = addSession(initialState, { agentId: "gemini" });
191+
192+
// Should create a new Gemini session with null externalAgentSessionId
193+
// and remove the Claude session (MAX_SESSIONS = 1)
194+
expect(newState.sessions).toHaveLength(1);
195+
expect(newState.sessions[0].agentId).toBe("gemini");
196+
expect(newState.sessions[0].externalAgentSessionId).toBe(null);
197+
198+
// The active tab should be the new Gemini session
199+
expect(newState.activeTabId).toBe(newState.sessions[0].tabId);
200+
});
201+
202+
it("should clear externalAgentSessionId when switching from Gemini to Claude", () => {
203+
// Start with a Gemini session that has an active external session ID
204+
const geminiSession: AgentSession = {
205+
agentId: "gemini",
206+
tabId: "tab_gemini" as TabId,
207+
title: "Gemini session",
208+
createdAt: 1_735_689_600_000,
209+
lastUsedAt: 1_735_689_600_000,
210+
externalAgentSessionId: "gemini-session-456" as ExternalAgentSessionId,
211+
};
212+
const initialState: AgentSessionState = {
213+
sessions: [geminiSession],
214+
activeTabId: geminiSession.tabId,
215+
};
216+
217+
// Switch to Claude
218+
const newState = addSession(initialState, { agentId: "claude" });
219+
220+
// Should create a new Claude session with null externalAgentSessionId
221+
// and remove the Gemini session (MAX_SESSIONS = 1)
222+
expect(newState.sessions).toHaveLength(1);
223+
expect(newState.sessions[0].agentId).toBe("claude");
224+
expect(newState.sessions[0].externalAgentSessionId).toBe(null);
225+
226+
// The active tab should be the new Claude session
227+
expect(newState.activeTabId).toBe(newState.sessions[0].tabId);
228+
});
229+
174230
it("should not mutate original state", () => {
175231
const initialState: AgentSessionState = {
176232
sessions: [],

frontend/src/components/chat/acp/agent-panel.tsx

Lines changed: 142 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,14 @@ import {
6262
SelectTrigger,
6363
} from "@/components/ui/select";
6464
import { toast } from "@/components/ui/use-toast";
65+
import { DelayMount } from "@/components/utils/delay-mount";
6566
import { useRequestClient } from "@/core/network/requests";
6667
import { filenameAtom } from "@/core/saving/file-state";
6768
import { store } from "@/core/state/jotai";
6869
import { Functions } from "@/utils/functions";
6970
import { Paths } from "@/utils/paths";
7071
import { FileAttachmentPill } from "../chat-components";
72+
import { ReadyToChatBlock } from "./blocks";
7173
import {
7274
convertFilesToResourceLinks,
7375
parseContextFromPrompt,
@@ -548,6 +550,52 @@ const ChatContent = memo<ChatContentProps>(
548550
}
549551
}, [notifications.length, isScrolledToBottom, scrollToBottom]);
550552

553+
const renderThread = () => {
554+
if (hasNotifications) {
555+
return (
556+
<AgentThread
557+
isConnected={connectionState.status === "connected"}
558+
notifications={notifications}
559+
onRetryConnection={onRetryConnection}
560+
onRetryLastAction={onRetryLastAction}
561+
/>
562+
);
563+
}
564+
565+
const isConnected = connectionState.status === "connected";
566+
if (isConnected) {
567+
return <ReadyToChatBlock />;
568+
}
569+
570+
return (
571+
<div className="flex items-center justify-center h-full min-h-[200px] flex-col">
572+
<PanelEmptyState
573+
title="Waiting for agent"
574+
description="Your AI agent will appear here when active"
575+
icon={<BotMessageSquareIcon />}
576+
/>
577+
{isDisconnected && agentId && (
578+
<AgentDocs
579+
className="border-t pt-6 px-5"
580+
title="Make sure you're connected to an agent"
581+
description="Run this command in your terminal:"
582+
agents={[agentId]}
583+
/>
584+
)}
585+
{isDisconnected && (
586+
<Button
587+
variant="outline"
588+
onClick={onRetryConnection}
589+
type="button"
590+
className="mt-4"
591+
>
592+
Retry
593+
</Button>
594+
)}
595+
</div>
596+
);
597+
};
598+
551599
return (
552600
<div className="flex-1 flex flex-col overflow-hidden flex-shrink-0 relative">
553601
{pendingPermission && (
@@ -569,43 +617,7 @@ const ChatContent = memo<ChatContentProps>(
569617
Session ID: {sessionId}
570618
</div>
571619
)}
572-
{hasNotifications ? (
573-
<div className="space-y-2">
574-
<AgentThread
575-
isConnected={connectionState.status === "connected"}
576-
notifications={notifications}
577-
onRetryConnection={onRetryConnection}
578-
onRetryLastAction={onRetryLastAction}
579-
onDismissError={onDismissError}
580-
/>
581-
</div>
582-
) : (
583-
<div className="flex items-center justify-center h-full min-h-[200px] flex-col">
584-
<PanelEmptyState
585-
title="Waiting for agent"
586-
description="Your AI agent will appear here when active"
587-
icon={<BotMessageSquareIcon />}
588-
/>
589-
{isDisconnected && agentId && (
590-
<AgentDocs
591-
className="border-t pt-6 px-5"
592-
title="Make sure you're connected to an agent"
593-
description="Run this command in your terminal:"
594-
agents={[agentId]}
595-
/>
596-
)}
597-
{isDisconnected && (
598-
<Button
599-
variant="outline"
600-
onClick={onRetryConnection}
601-
type="button"
602-
className="mt-4"
603-
>
604-
Retry
605-
</Button>
606-
)}
607-
</div>
608-
)}
620+
{renderThread()}
609621
</div>
610622

611623
<ScrollToBottomButton
@@ -671,6 +683,7 @@ const AgentPanel: React.FC = () => {
671683
const {
672684
connect,
673685
disconnect,
686+
setActiveSessionId,
674687
connectionState,
675688
notifications,
676689
pendingPermission,
@@ -695,14 +708,18 @@ const AgentPanel: React.FC = () => {
695708

696709
// Auto-connect to agent when we have an active session, but only once per session
697710
useEffect(() => {
711+
setActiveSessionId(null);
712+
698713
if (wsUrl === NO_WS_SET) {
699714
return;
700715
}
701716

702717
logger.debug("Auto-connecting to agent", {
703718
sessionId: activeSessionId,
704719
});
705-
connect();
720+
void connect().catch((error) => {
721+
logger.error("Failed to connect to agent", { error });
722+
});
706723

707724
return () => {
708725
// We don't want to disconnect so users can switch between different
@@ -969,6 +986,92 @@ const AgentPanel: React.FC = () => {
969986
);
970987
}
971988

989+
const renderBody = () => {
990+
const isConnecting = connectionState.status === "connecting";
991+
const delay = 200; // ms
992+
if (isConnecting) {
993+
return (
994+
<DelayMount milliseconds={delay}>
995+
<div className="flex items-center justify-center h-full min-h-[200px] flex-col">
996+
<Spinner size="medium" className="text-primary" />
997+
<span className="text-sm text-muted-foreground">
998+
Connecting to the agent...
999+
</span>
1000+
</div>
1001+
</DelayMount>
1002+
);
1003+
}
1004+
1005+
const isLoadingSession =
1006+
tabLastActiveSessionId == null && connectionState.status === "connected";
1007+
if (isLoadingSession) {
1008+
return (
1009+
<DelayMount milliseconds={delay}>
1010+
<div className="flex items-center justify-center h-full min-h-[200px] flex-col">
1011+
<Spinner size="medium" className="text-primary" />
1012+
<span className="text-sm text-muted-foreground">
1013+
Creating a new session...
1014+
</span>
1015+
</div>
1016+
</DelayMount>
1017+
);
1018+
}
1019+
1020+
return (
1021+
<>
1022+
<ChatContent
1023+
key={activeSessionId}
1024+
agentId={selectedTab?.agentId}
1025+
sessionId={selectedTab?.externalAgentSessionId ?? null}
1026+
hasNotifications={hasNotifications}
1027+
connectionState={connectionState}
1028+
notifications={notifications}
1029+
pendingPermission={pendingPermission}
1030+
onResolvePermission={(option) => {
1031+
logger.debug("Resolving permission request", {
1032+
sessionId: activeSessionId,
1033+
option,
1034+
});
1035+
resolvePermission(option);
1036+
}}
1037+
onRetryConnection={handleManualConnect}
1038+
/>
1039+
1040+
<LoadingIndicator
1041+
isLoading={isLoading}
1042+
isRequestingPermission={!!pendingPermission}
1043+
onStop={handleStop}
1044+
/>
1045+
1046+
{files && files.length > 0 && (
1047+
<div className="flex flex-row gap-1 flex-wrap p-3 border-t">
1048+
{files.map((file) => (
1049+
<FileAttachmentPill
1050+
file={file}
1051+
key={file.name}
1052+
onRemove={() => handleRemoveFile(file)}
1053+
/>
1054+
))}
1055+
</div>
1056+
)}
1057+
1058+
<PromptArea
1059+
isLoading={isLoading}
1060+
activeSessionId={activeSessionId}
1061+
promptValue={promptValue}
1062+
onPromptValueChange={setPromptValue}
1063+
onPromptSubmit={handlePromptSubmit}
1064+
onAddFiles={handleAddFiles}
1065+
onStop={handleStop}
1066+
fileInputRef={fileInputRef}
1067+
commands={availableCommands}
1068+
sessionMode={sessionMode}
1069+
onModeChange={handleModeChange}
1070+
/>
1071+
</>
1072+
);
1073+
};
1074+
9721075
return (
9731076
<div className="flex flex-col flex-1 overflow-hidden mo-agent-panel">
9741077
<AgentPanelHeader
@@ -982,54 +1085,7 @@ const AgentPanel: React.FC = () => {
9821085
/>
9831086
<SessionTabs />
9841087

985-
<ChatContent
986-
agentId={selectedTab?.agentId}
987-
sessionId={activeSessionId}
988-
hasNotifications={hasNotifications}
989-
connectionState={connectionState}
990-
notifications={notifications}
991-
pendingPermission={pendingPermission}
992-
onResolvePermission={(option) => {
993-
logger.debug("Resolving permission request", {
994-
sessionId: activeSessionId,
995-
option,
996-
});
997-
resolvePermission(option);
998-
}}
999-
onRetryConnection={handleManualConnect}
1000-
/>
1001-
1002-
<LoadingIndicator
1003-
isLoading={isLoading}
1004-
isRequestingPermission={!!pendingPermission}
1005-
onStop={handleStop}
1006-
/>
1007-
1008-
{files && files.length > 0 && (
1009-
<div className="flex flex-row gap-1 flex-wrap p-3 border-t">
1010-
{files.map((file) => (
1011-
<FileAttachmentPill
1012-
file={file}
1013-
key={file.name}
1014-
onRemove={() => handleRemoveFile(file)}
1015-
/>
1016-
))}
1017-
</div>
1018-
)}
1019-
1020-
<PromptArea
1021-
isLoading={isLoading}
1022-
activeSessionId={activeSessionId}
1023-
promptValue={promptValue}
1024-
onPromptValueChange={setPromptValue}
1025-
onPromptSubmit={handlePromptSubmit}
1026-
onAddFiles={handleAddFiles}
1027-
onStop={handleStop}
1028-
fileInputRef={fileInputRef}
1029-
commands={availableCommands}
1030-
sessionMode={sessionMode}
1031-
onModeChange={handleModeChange}
1032-
/>
1088+
{renderBody()}
10331089
</div>
10341090
);
10351091
};

0 commit comments

Comments
 (0)