Skip to content

Commit bdc2539

Browse files
authored
Merge pull request Stack-Cairn#330 from hopingmiao/main
feat(settings): 模型选择器中增加直接编辑供应商的入口
2 parents 75374a9 + d803d5f commit bdc2539

13 files changed

Lines changed: 249 additions & 59 deletions

File tree

crates/agent-gateway/web/src/app/GatewayApp.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ export default function GatewayApp() {
311311
const [projectPickerOpen, setProjectPickerOpen] = useState(false);
312312
const [workspaceCreateModalOpen, setWorkspaceCreateModalOpen] = useState(false);
313313
const [settingsSection, setSettingsSection] = useState<SectionId>("system");
314+
const [settingsProviderId, setSettingsProviderId] = useState<string>();
314315
const [overlay, setOverlay] = useState<OverlayState>("closed");
315316
const { settings, setSettings, settingsSyncReady, settingsSyncError, settingsSaveState } =
316317
useGatewaySettingsSync({ token, api, activeAgentId: activeAgentScope });
@@ -3626,11 +3627,12 @@ export default function GatewayApp() {
36263627

36273628
const handleComposerBusyChange = useCallback((_isBusy: boolean) => {}, []);
36283629

3629-
function openSettings(section: SectionId = "system") {
3630+
function openSettings(section: SectionId = "system", providerId?: string) {
36303631
if (isMobileSidebarLayout()) {
36313632
setSidebarOpen(false);
36323633
}
36333634
setSettingsSection(section);
3635+
setSettingsProviderId(section === "providers" ? providerId : undefined);
36343636
setSettingsOpen(true);
36353637
setOverlay("entering");
36363638
requestAnimationFrame(() => requestAnimationFrame(() => setOverlay("open")));
@@ -5203,6 +5205,7 @@ export default function GatewayApp() {
52035205
saveState={settingsSaveState}
52045206
onBack={closeSettings}
52055207
initialSection={settingsSection}
5208+
initialProviderId={settingsProviderId}
52065209
hiddenSections={["remote"]}
52075210
onAgentDirectoryChanged={async () => {
52085211
if (!api) return;

crates/agent-gateway/web/src/pages/SettingsPage.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,13 @@ export function SettingsPage(props: SettingsPageProps) {
130130
saveState,
131131
onBack,
132132
initialSection = "system",
133+
initialProviderId,
133134
hiddenSections = [],
134135
onAgentDirectoryChanged,
135136
} = props;
136137
const { t } = useLocale();
137138
const [section, setSection] = useState<SectionId>(initialSection);
139+
const [pendingProviderId, setPendingProviderId] = useState(initialProviderId);
138140

139141
const sectionLabels: Record<SectionId, string> = {
140142
system: t("settings.navSystem"),
@@ -164,7 +166,8 @@ export function SettingsPage(props: SettingsPageProps) {
164166

165167
useEffect(() => {
166168
setSection(initialSection);
167-
}, [initialSection]);
169+
setPendingProviderId(initialProviderId);
170+
}, [initialProviderId, initialSection]);
168171

169172
useEffect(() => {
170173
if (allNavItems.some((item) => item.id === section)) {
@@ -177,7 +180,14 @@ export function SettingsPage(props: SettingsPageProps) {
177180
const sectionContent = (() => {
178181
switch (section) {
179182
case "providers":
180-
return <ProvidersSection settings={settings} setSettings={setSettings} />;
183+
return (
184+
<ProvidersSection
185+
settings={settings}
186+
setSettings={setSettings}
187+
initialProviderId={pendingProviderId}
188+
onInitialProviderHandled={() => setPendingProviderId(undefined)}
189+
/>
190+
);
181191
case "system":
182192
return <SystemSettingsForm settings={settings} setSettings={setSettings} />;
183193
case "systemTools":

crates/agent-gateway/web/src/pages/chat/ChatHeader.tsx

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Moon,
1313
OpenaiChatgptIcon,
1414
PanelLeft,
15+
Pencil,
1516
Search,
1617
Settings,
1718
Sun,
@@ -64,7 +65,7 @@ export const ChatHeader = memo(function ChatHeader(props: {
6465
// 模型下拉内嵌的执行模式分段器:请求切到 Chat("text") 或 Agent("tools")。
6566
// agent-dev 视为 Agent 的一种,由调用方决定是否保持不降级。
6667
onSelectExecutionMode: (mode: "text" | "tools") => void;
67-
onOpenSettings: (section?: SectionId) => void;
68+
onOpenSettings: (section?: SectionId, providerId?: string) => void;
6869
onToggleTheme: () => void;
6970
onOpenSidebar: () => void;
7071
preThemeActions?: ReactNode;
@@ -303,28 +304,50 @@ export const ChatHeader = memo(function ChatHeader(props: {
303304
return (
304305
<div key={group.id} className="flex flex-col gap-0.5">
305306
{groupIndex > 0 ? <hr className="my-1 h-px border-0 bg-muted" /> : null}
306-
<button
307-
type="button"
308-
onClick={() => toggleGroup(group.id)}
309-
aria-expanded={expanded}
310-
title={expanded ? t("chat.collapseProvider") : t("chat.expandProvider")}
311-
className="model-selector-group-label sticky top-0 z-10 flex h-[30px] w-full shrink-0 cursor-pointer items-center gap-1.5 bg-popover/95 px-2 py-0 text-left text-xs font-medium text-muted-foreground backdrop-blur transition-colors hover:bg-muted/40 focus-visible:bg-muted/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 supports-[backdrop-filter]:bg-popover/80 dark:text-white/80"
312-
>
313-
<ProviderBrandIcon
314-
type={group.providerType}
315-
className="h-3.5 w-3.5 opacity-90"
316-
/>
317-
<span className="min-w-0 flex-1 truncate">{group.name}</span>
318-
<span className="inline-flex h-4 min-w-[1.1rem] shrink-0 items-center justify-center rounded-full bg-muted/70 px-1 text-[10px] tabular-nums">
319-
{group.opts.length}
320-
</span>
321-
<ChevronDown
322-
className={cn(
323-
"h-3.5 w-3.5 shrink-0 transition-transform duration-200",
324-
expanded && "rotate-180",
325-
)}
326-
/>
327-
</button>
307+
<div className="group sticky top-0 z-10 flex h-[30px] shrink-0 items-stretch bg-popover/95 backdrop-blur transition-colors hover:bg-muted/40 focus-within:bg-muted/40 supports-[backdrop-filter]:bg-popover/80">
308+
<button
309+
type="button"
310+
onClick={() => toggleGroup(group.id)}
311+
aria-expanded={expanded}
312+
className="model-selector-group-label flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 px-2 py-0 text-left text-xs font-medium text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 dark:text-white/80"
313+
>
314+
<ProviderBrandIcon
315+
type={group.providerType}
316+
className="h-3.5 w-3.5 opacity-90"
317+
/>
318+
<span className="min-w-0 flex-1 truncate">{group.name}</span>
319+
</button>
320+
<button
321+
type="button"
322+
onClick={() => {
323+
setIsModelPickerOpen(false);
324+
onOpenSettings("providers", group.id);
325+
}}
326+
aria-label={`${t("settings.editProvider")}: ${group.name}`}
327+
className="pointer-events-none flex w-7 max-w-0 shrink-0 cursor-pointer items-center justify-center overflow-hidden text-muted-foreground/70 opacity-0 transition-[max-width,opacity,color,background-color] duration-150 group-hover:max-w-7 group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:max-w-7 group-focus-within:pointer-events-auto group-focus-within:opacity-100 hover:bg-muted/60 hover:text-foreground focus-visible:max-w-7 focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
328+
>
329+
<Pencil className="h-3.5 w-3.5" />
330+
</button>
331+
<button
332+
type="button"
333+
onClick={() => toggleGroup(group.id)}
334+
aria-expanded={expanded}
335+
aria-label={`${
336+
expanded ? t("chat.collapseProvider") : t("chat.expandProvider")
337+
}: ${group.name}`}
338+
className="model-selector-group-label flex shrink-0 cursor-pointer items-center gap-1.5 px-2 py-0 text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 dark:text-white/80"
339+
>
340+
<span className="inline-flex h-4 min-w-[1.1rem] shrink-0 items-center justify-center rounded-full bg-muted/70 px-1 text-[10px] tabular-nums">
341+
{group.opts.length}
342+
</span>
343+
<ChevronDown
344+
className={cn(
345+
"h-3.5 w-3.5 shrink-0 transition-transform duration-200",
346+
expanded && "rotate-180",
347+
)}
348+
/>
349+
</button>
350+
</div>
328351
{expanded
329352
? group.opts.map((option) => {
330353
const isSelected = option.value === selectedValue;

crates/agent-gateway/web/src/pages/settings/ProvidersSection.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3007,8 +3007,13 @@ function ProviderList(props: {
30073007
);
30083008
}
30093009

3010-
export function ProvidersSection(props: SettingsSectionProps) {
3011-
const { settings, setSettings } = props;
3010+
export function ProvidersSection(
3011+
props: SettingsSectionProps & {
3012+
initialProviderId?: string;
3013+
onInitialProviderHandled?: () => void;
3014+
},
3015+
) {
3016+
const { settings, setSettings, initialProviderId, onInitialProviderHandled } = props;
30123017
const { t } = useLocale();
30133018

30143019
const [activeTab, setActiveTab] = useState<ProviderId>("claude_code");
@@ -3019,6 +3024,19 @@ export function ProvidersSection(props: SettingsSectionProps) {
30193024
const { usageByProvider, refreshingProviderIds, refreshProvider } = useProviderUsage(
30203025
settings.customProviders,
30213026
);
3027+
const openedInitialProviderIdRef = useRef<string | null>(null);
3028+
3029+
useEffect(() => {
3030+
const providerId = initialProviderId?.trim();
3031+
if (!providerId || openedInitialProviderIdRef.current === providerId) return;
3032+
const provider = settings.customProviders.find((item) => item.id === providerId);
3033+
if (!provider) return;
3034+
openedInitialProviderIdRef.current = providerId;
3035+
setActiveTab(provider.type);
3036+
setEditingProvider(provider);
3037+
setModalOpen(true);
3038+
onInitialProviderHandled?.();
3039+
}, [initialProviderId, onInitialProviderHandled, settings.customProviders]);
30223040

30233041
function openAdd() {
30243042
setEditingProvider(null);

crates/agent-gateway/web/src/pages/settings/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type SettingsPageProps = {
2121
saveState: WebSettingsSaveState;
2222
onBack: () => void;
2323
initialSection?: SectionId;
24+
initialProviderId?: string;
2425
hiddenSections?: SectionId[];
2526
onAgentDirectoryChanged?: () => void | Promise<void>;
2627
};

crates/agent-gui/src/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ function applyRuntimeSystemDefaults(settings: AppSettings, defaultWorkdir: strin
169169
export default function App() {
170170
const [settingsOpen, setSettingsOpen] = useState(false);
171171
const [settingsSection, setSettingsSection] = useState<SectionId>("system");
172+
const [settingsProviderId, setSettingsProviderId] = useState<string>();
172173
const [settingsReady, setSettingsReady] = useState(false);
173174
const [settings, setSettingsState] = useState<AppSettings>(() => getDefaultSettings());
174175
const [settingsSaveState, setSettingsSaveState] = useState<SettingsSaveState>({
@@ -405,8 +406,9 @@ export default function App() {
405406
);
406407

407408
const openSettings = useCallback(
408-
(section: SectionId = "system") => {
409+
(section: SectionId = "system", providerId?: string) => {
409410
setSettingsSection(section);
411+
setSettingsProviderId(section === "providers" ? providerId : undefined);
410412
setSettingsOpen(true);
411413
setOverlay("entering");
412414
requestAnimationFrame(() => requestAnimationFrame(() => setOverlay("open")));
@@ -617,6 +619,7 @@ export default function App() {
617619
saveState={settingsSaveState}
618620
onBack={closeSettings}
619621
initialSection={settingsSection}
622+
initialProviderId={settingsProviderId}
620623
appUpdate={appUpdate}
621624
/>
622625
</AppErrorBoundary>

crates/agent-gui/src/pages/ChatPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ type ChatPageProps = {
161161
getToolPolicies: () => AppSettings["system"]["toolPolicies"];
162162
context: Context;
163163
setContext: (next: Context) => void;
164-
onOpenSettings: (section?: SectionId) => void;
164+
onOpenSettings: (section?: SectionId, providerId?: string) => void;
165165
onToggleTheme: () => void;
166166
appUpdate?: AppUpdateController;
167167
};

crates/agent-gui/src/pages/SettingsPage.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,13 @@ export function SettingsPage(props: SettingsPageProps) {
137137
saveState,
138138
onBack,
139139
initialSection = "system",
140+
initialProviderId,
140141
hiddenSections = [],
141142
appUpdate,
142143
} = props;
143144
const { t } = useLocale();
144145
const [section, setSection] = useState<SectionId>(initialSection);
146+
const [pendingProviderId, setPendingProviderId] = useState(initialProviderId);
145147

146148
const sectionLabels: Record<SectionId, string> = {
147149
system: t("settings.navSystem"),
@@ -172,7 +174,8 @@ export function SettingsPage(props: SettingsPageProps) {
172174

173175
useEffect(() => {
174176
setSection(initialSection);
175-
}, [initialSection]);
177+
setPendingProviderId(initialProviderId);
178+
}, [initialProviderId, initialSection]);
176179

177180
useEffect(() => {
178181
if (allNavItems.some((item) => item.id === section)) {
@@ -185,7 +188,14 @@ export function SettingsPage(props: SettingsPageProps) {
185188
const sectionContent = (() => {
186189
switch (section) {
187190
case "providers":
188-
return <ProvidersSection settings={settings} setSettings={setSettings} />;
191+
return (
192+
<ProvidersSection
193+
settings={settings}
194+
setSettings={setSettings}
195+
initialProviderId={pendingProviderId}
196+
onInitialProviderHandled={() => setPendingProviderId(undefined)}
197+
/>
198+
);
189199
case "system":
190200
return <SystemSettingsForm settings={settings} setSettings={setSettings} />;
191201
case "shortcuts":

0 commit comments

Comments
 (0)