Skip to content

Commit 32bd3f2

Browse files
fix(agentex-ui): hydrate agent from task on deep-link, drop inaccessible task_id
A URL carrying only task_id (a shared or hand-built link) left the agent pill empty, and a task the caller can't see showed a phantom task view. - ChatView fills agent_name from the task's own (access-checked) agent when the URL omits it, and drops task_id on a 403/404 retrieve instead of leaving a phantom. - agentex-ui-root no longer restores the last-used agent from storage while a task is open, so it can't override the task's agent. Access is unchanged: the BFF attaches credentials server-side and account_id is only a selector, so this can only surface tasks/agents the caller was already authorized to load. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 824286c commit 32bd3f2

3 files changed

Lines changed: 50 additions & 4 deletions

File tree

agentex-ui/components/agentex-ui-root.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,21 @@ export function AgentexUIRoot() {
6363
// than bouncing a possibly-valid deep link to the home grid.
6464
const couldNotDetermine = !agentInList && isAgentByNameError;
6565

66-
if (agentName && !isAgentValid && !couldNotDetermine) {
66+
// Only validate/clear on the home surface. With a task open its agent wins (hydrated in
67+
// ChatView) even if non-Ready, so this doesn't ping-pong against that restore.
68+
if (agentName && !taskID && !isAgentValid && !couldNotDetermine) {
6769
updateParams({ [SearchParamKey.AGENT_NAME]: null });
6870
setLocalAgentName(undefined);
6971
}
7072

71-
if (!agentName && localAgentName) {
73+
// With a task open, its agent wins (hydrated in ChatView) — don't restore from storage.
74+
if (!agentName && !taskID && localAgentName) {
7275
updateParams({ [SearchParamKey.AGENT_NAME]: localAgentName });
7376
}
7477
// eslint-disable-next-line react-hooks/exhaustive-deps
7578
}, [
7679
sgpAccountID,
80+
taskID,
7781
isAgentsFetching,
7882
isAgentByNameFetching,
7983
isAgentByNameError,

agentex-ui/components/primary-content/chat-view.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { useCallback, useRef } from 'react';
1+
import { useCallback, useEffect, useRef } from 'react';
22

3+
import { APIError } from 'agentex';
34
import { motion } from 'framer-motion';
45

56
import { TaskProvider, useAgentexClient } from '@/components/providers';
@@ -9,6 +10,7 @@ import {
910
SearchParamKey,
1011
useSafeSearchParams,
1112
} from '@/hooks/use-safe-search-params';
13+
import { useTask } from '@/hooks/use-tasks';
1214

1315
import { TaskHeader } from '../task-header/task-header';
1416

@@ -29,10 +31,41 @@ export function ChatView({
2931
}: ChatViewProps) {
3032
const { agentexClient } = useAgentexClient();
3133
const { data: agents = [] } = useAgents(agentexClient);
32-
const { updateParams } = useSafeSearchParams();
34+
const { agentName, updateParams } = useSafeSearchParams();
35+
const {
36+
data: task,
37+
isError: isTaskError,
38+
error: taskError,
39+
} = useTask({ agentexClient, taskId: taskID });
3340

3441
const headerRef = useRef<HTMLDivElement>(null);
3542

43+
// Deep links may carry only task_id — fill the agent pill from the task's own agent.
44+
const taskAgentName = task?.agents?.[0]?.name;
45+
useEffect(() => {
46+
if (agentName || !taskAgentName) return;
47+
updateParams({ [SearchParamKey.AGENT_NAME]: taskAgentName }, true);
48+
}, [agentName, taskAgentName, updateParams]);
49+
50+
// Drop a task_id the client can't use: a 4xx means it's bad/off-limits (400/403/404/422).
51+
// 401 is handled by the session refresh + login redirect, and 429/5xx are transient.
52+
useEffect(() => {
53+
const status = taskError instanceof APIError ? taskError.status : undefined;
54+
if (
55+
isTaskError &&
56+
status !== undefined &&
57+
status >= 400 &&
58+
status < 500 &&
59+
status !== 401 &&
60+
status !== 429
61+
) {
62+
updateParams(
63+
{ [SearchParamKey.TASK_ID]: null, [SearchParamKey.AGENT_NAME]: null },
64+
true
65+
);
66+
}
67+
}, [isTaskError, taskError, updateParams]);
68+
3669
const handleSelectAgent = useCallback(
3770
(agentName: string | undefined) => {
3871
updateParams({

agentex-ui/hooks/use-tasks.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
2+
import { APIError } from 'agentex';
23

34
import type AgentexSDK from 'agentex';
45
import type {
@@ -42,6 +43,14 @@ export function useTask({
4243
});
4344
},
4445
enabled: !!taskId,
46+
// A 4xx (bad/forbidden task_id) is definitive — fail fast so a deep-link denial clears on
47+
// the first response instead of after retry backoff. Still retry transient 5xx/network.
48+
retry: (failureCount, error) =>
49+
!(
50+
error instanceof APIError &&
51+
error.status >= 400 &&
52+
error.status < 500
53+
) && failureCount < 3,
4554
});
4655
}
4756

0 commit comments

Comments
 (0)