Skip to content

Commit 0b756ec

Browse files
refactor(agentex-ui): DRY account picker; reset open task on account switch
- Account picker: extract a shared `iconTile` (loading + single-account collapsed tiles) and a shared `options` element (the SelectContent list previously duplicated across the collapsed and expanded switchers); single change handler `onAccountChange`. - Provider: `setSelectedAccountId` now clears `task_id` on an explicit switch (the open task is account-scoped and 404s under a new account). This preserves the reset that previously lived in agentex-ui-root's validation effect, which #347 reworked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent dee8932 commit 0b756ec

2 files changed

Lines changed: 59 additions & 50 deletions

File tree

agentex-ui/components/account-picker/account-picker.tsx

Lines changed: 50 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ export function AccountPicker({
4646
}),
4747
[queryClient]
4848
);
49+
const onAccountChange = useCallback(
50+
(id: string) => {
51+
if (id === selectedId) return;
52+
setSelectedAccountId(id);
53+
void refetchAccountScoped();
54+
},
55+
[selectedId, setSelectedAccountId, refetchAccountScoped]
56+
);
4957

5058
// Default to the first account when the URL has no valid account_id (fixes the
5159
// "no account → 401" first load). `replace` so it doesn't add a history entry.
@@ -62,20 +70,29 @@ export function AccountPicker({
6270
}, [profiles, selectedId, setSelectedAccountId, refetchAccountScoped]);
6371

6472
if (!accountsEnabled) return null;
65-
// While accounts load, show a disabled, empty picker (building icon only) rather than
66-
// a bare skeleton block — keeps the footer row stable and reads as "account selector,
67-
// loading". Once loaded with no accounts, render nothing.
73+
74+
// A size-9 icon tile — the collapsed-rail footprint. Shared by the loading placeholder
75+
// (muted) and the single-account display (solid, with the name as a tooltip).
76+
const iconTile = (opts?: { muted?: boolean; title?: string | undefined }) => (
77+
<div
78+
title={opts?.title}
79+
className={cn('flex size-9 items-center justify-center', className)}
80+
>
81+
<Building2
82+
className={cn(
83+
'size-5 shrink-0',
84+
opts?.muted ? 'text-muted-foreground' : 'text-foreground'
85+
)}
86+
/>
87+
</div>
88+
);
89+
90+
// While accounts load, show a disabled, empty picker (icon only) instead of a skeleton
91+
// block — keeps the row stable and reads as a loading account selector.
6892
if (isLoading) {
69-
if (collapsed) {
70-
return (
71-
<div
72-
className={cn('flex size-9 items-center justify-center', className)}
73-
>
74-
<Building2 className="text-muted-foreground size-5 shrink-0" />
75-
</div>
76-
);
77-
}
78-
return (
93+
return collapsed ? (
94+
iconTile({ muted: true })
95+
) : (
7996
<Select disabled>
8097
<SelectTrigger
8198
aria-label="Account"
@@ -88,31 +105,27 @@ export function AccountPicker({
88105
</Select>
89106
);
90107
}
91-
if (profiles.length === 0) return null;
92108

93-
const select = (id: string) => {
94-
if (id === selectedId) return;
95-
setSelectedAccountId(id);
96-
void refetchAccountScoped();
97-
};
109+
if (profiles.length === 0) return null;
98110

99111
const current = profiles.find(p => p.account.id === selectedId);
112+
const single = profiles.length === 1;
113+
const options = (
114+
<SelectContent>
115+
{profiles.map(p => (
116+
<SelectItem key={p.account.id} value={p.account.id}>
117+
{p.account.name}
118+
</SelectItem>
119+
))}
120+
</SelectContent>
121+
);
100122

101123
if (collapsed) {
102124
// Single account → static icon; multiple → an icon-only trigger whose dropdown pops
103-
// out beside the collapsed rail (Radix positions it to stay in view).
104-
if (profiles.length === 1) {
105-
return (
106-
<div
107-
title={current?.account.name}
108-
className={cn('flex size-9 items-center justify-center', className)}
109-
>
110-
<Building2 className="text-foreground size-5 shrink-0" />
111-
</div>
112-
);
113-
}
125+
// out beside the collapsed rail (Radix keeps it in view).
126+
if (single) return iconTile({ title: current?.account.name });
114127
return (
115-
<Select value={selectedId ?? ''} onValueChange={select}>
128+
<Select value={selectedId ?? ''} onValueChange={onAccountChange}>
116129
<SelectTrigger
117130
aria-label="Account"
118131
className={cn(
@@ -122,23 +135,17 @@ export function AccountPicker({
122135
>
123136
<Building2 className="text-foreground size-5 shrink-0" />
124137
</SelectTrigger>
125-
<SelectContent>
126-
{profiles.map(p => (
127-
<SelectItem key={p.account.id} value={p.account.id}>
128-
{p.account.name}
129-
</SelectItem>
130-
))}
131-
</SelectContent>
138+
{options}
132139
</Select>
133140
);
134141
}
135142

136-
// A single account needs no switcher — show it as static context.
137-
if (profiles.length === 1) {
143+
// A single account needs no switcher — show it as static context, matching the New Chat
144+
// button's size + spacing (size-5 icon, p-2, medium weight).
145+
if (single) {
138146
return (
139147
<div
140148
className={cn(
141-
// Match the New Chat button's size + spacing (size-5 icon, p-2, medium weight).
142149
'text-foreground flex items-center gap-2 p-2 text-sm font-medium select-none',
143150
className
144151
)}
@@ -150,7 +157,7 @@ export function AccountPicker({
150157
}
151158

152159
return (
153-
<Select value={selectedId ?? ''} onValueChange={select}>
160+
<Select value={selectedId ?? ''} onValueChange={onAccountChange}>
154161
<SelectTrigger
155162
className={cn('w-full gap-2 rounded-md font-medium', className)}
156163
aria-label="Account"
@@ -162,13 +169,7 @@ export function AccountPicker({
162169
<SelectValue placeholder="Select account" />
163170
</span>
164171
</SelectTrigger>
165-
<SelectContent>
166-
{profiles.map(p => (
167-
<SelectItem key={p.account.id} value={p.account.id}>
168-
{p.account.name}
169-
</SelectItem>
170-
))}
171-
</SelectContent>
172+
{options}
172173
</Select>
173174
);
174175
}

agentex-ui/components/providers/agentex-provider.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,15 @@ export function AgentexProvider({
5858
const setSelectedAccountId = useCallback(
5959
(id: string, replace = false) => {
6060
selectedAccountIdRef.current = id;
61-
updateParams({ [SearchParamKey.SGP_ACCOUNT_ID]: id }, replace);
61+
updateParams(
62+
{
63+
[SearchParamKey.SGP_ACCOUNT_ID]: id,
64+
// An explicit switch (not the initial bootstrap, which passes replace) drops the
65+
// open task — it's account-scoped and won't resolve under the new account.
66+
...(replace ? {} : { [SearchParamKey.TASK_ID]: null }),
67+
},
68+
replace
69+
);
6270
},
6371
[updateParams]
6472
);

0 commit comments

Comments
 (0)