Skip to content

Commit fe12b70

Browse files
authored
Merge branch 'Stack-Cairn:main' into main
2 parents 5b27808 + 88e7c5d commit fe12b70

24 files changed

Lines changed: 1357 additions & 207 deletions

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/components/chat/AskUserQuestionCard.tsx

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,23 @@ function formatCountdown(remainingMs: number) {
2626
/**
2727
* 倒计时提示:优先使用调用方传入的权威截止时间(GUI 读工具挂起表,WebUI 读
2828
* 网关参数上的 deadline 盖章),两端与桌面计时同源;缺失时(历史/降级数据)
29-
* 回退为挂载时刻近似。超时后 tool_result 会把卡片切到只读态。
29+
* 回退为挂载时刻近似。倒计时归零立即禁止交互,随后 tool_result 把卡片
30+
* 切到只读态。
31+
*
32+
* 盖章用的是桌面时钟,而倒计时读本机时钟:远端浏览器时钟偏移足够大时,
33+
* 一个仍在挂起的提问会在挂载瞬间就显示过期(或远超完整窗口)。因此仅当
34+
* 截止时间落在“挂载时刻(不含)~挂载时刻 + 完整应答窗口(含)”内才采信,
35+
* 否则视为时钟不可比、回退挂载近似,避免把可作答的卡片锁死;真正过期的
36+
* 提交仍由桌面挂起表权威拒绝。
3037
*/
3138
function useAnswerCountdown(active: boolean, deadlineAt?: number) {
32-
const [fallbackDeadline] = useState(() => Date.now() + ASK_USER_QUESTION_TIMEOUT_MS);
33-
const deadline = deadlineAt ?? fallbackDeadline;
39+
const [mountedAt] = useState(() => Date.now());
40+
const deadline =
41+
deadlineAt !== undefined &&
42+
deadlineAt > mountedAt &&
43+
deadlineAt <= mountedAt + ASK_USER_QUESTION_TIMEOUT_MS
44+
? deadlineAt
45+
: mountedAt + ASK_USER_QUESTION_TIMEOUT_MS;
3446
const [remainingMs, setRemainingMs] = useState(() => deadline - Date.now());
3547

3648
useEffect(() => {
@@ -98,8 +110,10 @@ export function AskUserQuestionCard({
98110

99111
const isSettled = (answers?.length ?? 0) > 0;
100112
const selections = isSettled ? settledSelections : draftSelections;
101-
const canInteract = interactive && !isSettled && !cancelled && !submitting;
102-
const remainingMs = useAnswerCountdown(interactive && !isSettled && !cancelled, deadlineAt);
113+
const countdownActive = interactive && !isSettled && !cancelled;
114+
const remainingMs = useAnswerCountdown(countdownActive, deadlineAt);
115+
const countdownExpired = countdownActive && remainingMs <= 0;
116+
const canInteract = countdownActive && remainingMs > 0 && !submitting;
103117

104118
// 该题是否已作答:普通选项已选,或“其他”选中且文本非空。
105119
const isQuestionAnswered = (questionId: string) => {
@@ -267,7 +281,9 @@ export function AskUserQuestionCard({
267281
canInteract && !isSelected
268282
? "hover:border-border/70 hover:bg-foreground/[0.03] dark:hover:border-white/[0.14]"
269283
: "",
270-
!canInteract && !isSelected && (isSettled || cancelled) ? "opacity-55" : "",
284+
!canInteract && !isSelected && (isSettled || cancelled || countdownExpired)
285+
? "opacity-55"
286+
: "",
271287
canInteract ? "cursor-pointer" : "cursor-default",
272288
)}
273289
>
@@ -323,7 +339,9 @@ export function AskUserQuestionCard({
323339
canInteract && !activeCustomSelected
324340
? "hover:border-border/70 hover:bg-foreground/[0.03] dark:hover:border-white/[0.14]"
325341
: "",
326-
!canInteract && !activeCustomSelected && (isSettled || cancelled)
342+
!canInteract &&
343+
!activeCustomSelected &&
344+
(isSettled || cancelled || countdownExpired)
327345
? "opacity-55"
328346
: "",
329347
canInteract ? "cursor-pointer" : "cursor-default",
@@ -403,7 +421,7 @@ export function AskUserQuestionCard({
403421
</span>
404422
<button
405423
type="button"
406-
disabled={!allAnswered || submitting}
424+
disabled={!allAnswered || !canInteract}
407425
onClick={() => void submit()}
408426
className="shrink-0 rounded-lg bg-primary px-3 py-1.5 text-[calc(11px*var(--zone-font-scale,1))] font-medium leading-none text-primary-foreground transition-opacity hover:opacity-90 disabled:pointer-events-none disabled:opacity-40"
409427
>

crates/agent-gateway/web/src/i18n/config.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,8 +1386,6 @@ export const translations: Record<Locale, Record<string, string>> = {
13861386
"settings.noCustomHeaders": "暂无自定义请求头",
13871387
"settings.noCustomHeadersHint": "点击此处新增,输入名称时会自动联想常用请求头。",
13881388
"settings.customHeaderReservedTitle": "保留头,由系统管理",
1389-
"settings.showCustomHeaderValue": "显示请求头值",
1390-
"settings.hideCustomHeaderValue": "隐藏请求头值",
13911389
"settings.manualAddModel": "手动添加",
13921390
"settings.customHeaderKeyPlaceholder": "请求头名称",
13931391
"settings.addCustomHeader": "添加",
@@ -3613,8 +3611,6 @@ export const translations: Record<Locale, Record<string, string>> = {
36133611
"settings.noCustomHeadersHint":
36143612
"Click to add one — common header names are suggested as you type.",
36153613
"settings.customHeaderReservedTitle": "Reserved header managed by the system",
3616-
"settings.showCustomHeaderValue": "Show header value",
3617-
"settings.hideCustomHeaderValue": "Hide header value",
36183614
"settings.manualAddModel": "Add manually",
36193615
"settings.customHeaderKeyPlaceholder": "Header name",
36203616
"settings.addCustomHeader": "Add",

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;

0 commit comments

Comments
 (0)