Fix agent web loading issues

This commit is contained in:
Mohamed Boudra
2025-11-23 14:49:29 +01:00
parent 3b20cf0654
commit a95184cc5a
2 changed files with 315 additions and 23 deletions

View File

@@ -18,10 +18,55 @@ import { BackHeader } from "@/components/headers/back-header";
import { AgentStreamView } from "@/components/agent-stream-view";
import { AgentInputArea } from "@/components/agent-input-area";
import { useSession } from "@/contexts/session-context";
import type { Agent } from "@/contexts/session-context";
import { useFooterControls } from "@/contexts/footer-controls-context";
import { generateMessageId } from "@/types/stream";
const DROPDOWN_WIDTH = 220;
type BranchStatus = "idle" | "loading" | "ready" | "error";
function extractAgentModel(agent?: Agent | null): string | null {
if (!agent) {
return null;
}
const directModel = typeof agent.model === "string" ? agent.model.trim() : "";
if (directModel.length > 0) {
return directModel;
}
const metadata = agent.persistence?.metadata;
if (!metadata || typeof metadata !== "object") {
return null;
}
const persistedModel = (metadata as Record<string, unknown>).model;
if (typeof persistedModel === "string" && persistedModel.trim().length > 0) {
return persistedModel.trim();
}
const extra = (metadata as Record<string, unknown>).extra;
if (!extra || typeof extra !== "object") {
return null;
}
const getModelFrom = (source: unknown) => {
if (!source || typeof source !== "object") {
return null;
}
const candidate = (source as Record<string, unknown>).model;
return typeof candidate === "string" && candidate.trim().length > 0
? candidate.trim()
: null;
};
return (
getModelFrom((extra as Record<string, unknown>).codex) ??
getModelFrom((extra as Record<string, unknown>).claude)
);
}
export default function AgentScreen() {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
@@ -36,6 +81,7 @@ export default function AgentScreen() {
initializeAgent,
refreshAgent,
setFocusedAgentId,
ws,
} = useSession();
const { registerFooterControls, unregisterFooterControls } = useFooterControls();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
@@ -43,6 +89,10 @@ export default function AgentScreen() {
const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 });
const [menuContentHeight, setMenuContentHeight] = useState(0);
const menuButtonRef = useRef<View>(null);
const [branchStatus, setBranchStatus] = useState<BranchStatus>("idle");
const [branchLabel, setBranchLabel] = useState<string | null>(null);
const [branchError, setBranchError] = useState<string | null>(null);
const repoInfoRequestIdRef = useRef<string | null>(null);
// Keyboard animation
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
@@ -66,6 +116,86 @@ export default function AgentScreen() {
const agentPermissions = new Map(
Array.from(pendingPermissions.entries()).filter(([_, perm]) => perm.agentId === id)
);
const agentModel = extractAgentModel(agent);
const modelDisplayValue = agentModel ?? "Unknown";
const resetBranchState = useCallback(() => {
repoInfoRequestIdRef.current = null;
setBranchStatus("idle");
setBranchLabel(null);
setBranchError(null);
}, []);
const sendGitRepoInfoRequest = useCallback(
(cwd: string) => {
if (!cwd) {
resetBranchState();
return;
}
const requestId = generateMessageId();
repoInfoRequestIdRef.current = requestId;
setBranchStatus("loading");
setBranchLabel(null);
setBranchError(null);
ws.send({
type: "session",
message: {
type: "git_repo_info_request",
cwd,
requestId,
},
});
},
[resetBranchState, ws]
);
useEffect(() => {
if (!agent?.cwd) {
resetBranchState();
return;
}
sendGitRepoInfoRequest(agent.cwd);
}, [agent?.cwd, resetBranchState, sendGitRepoInfoRequest]);
useEffect(() => {
const unsubscribe = ws.on("git_repo_info_response", (message) => {
if (message.type !== "git_repo_info_response") {
return;
}
if (
repoInfoRequestIdRef.current &&
message.payload.requestId &&
message.payload.requestId !== repoInfoRequestIdRef.current
) {
return;
}
if (agent?.cwd && message.payload.cwd && message.payload.cwd !== agent.cwd) {
return;
}
repoInfoRequestIdRef.current = null;
if (message.payload.error) {
setBranchStatus("error");
setBranchError(message.payload.error);
setBranchLabel(null);
return;
}
setBranchStatus("ready");
setBranchError(null);
setBranchLabel(message.payload.currentBranch ?? null);
});
return () => {
unsubscribe();
};
}, [agent?.cwd, ws]);
useEffect(() => {
if (!id) {
@@ -169,10 +299,14 @@ export default function AgentScreen() {
);
const handleOpenMenu = useCallback(() => {
if (agent?.cwd) {
sendGitRepoInfoRequest(agent.cwd);
}
recalculateMenuPosition(() => {
setMenuVisible(true);
});
}, [recalculateMenuPosition]);
}, [agent?.cwd, recalculateMenuPosition, sendGitRepoInfoRequest]);
const handleCloseMenu = useCallback(() => {
setMenuVisible(false);
@@ -192,6 +326,10 @@ export default function AgentScreen() {
setMenuContentHeight((current) => (current === height ? current : height));
}, []);
const handleBackToHome = useCallback(() => {
router.replace("/");
}, [router]);
const handleViewChanges = useCallback(() => {
handleCloseMenu();
if (id) {
@@ -214,10 +352,15 @@ export default function AgentScreen() {
refreshAgent({ agentId: id });
}, [handleCloseMenu, id, refreshAgent]);
const branchDisplayValue =
branchStatus === "error"
? branchError ?? "Unavailable"
: branchLabel ?? "Unknown";
if (!agent) {
return (
<View style={styles.container}>
<BackHeader />
<BackHeader onBack={handleBackToHome} />
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Agent not found</Text>
</View>
@@ -230,6 +373,7 @@ export default function AgentScreen() {
{/* Header */}
<BackHeader
title={agent.title || "Agent"}
onBack={handleBackToHome}
rightContent={
<View ref={menuButtonRef} collapsable={false}>
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
@@ -282,6 +426,58 @@ export default function AgentScreen() {
]}
onLayout={handleMenuLayout}
>
<View style={styles.menuMetaContainer}>
<View style={styles.menuMetaRow}>
<Text style={styles.menuMetaLabel}>Directory</Text>
<Text
style={styles.menuMetaValue}
numberOfLines={2}
ellipsizeMode="middle"
>
{agent.cwd}
</Text>
</View>
<View style={styles.menuMetaRow}>
<Text style={styles.menuMetaLabel}>Model</Text>
<Text
style={styles.menuMetaValue}
numberOfLines={1}
ellipsizeMode="middle"
>
{modelDisplayValue}
</Text>
</View>
<View style={styles.menuMetaRow}>
<Text style={styles.menuMetaLabel}>Branch</Text>
<View style={styles.menuMetaValueRow}>
{branchStatus === "loading" ? (
<>
<ActivityIndicator
size="small"
color={theme.colors.mutedForeground}
/>
<Text style={styles.menuMetaPendingText}>Fetching</Text>
</>
) : (
<Text
style={[
styles.menuMetaValue,
branchStatus === "error" ? styles.menuMetaValueError : null,
]}
numberOfLines={1}
ellipsizeMode="middle"
>
{branchDisplayValue}
</Text>
)}
</View>
</View>
</View>
<View style={styles.menuDivider} />
<Pressable onPress={handleViewChanges} style={styles.menuItem}>
<GitBranch size={20} color={theme.colors.foreground} />
<Text style={styles.menuItemText}>View Changes</Text>
@@ -368,6 +564,40 @@ const styles = StyleSheet.create((theme) => ({
shadowRadius: 8,
elevation: 5,
},
menuMetaContainer: {
gap: theme.spacing[2],
marginBottom: theme.spacing[2],
},
menuMetaRow: {
gap: theme.spacing[1],
},
menuMetaLabel: {
fontSize: theme.fontSize.xs,
color: theme.colors.mutedForeground,
letterSpacing: 0.5,
textTransform: "uppercase",
},
menuMetaValue: {
fontSize: theme.fontSize.sm,
color: theme.colors.foreground,
},
menuMetaValueRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
},
menuMetaPendingText: {
fontSize: theme.fontSize.sm,
color: theme.colors.mutedForeground,
},
menuMetaValueError: {
color: theme.colors.destructive,
},
menuDivider: {
height: StyleSheet.hairlineWidth,
backgroundColor: theme.colors.border,
marginVertical: theme.spacing[2],
},
menuItem: {
flexDirection: "row",
alignItems: "center",

View File

@@ -7,7 +7,6 @@ import type {
ActivityLogPayload,
AgentSnapshotPayload,
AgentStreamEventPayload,
SessionInboundMessage,
WSInboundMessage,
GitSetupOptions,
} from "@server/server/messages";
@@ -104,6 +103,7 @@ export interface Agent {
lastError?: string | null;
title: string | null;
cwd: string;
model: string | null;
}
export interface Command {
@@ -178,6 +178,14 @@ const pushHistory = (history: string[], path: string): string[] => {
return [...normalizedHistory, path];
};
type PendingAgentLifecycleRequest = {
kind: "initialize" | "refresh";
params: {
agentId: string;
requestId?: string;
};
};
function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload): Agent {
const createdAt = new Date(snapshot.createdAt);
const updatedAt = new Date(snapshot.updatedAt);
@@ -200,6 +208,7 @@ function normalizeAgentSnapshot(snapshot: AgentSnapshotPayload): Agent {
lastError: snapshot.lastError ?? null,
title: snapshot.title ?? null,
cwd: snapshot.cwd,
model: snapshot.model ?? null,
};
}
@@ -293,6 +302,7 @@ interface SessionProviderProps {
export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
const ws = useWebSocket(serverUrl);
const wsIsConnected = ws.isConnected;
// State for voice detection flags (will be set by RealtimeContext)
const isDetectingRef = useRef(false);
@@ -322,6 +332,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
const activeAudioGroupsRef = useRef<Set<string>>(new Set());
const previousAgentStatusRef = useRef<Map<string, AgentLifecycleStatus>>(new Map());
const providerModelRequestIdsRef = useRef<Map<AgentProvider, string>>(new Map());
const pendingAgentLifecycleRequestsRef = useRef<PendingAgentLifecycleRequest[]>([]);
// Buffer for streaming audio chunks
interface AudioChunk {
@@ -377,6 +388,41 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
[]
);
const sendAgentLifecycleRequest = useCallback(
(request: PendingAgentLifecycleRequest) => {
const { kind, params } = request;
const messageType = kind === "initialize" ? "initialize_agent_request" : "refresh_agent_request";
const msg: WSInboundMessage = {
type: "session",
message: {
type: messageType,
agentId: params.agentId,
requestId: params.requestId,
},
};
ws.send(msg);
},
[ws]
);
useEffect(() => {
if (!wsIsConnected) {
return;
}
if (pendingAgentLifecycleRequestsRef.current.length === 0) {
return;
}
const pending = [...pendingAgentLifecycleRequestsRef.current];
pendingAgentLifecycleRequestsRef.current = [];
for (const request of pending) {
sendAgentLifecycleRequest(request);
}
}, [wsIsConnected, sendAgentLifecycleRequest]);
// WebSocket message handlers
useEffect(() => {
// Session state - initial agents/commands
@@ -484,6 +530,16 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
return next;
});
setInitializingAgents((prev) => {
const currentState = prev.get(agentId);
if (currentState === false) {
return prev;
}
const next = new Map(prev);
next.set(agentId, false);
return next;
});
setAgents((prev) => {
const existing = prev.get(agentId);
if (!existing) {
@@ -1016,16 +1072,19 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
return next;
});
const msg: WSInboundMessage = {
type: "session",
message: {
type: "initialize_agent_request",
agentId,
requestId,
},
};
ws.send(msg);
}, [ws]);
if (!wsIsConnected) {
pendingAgentLifecycleRequestsRef.current.push({
kind: "initialize",
params: { agentId, requestId },
});
return;
}
sendAgentLifecycleRequest({
kind: "initialize",
params: { agentId, requestId },
});
}, [wsIsConnected, sendAgentLifecycleRequest]);
const refreshAgent = useCallback(({ agentId, requestId }: { agentId: string; requestId?: string }) => {
setInitializingAgents((prev) => {
@@ -1040,16 +1099,19 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
return next;
});
const msg: WSInboundMessage = {
type: "session",
message: {
type: "refresh_agent_request",
agentId,
requestId,
},
};
ws.send(msg);
}, [ws]);
if (!wsIsConnected) {
pendingAgentLifecycleRequestsRef.current.push({
kind: "refresh",
params: { agentId, requestId },
});
return;
}
sendAgentLifecycleRequest({
kind: "refresh",
params: { agentId, requestId },
});
}, [wsIsConnected, sendAgentLifecycleRequest]);
const requestProviderModels = useCallback((provider: AgentProvider, options?: { cwd?: string }) => {
const requestId = generateMessageId();