Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,6 @@ export function AppLayout() {
<div className="pointer-events-none absolute inset-x-0 top-0 z-10 bg-background">
<ChatHeader
sidebarOpen={sidebar.isOpen}
isProcessing={manager.isProcessing}
model={manager.sessionInfo?.model}
sessionId={manager.sessionInfo?.sessionId}
totalCost={manager.totalCost}
Expand All @@ -522,6 +521,7 @@ export function AppLayout() {
</div>
<ChatView
messages={manager.messages}
isProcessing={manager.isProcessing}
extraBottomPadding={!!manager.pendingPermission}
scrollToMessageId={scrollToMessageId}
onScrolledToMessage={() => setScrollToMessageId(undefined)}
Expand Down
46 changes: 22 additions & 24 deletions src/core/runtime/hooks/useAgentRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ export function useOAgent({
const [contextUsage, setContextUsage] = useState<ContextUsage | null>(null);
const [isCompacting, setIsCompacting] = useState(false);
const [mcpServerStatuses, setMcpServerStatuses] = useState<McpServerStatus[]>([]);
const mapMcpStatuses = useCallback(
(servers: Array<{ name: string; status: string; error?: string }> = []): McpServerStatus[] =>
servers.map((s) => ({
name: s.name,
status: toMcpStatusState(s.status),
...(s.error ? { error: s.error } : {}),
})),
[],
);

const buffer = useRef(new StreamingBuffer());
const parentToolMap = useRef<ParentToolMap>(new Map());
Expand Down Expand Up @@ -256,22 +265,15 @@ export function useOAgent({
version: init.claude_code_version,
permissionMode: init.permissionMode,
});
if (init.mcp_servers?.length) {
setMcpServerStatuses(init.mcp_servers.map((s) => ({
name: s.name,
status: toMcpStatusState(s.status),
})));
// Auto-refresh detailed MCP status after a short delay (auth flows may still be in progress)
const sid = sessionIdRef.current;
if (sid) {
setTimeout(() => {
window.clientCore.mcpStatus(sid).then((result) => {
if (result.servers?.length) {
setMcpServerStatuses(result.servers as McpServerStatus[]);
}
}).catch(() => { /* session may have been stopped */ });
}, 3000);
}
setMcpServerStatuses(mapMcpStatuses(init.mcp_servers ?? []));
// Auto-refresh detailed MCP status after a short delay (auth flows may still be in progress)
const sid = sessionIdRef.current;
if (sid) {
setTimeout(() => {
window.clientCore.mcpStatus(sid).then((result) => {
setMcpServerStatuses(mapMcpStatuses(result.servers ?? []));
}).catch(() => { /* session may have been stopped */ });
}, 3000);
}
setIsConnected(true);
setIsProcessing(true);
Expand Down Expand Up @@ -587,16 +589,14 @@ export function useOAgent({
// After auth completes, refresh MCP server statuses
if (!authEvt.isAuthenticating && sessionIdRef.current) {
window.clientCore.mcpStatus(sessionIdRef.current).then((result) => {
if (result.servers?.length) {
setMcpServerStatuses(result.servers as McpServerStatus[]);
}
setMcpServerStatuses(mapMcpStatuses(result.servers ?? []));
}).catch(() => { /* session may have been stopped */ });
}
break;
}
}
},
[resetStreaming, scheduleFlush, flushNow, handleSubagentEvent],
[resetStreaming, scheduleFlush, flushNow, handleSubagentEvent, mapMcpStatuses],
);

const send = useCallback(
Expand Down Expand Up @@ -751,10 +751,8 @@ export function useOAgent({
const refreshMcpStatus = useCallback(async () => {
if (!sessionIdRef.current) return;
const result = await window.clientCore.mcpStatus(sessionIdRef.current);
if (result.servers?.length) {
setMcpServerStatuses(result.servers as McpServerStatus[]);
}
}, []);
setMcpServerStatuses(mapMcpStatuses(result.servers ?? []));
}, [mapMcpStatuses]);

const reconnectMcpServer = useCallback(async (serverName: string) => {
if (!sessionIdRef.current) return;
Expand Down
48 changes: 22 additions & 26 deletions src/core/workspace/hooks/useWorkspaceSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@
const [draftMcpStatuses, setDraftMcpStatuses] = useState<McpServerStatus[]>([]);
const draftMcpStatusesRef = useRef<McpServerStatus[]>([]);
draftMcpStatusesRef.current = draftMcpStatuses;
const mapMcpStatuses = useCallback(
(servers: Array<{ name: string; status: string; error?: string }> = []): McpServerStatus[] =>
servers.map((s) => ({
name: s.name,
status: toMcpStatusState(s.status),
...(s.error ? { error: s.error } : {}),
})),
[],
);
// OAP agent tracking — needed to restart the session with updated MCP servers
const oapAgentIdRef = useRef<string | null>(null);
// OAP-side session ID — persisted so we can call session/load on revival after restart
Expand Down Expand Up @@ -193,11 +202,8 @@
// couldn't match it (preStartedSessionIdRef was still null). Query MCP
// status directly now that the session is initialized.
const statusResult = await window.clientCore.mcpStatus(result.sessionId);
if (statusResult.servers?.length && preStartedSessionIdRef.current === result.sessionId) {
setDraftMcpStatuses(statusResult.servers.map(s => ({
name: s.name,
status: toMcpStatusState(s.status),
})));
if (preStartedSessionIdRef.current === result.sessionId) {
setDraftMcpStatuses(mapMcpStatuses(statusResult.servers ?? []));
}
} else {
// Draft was abandoned before eager start completed
Expand Down Expand Up @@ -351,12 +357,7 @@
backgroundStoreRef.current.handleEvent(event);
if (event.type === "system" && "subtype" in event && event.subtype === "init") {
const init = event as SystemInitEvent;
if (init.mcp_servers?.length) {
setDraftMcpStatuses(init.mcp_servers.map(s => ({
name: s.name,
status: toMcpStatusState(s.status),
})));
}
setDraftMcpStatuses(mapMcpStatuses(init.mcp_servers ?? []));
}
return;
}
Expand Down Expand Up @@ -987,6 +988,7 @@
setAcpMcpStatuses((result.mcpStatuses ?? []).map(s => ({
name: s.name,
status: toMcpStatusState(s.status),
...(s.error ? { error: s.error } : {}),

Check failure on line 991 in src/core/workspace/hooks/useWorkspaceSessions.ts

View workflow job for this annotation

GitHub Actions / build

Property 'error' does not exist on type '{ name: string; status: string; }'.

Check failure on line 991 in src/core/workspace/hooks/useWorkspaceSessions.ts

View workflow job for this annotation

GitHub Actions / build

Property 'error' does not exist on type '{ name: string; status: string; }'.
})));
setInitialMessages(messagesRef.current);
setInitialMeta({ isProcessing: false, isConnected: true, sessionInfo: null, totalCost: totalCostRef.current });
Expand Down Expand Up @@ -1258,9 +1260,13 @@
compact: engine.compact,
oapConfigOptions: oap.configOptions,
setOAPConfig: oap.setConfig,
mcpServerStatuses: isOAP
? (oapMcpStatuses.length > 0 ? oapMcpStatuses : draftMcpStatuses)
: (agent.mcpServerStatuses.length > 0 ? agent.mcpServerStatuses : draftMcpStatuses),
mcpServerStatuses: isDraft
? (
isOAP
? (oapMcpStatuses.length > 0 ? oapMcpStatuses : draftMcpStatuses)
: (agent.mcpServerStatuses.length > 0 ? agent.mcpServerStatuses : draftMcpStatuses)
)
: (isOAP ? oapMcpStatuses : agent.mcpServerStatuses),
mcpStatusPreliminary: isDraft && draftMcpStatuses.length > 0 && (
isOAP ? oapMcpStatuses.length === 0 : agent.mcpServerStatuses.length === 0
),
Expand All @@ -1269,12 +1275,7 @@
: (preStartedSessionId && isDraft)
? (async () => {
const result = await window.clientCore.mcpStatus(preStartedSessionId);
if (result.servers?.length) {
setDraftMcpStatuses(result.servers.map(s => ({
name: s.name,
status: toMcpStatusState(s.status),
})));
}
setDraftMcpStatuses(mapMcpStatuses(result.servers ?? []));
})
: agent.refreshMcpStatus,
reconnectMcpServer: isOAP
Expand All @@ -1298,12 +1299,7 @@
await new Promise(r => setTimeout(r, 3000));
}
const statusResult = await window.clientCore.mcpStatus(preStartedSessionId);
if (statusResult.servers?.length) {
setDraftMcpStatuses(statusResult.servers.map(s => ({
name: s.name,
status: toMcpStatusState(s.status),
})));
}
setDraftMcpStatuses(mapMcpStatuses(statusResult.servers ?? []));
})
: agent.reconnectMcpServer,
restartWithMcpServers: isOAP
Expand Down
10 changes: 0 additions & 10 deletions src/features/chat/components/ChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { memo, useState, useEffect } from "react";
import {
Loader2,
MoreHorizontal,
PanelLeft,
Pin,
Expand Down Expand Up @@ -35,7 +34,6 @@ const PERMISSION_MODE_LABELS: Record<string, string> = {

interface ChatHeaderProps {
sidebarOpen: boolean;
isProcessing: boolean;
model?: string;
sessionId?: string;
totalCost: number;
Expand All @@ -53,7 +51,6 @@ interface ChatHeaderProps {

export const ChatHeader = memo(function ChatHeader({
sidebarOpen,
isProcessing,
model,
sessionId,
totalCost,
Expand Down Expand Up @@ -151,13 +148,6 @@ export const ChatHeader = memo(function ChatHeader({
</div>
)}

{isProcessing && (
<span className="no-drag flex items-center gap-1.5 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
Processing
</span>
)}

{model && (
<Badge variant="secondary" className="no-drag text-[11px] font-normal">
{model}
Expand Down
34 changes: 33 additions & 1 deletion src/features/chat/components/ChatView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useRef, useMemo, useCallback, memo } from "react";
import { Loader2 } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import type { UIMessage } from "@/types";
import { MessageBubble } from "@/components/MessageBubble";
Expand All @@ -7,12 +8,19 @@ import { ToolCall } from "@/features/tools";

interface ChatViewProps {
messages: UIMessage[];
isProcessing: boolean;
extraBottomPadding?: boolean;
scrollToMessageId?: string;
onScrolledToMessage?: () => void;
}

export const ChatView = memo(function ChatView({ messages, extraBottomPadding, scrollToMessageId, onScrolledToMessage }: ChatViewProps) {
export const ChatView = memo(function ChatView({
messages,
isProcessing,
extraBottomPadding,
scrollToMessageId,
onScrolledToMessage,
}: ChatViewProps) {
const bottomRef = useRef<HTMLDivElement>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null);
const scrollTimerRef = useRef(0);
Expand Down Expand Up @@ -92,6 +100,17 @@ export const ChatView = memo(function ChatView({ messages, extraBottomPadding, s
return ids;
}, [messages]);

const activePromptId = useMemo(() => {
if (!isProcessing) return null;
for (let i = messages.length - 1; i >= 0; i -= 1) {
const msg = messages[i];
if (msg.role !== "user") continue;
const hasAssistantAfter = messages.slice(i + 1).some((next) => next.role === "assistant");
return hasAssistantAfter ? null : msg.id;
}
return null;
}, [messages, isProcessing]);

if (messages.length === 0) {
return (
<div className="flex flex-1 items-center justify-center text-muted-foreground">
Expand Down Expand Up @@ -123,6 +142,19 @@ export const ChatView = memo(function ChatView({ messages, extraBottomPadding, s
message={msg}
isContinuation={continuationIds.has(msg.id)}
/>
{msg.id === activePromptId && (
<div className="flex justify-end px-0 pb-1.5">
<div className="max-w-[76%] pe-1">
<span
aria-live="polite"
className="inline-flex items-center gap-1.5 text-xs text-muted-foreground"
>
<Loader2 className="h-3 w-3 animate-spin" />
Processing
</span>
</div>
</div>
)}
</div>
);
})}
Expand Down
Loading