mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Fix agent web loading issues
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user