feat: add frosted glass appearance and improve local MCP recovery

Add an opt-in persistent frosted glass setting for the top bar and runtime checklist, with chat scrolling behind the header while preserving scroll anchors and window controls.

Retry transient local MCP initialization failures once after cleanup, include redacted connection diagnostics in feedback, and index lifecycle events to reduce execution statistics scans.

Release note: Add optional frosted glass appearance with chat visible behind the title bar; improve local tool connection recovery and diagnostic feedback.
This commit is contained in:
mesalogo
2026-09-30 23:37:26 +08:00
parent e4f7b8d993
commit f7f501a8e3
39 changed files with 1666 additions and 251 deletions
+86
View File
@@ -1211,6 +1211,7 @@ function installRemoteProjectsSetting(enabled: boolean): {
let settings: ApplicationSettings = {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github",
modelDownloadSource: "modelscope",
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3291,6 +3292,7 @@ describe("App", () => {
getSettings: vi.fn(async () => ({
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3395,6 +3397,7 @@ describe("App", () => {
getSettings: vi.fn(async () => ({
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3411,6 +3414,7 @@ describe("App", () => {
updateSettings: vi.fn(async () => ({
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3513,6 +3517,7 @@ describe("App", () => {
getSettings: vi.fn(async () => ({
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -3529,6 +3534,7 @@ describe("App", () => {
updateSettings: vi.fn(async () => ({
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -5922,6 +5928,7 @@ describe("App", () => {
let applicationSettings: ApplicationSettings = {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github",
modelDownloadSource: "modelscope",
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -7835,6 +7842,77 @@ describe("App", () => {
expect(localStorage.getItem("goodbuddy.appearance-theme")).toBe("dark");
});
it('applies frosted glass only after saving and removes it when disabled', async () => {
const updates = api.updates!
const { container, unmount } = render(<App />)
const shell = container.querySelector('.app-shell')
expect(shell).not.toHaveAttribute('data-frosted-glass')
expect(document.documentElement).not.toHaveAttribute('data-frosted-glass')
fireEvent.click(await screen.findByRole('button', { name: '设置' }))
fireEvent.click(await screen.findByRole('tab', { name: '外观' }))
const toggle = screen.getByRole('switch', { name: '透明磨砂特效' })
expect(toggle.closest('.toggle-row')?.firstElementChild).toHaveTextContent('透明磨砂特效')
expect(toggle.closest('.toggle-row')?.lastElementChild).toBe(toggle)
await waitFor(() => expect(toggle).toBeEnabled())
const saved = { ...await updates.getSettings(), transparentFrostedEffectEnabled: true }
let resolveSave!: (settings: ApplicationSettings) => void
vi.mocked(updates.updateSettings).mockImplementationOnce(() => new Promise(resolve => { resolveSave = resolve }))
fireEvent.click(toggle)
expect(toggle).toBeDisabled()
expect(toggle).not.toBeChecked()
expect(shell).not.toHaveAttribute('data-frosted-glass')
expect(document.documentElement).not.toHaveAttribute('data-frosted-glass')
expect(updates.updateSettings).toHaveBeenLastCalledWith({ transparentFrostedEffectEnabled: true })
await act(async () => resolveSave(saved))
expect(toggle).toBeChecked()
expect(toggle).toBeEnabled()
expect(shell).toHaveAttribute('data-frosted-glass', 'true')
expect(document.documentElement).toHaveAttribute('data-frosted-glass', 'true')
fireEvent.click(toggle)
await waitFor(() => {
expect(toggle).not.toBeChecked()
expect(toggle).toBeEnabled()
expect(shell).not.toHaveAttribute('data-frosted-glass')
expect(document.documentElement).not.toHaveAttribute('data-frosted-glass')
})
expect(updates.updateSettings).toHaveBeenLastCalledWith({ transparentFrostedEffectEnabled: false })
fireEvent.click(toggle)
await waitFor(() => expect(shell).toHaveAttribute('data-frosted-glass', 'true'))
unmount()
expect(document.documentElement).not.toHaveAttribute('data-frosted-glass')
const reloaded = render(<App />)
await waitFor(() => expect(reloaded.container.querySelector('.app-shell')).toHaveAttribute('data-frosted-glass', 'true'))
expect(document.documentElement).toHaveAttribute('data-frosted-glass', 'true')
})
it('retains confirmed frosted glass after a failed save and allows rereading locked settings', async () => {
const updates = api.updates!
await updates.updateSettings({ transparentFrostedEffectEnabled: true })
const { container } = render(<App />)
fireEvent.click(await screen.findByRole('button', { name: '设置' }))
fireEvent.click(await screen.findByRole('tab', { name: '外观' }))
const toggle = screen.getByRole('switch', { name: '透明磨砂特效' })
await waitFor(() => {
expect(toggle).toBeEnabled()
expect(toggle).toBeChecked()
})
vi.mocked(updates.updateSettings).mockRejectedValueOnce(new Error('response lost'))
vi.mocked(updates.getSettings).mockRejectedValueOnce(new Error('read unavailable'))
fireEvent.click(toggle)
await screen.findByText('无法确认保存结果,请重新读取后再修改。')
expect(toggle).toBeChecked()
expect(toggle).toBeDisabled()
expect(container.querySelector('.app-shell')).toHaveAttribute('data-frosted-glass', 'true')
expect(document.documentElement).toHaveAttribute('data-frosted-glass', 'true')
fireEvent.click(screen.getByRole('button', { name: '重试' }))
await waitFor(() => expect(toggle).toBeEnabled())
fireEvent.click(toggle)
await waitFor(() => {
expect(toggle).not.toBeChecked()
expect(container.querySelector('.app-shell')).not.toHaveAttribute('data-frosted-glass')
})
})
it("loads token usage in activity and refreshes it when a run finishes", async () => {
vi.mocked(api.usage.getTokenSummary).mockResolvedValueOnce({
totals: {
@@ -13848,6 +13926,7 @@ describe("App", () => {
getSettings: vi.fn(async () => ({
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -13864,6 +13943,7 @@ describe("App", () => {
updateSettings: vi.fn(async () => ({
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -13917,6 +13997,7 @@ describe("App", () => {
getSettings: vi.fn(async () => ({
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -13977,6 +14058,7 @@ describe("App", () => {
getSettings: vi.fn(async () => ({
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14014,6 +14096,7 @@ describe("App", () => {
getSettings: vi.fn(async () => ({
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14030,6 +14113,7 @@ describe("App", () => {
updateSettings: vi.fn(async () => ({
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github" as const,
modelDownloadSource: "modelscope" as const,
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14092,6 +14176,7 @@ describe("App", () => {
let applicationSettings: ApplicationSettings = {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github",
modelDownloadSource: "modelscope",
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -14151,6 +14236,7 @@ describe("App", () => {
let applicationSettings: ApplicationSettings = {
checkUpdatesOnStartup: false,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: "github",
modelDownloadSource: "modelscope",
localToolEnvironment: defaultLocalToolEnvironmentSettings,
+15 -1
View File
@@ -1996,6 +1996,14 @@ function App(): React.JSX.Element {
const [applicationMenuOpen, setApplicationMenuOpen] = useState(false);
const applicationMenuTriggerRef = useRef<HTMLButtonElement>(null);
const [applicationSettings, setApplicationSettings] = useState<ApplicationSettings>();
useLayoutEffect(() => {
// FloatingPortal surfaces live outside app-shell, including in the top layer.
delete document.documentElement.dataset.frostedGlass;
if (applicationSettings?.transparentFrostedEffectEnabled) {
document.documentElement.dataset.frostedGlass = 'true';
}
return () => { delete document.documentElement.dataset.frostedGlass; };
}, [applicationSettings?.transparentFrostedEffectEnabled]);
const [applicationSettingsPending, setApplicationSettingsPending] = useState(false);
const [applicationSettingsUnconfirmed, setApplicationSettingsUnconfirmed] = useState(false);
const applicationSettingsPendingRef = useRef(0);
@@ -8559,7 +8567,7 @@ function App(): React.JSX.Element {
};
return (
<div className="app-shell">
<div className="app-shell" data-frosted-glass={applicationSettings?.transparentFrostedEffectEnabled ? 'true' : undefined}>
<DocumentConversationContext value={{
activeId: activeConversation?.remote ? undefined : activeId,
create: () => new Promise((resolve, reject) => {
@@ -10971,6 +10979,12 @@ function App(): React.JSX.Element {
<Suspense fallback={null}>
<SettingsPanel
appearanceTheme={appearanceTheme}
transparentFrostedEffectEnabled={applicationSettings?.transparentFrostedEffectEnabled ?? false}
applicationSettingsPending={applicationSettingsPending}
applicationSettingsLocked={applicationSettingsUnconfirmed || !applicationSettings}
applicationSettingsError={applicationSettingsError}
onTransparentFrostedEffectEnabledChange={(enabled) => updateApplicationSettings({ transparentFrostedEffectEnabled: enabled })}
onRetryApplicationSettings={() => void reloadApplicationSettings()}
brandingFallbackLogo={
resolvedAppearanceTheme === "dark"
? goodbuddyDarkIcon
+79 -76
View File
@@ -104,6 +104,7 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
const { t } = useTranslation("app");
const headingId = `chat-heading-${conversation.id}`;
const scrollRef = useRef<HTMLElement>(null);
const contextRef = useRef<HTMLDivElement>(null);
const pinnedToBottomRef = useRef(scrollSnapshot?.pinnedToBottom ?? true);
const latestScrollSnapshotRef = useRef(scrollSnapshot);
const restorePendingRef = useRef(true);
@@ -130,6 +131,29 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
);
const hiddenMessageCount = visibleMessageStartIndex;
useLayoutEffect(() => {
const context = contextRef.current;
const scrollContainer = scrollRef.current;
const content = scrollContainer?.querySelector<HTMLElement>('.chat-content');
if (!active || !context || !scrollContainer || !content) return;
const measure = (): void => {
// The fixed context strip also needs space inside the scrolling content.
context.parentElement?.style.setProperty(
'--chat-context-height', `${context.getBoundingClientRect().height}px`,
);
};
measure();
if (typeof ResizeObserver !== 'function') return;
const observer = new ResizeObserver(() => {
measure();
if (restorePendingRef.current || !pinnedToBottomRef.current) return;
scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'auto' });
});
observer.observe(context);
observer.observe(content);
return () => observer.disconnect();
}, [active]);
useEffect(() => {
if (!active || noteMessageNavigation?.conversationId !== conversation.id) return;
const element = messageArticleRefs.current.get(noteMessageNavigation.messageId);
@@ -232,29 +256,6 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
}
}, [active, conversation.messages, scrollSnapshot, visibleMessageCount]);
useLayoutEffect(() => {
if (!active || typeof ResizeObserver !== "function") {
return;
}
const scrollContainer = scrollRef.current;
const messageList =
scrollContainer?.querySelector<HTMLElement>(".message-list");
if (!scrollContainer || !messageList) {
return;
}
const observer = new ResizeObserver(() => {
if (restorePendingRef.current || !pinnedToBottomRef.current) {
return;
}
scrollContainer.scrollTo({
top: scrollContainer.scrollHeight,
behavior: "auto",
});
});
observer.observe(messageList);
return () => observer.disconnect();
}, [active]);
useLayoutEffect(() => {
const previous = prependScrollPositionRef.current;
if (!previous) {
@@ -330,7 +331,7 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
<h1 className="sr-only" id={headingId}>
{t("chat.heading")}
</h1>
{taskStrip}
<div className="chat-history-context" ref={contextRef}>{taskStrip}</div>
<section
aria-labelledby={headingId}
className="chat"
@@ -338,59 +339,61 @@ export const ChatHistoryPane = memo(function ChatHistoryPane({
onScroll={updateScrollPosition}
ref={handleScrollRef}
>
{isUnusedConversation(conversation) && (
<div className="welcome">
<div className="welcome__badge">
<Sparkles size={18} />
<div className="chat-content">
{isUnusedConversation(conversation) && (
<div className="welcome">
<div className="welcome__badge">
<Sparkles size={18} />
</div>
<h2>{t("chat.welcome.title")}</h2>
<div className="quick-actions">
{quickActions.map((action) => (
<button
key={action.title}
onClick={() => onSetInput(action.prompt)}
type="button"
>
<span className="quick-actions__icon">
<FileText size={17} />
</span>
<strong>{action.title}</strong>
<small>{action.description}</small>
</button>
))}
</div>
</div>
<h2>{t("chat.welcome.title")}</h2>
<div className="quick-actions">
{quickActions.map((action) => (
<button
key={action.title}
onClick={() => onSetInput(action.prompt)}
type="button"
>
<span className="quick-actions__icon">
<FileText size={17} />
</span>
<strong>{action.title}</strong>
<small>{action.description}</small>
</button>
))}
</div>
</div>
)}
<ChatTimeline
onOpenImageModelSettings={onOpenImageModelSettings}
onReselectImageSources={onReselectImageSources}
onEditImage={onEditImage}
artifactById={artifactById}
conversationId={conversation.id}
hiddenMessageCount={hiddenMessageCount}
isUnusedConversation={isUnusedConversation(conversation)}
locale={locale}
messages={visibleMessages}
messageStartIndex={visibleMessageStartIndex}
onArticleRef={handleArticleRef}
onCopyMessage={onCopyMessage}
onAddToNote={onAddToNote}
onDownloadImage={onDownloadImage}
onOpenCitationContext={onOpenCitationContext}
onOpenCitationSource={onOpenCitationSource}
onOpenImage={onOpenImage}
onRespondApproval={onRespondApproval}
onRespondQuestion={onRespondQuestion}
onRetry={onRetry}
onRevealEarlier={revealEarlierMessages}
renderAssistantHtml={conversationHtmlRenderingEnabled}
retryContent={
conversation.messages.at(-2)?.role === "user"
? conversation.messages.at(-2)?.content
: undefined
}
totalMessageCount={conversation.messages.length}
/>
)}
<ChatTimeline
onOpenImageModelSettings={onOpenImageModelSettings}
onReselectImageSources={onReselectImageSources}
onEditImage={onEditImage}
artifactById={artifactById}
conversationId={conversation.id}
hiddenMessageCount={hiddenMessageCount}
isUnusedConversation={isUnusedConversation(conversation)}
locale={locale}
messages={visibleMessages}
messageStartIndex={visibleMessageStartIndex}
onArticleRef={handleArticleRef}
onCopyMessage={onCopyMessage}
onAddToNote={onAddToNote}
onDownloadImage={onDownloadImage}
onOpenCitationContext={onOpenCitationContext}
onOpenCitationSource={onOpenCitationSource}
onOpenImage={onOpenImage}
onRespondApproval={onRespondApproval}
onRespondQuestion={onRespondQuestion}
onRetry={onRetry}
onRevealEarlier={revealEarlierMessages}
renderAssistantHtml={conversationHtmlRenderingEnabled}
retryContent={
conversation.messages.at(-2)?.role === "user"
? conversation.messages.at(-2)?.content
: undefined
}
totalMessageCount={conversation.messages.length}
/>
</div>
</section>
{active && showScrollToBottom && (
<button
@@ -74,6 +74,7 @@ const todo: MagicTodoItem = {
const otherTodo: MagicTodoItem = { ...todo, id: '00000000-0000-4000-8000-000000000604', noteId: secondNoteId, noteTitle: second.title, title: '准备演示' }
const settings: ApplicationSettings = {
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
checkUpdatesOnStartup: false, updateSource: 'github', modelDownloadSource: 'modelscope',
localToolEnvironment: defaultLocalToolEnvironmentSettings, applicationNavigation: defaultApplicationNavigation,
localInferenceEnabled: true, conversationHtmlRenderingEnabled: true, remoteProjectsEnabled: false,
+32
View File
@@ -520,6 +520,7 @@ const removeEmbeddingModel = vi.fn(
let applicationSettings: ApplicationSettings = {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -814,6 +815,7 @@ describe('SettingsPanel runtime files', () => {
applicationSettings = {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -1549,6 +1551,36 @@ describe('SettingsPanel runtime files', () => {
).toBeInTheDocument()
})
it.each([
['zh-CN', '外观', '透明磨砂特效', '应用于顶栏和展开的运行待办卡片,保存后立即生效。'],
['en-US', 'Appearance', 'Frosted glass effect', 'Applies to the top bar and expanded runtime checklist. Takes effect as soon as it is saved.']
] as const)('exposes the frosted glass Switch under Appearance in %s', async (locale, category, label, description) => {
await changeUiLocale(locale)
const onChange = vi.fn(async () => true)
const props = {
...heartbeatSettingsProps,
open: true,
onClearLocalData: vi.fn(async () => {}),
onClose: vi.fn(),
onSaved: vi.fn(),
onTransparentFrostedEffectEnabledChange: onChange
}
const { rerender } = render(<SettingsPanel {...props} />)
fireEvent.click(screen.getByRole('tab', { name: category }))
const toggle = screen.getByRole('switch', { name: label })
expect(toggle).not.toBeChecked()
expect(toggle).toHaveAccessibleDescription(description)
expect(toggle.closest('label')).toHaveClass('toggle-row')
fireEvent.click(toggle)
expect(onChange).toHaveBeenCalledExactlyOnceWith(true)
rerender(<SettingsPanel {...props} transparentFrostedEffectEnabled applicationSettingsPending />)
expect(toggle).toBeChecked()
expect(toggle).toBeDisabled()
rerender(<SettingsPanel {...props} transparentFrostedEffectEnabled />)
fireEvent.click(toggle)
expect(onChange).toHaveBeenLastCalledWith(false)
})
it.each([
['zh-CN', '平台功能', '桌面通知'],
['en-US', 'Platform features', 'Desktop notifications']
+38
View File
@@ -235,6 +235,12 @@ type SettingsPanelProps = {
onExpertsChanged?: (experts: AssistantExpert[]) => void
onClearLocalData: () => Promise<void>
appearanceTheme?: AppearanceTheme
transparentFrostedEffectEnabled?: boolean
applicationSettingsPending?: boolean
applicationSettingsLocked?: boolean
applicationSettingsError?: string
onTransparentFrostedEffectEnabledChange?: (enabled: boolean) => Promise<boolean>
onRetryApplicationSettings?: () => void
onAppearanceThemeChange?: (theme: AppearanceTheme) => void
brandingPreferences?: BrandingPreferences
brandingFallbackLogo?: string
@@ -648,6 +654,12 @@ export function SettingsPanel({
onClearLocalData,
onExpertsChanged = () => {},
appearanceTheme = 'system',
transparentFrostedEffectEnabled = false,
applicationSettingsPending = false,
applicationSettingsLocked = false,
applicationSettingsError,
onTransparentFrostedEffectEnabledChange,
onRetryApplicationSettings,
onAppearanceThemeChange = () => {},
brandingPreferences = defaultBrandingPreferences,
brandingFallbackLogo,
@@ -2340,6 +2352,32 @@ export function SettingsPanel({
)}
</div>
</div>
<div className="settings-section">
<label className="toggle-row">
<span>{t('appearance.frostedGlass.label')}</span>
<input
type="checkbox"
role="switch"
checked={transparentFrostedEffectEnabled}
disabled={applicationSettingsPending || applicationSettingsLocked || !onTransparentFrostedEffectEnabledChange}
aria-describedby="frosted-glass-description"
onChange={(event) => {
void onTransparentFrostedEffectEnabledChange?.(event.target.checked)
}}
/>
</label>
<p id="frosted-glass-description" className="settings-panel__description">
{t('appearance.frostedGlass.description')}
</p>
{applicationSettingsError && (
<div role="alert">
<p>{applicationSettingsError}</p>
<button className="secondary-button" disabled={applicationSettingsPending} onClick={onRetryApplicationSettings} type="button">
{t('appearance.frostedGlass.retry')}
</button>
</div>
)}
</div>
<div className="settings-section appearance-settings">
<div className="settings-section__title">
<span className="inline-help-label">
@@ -24,6 +24,7 @@ describe('UpdateSettingsSection', () => {
let applicationSettings: ApplicationSettings = {
checkUpdatesOnStartup: true,
desktopNotificationsEnabled: true,
transparentFrostedEffectEnabled: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
localToolEnvironment: defaultLocalToolEnvironmentSettings,
@@ -105,6 +105,9 @@ describe('WorkspacePrimitives', () => {
expect(toggleRowStyles).toMatch(/align-items:\s*center;/u)
expect(toggleRowStyles).not.toMatch(/border-top:/u)
expect(toggleRowStyles).not.toMatch(/padding-top:/u)
expect(stylesheet).toMatch(
/\.toggle-row > input\[role='switch'\]\s*\{[^}]*order:\s*1;[^}]*margin-inline-start:\s*auto;/u
)
})
it('keeps Platform Features tabs visible in scrollable Settings', () => {
@@ -558,7 +561,7 @@ describe('WorkspacePrimitives', () => {
/\.settings-backdrop \.settings-tabs button strong\s*\{[^}]*font-size:\s*var\(--font-body\);/u
)
expect(stylesheet).toMatch(
/\.settings-backdrop \.settings-tabs button small\s*\{[^}]*font-size:\s*var\(--font-caption\);/u
/\.settings-tabs__group-label\s*\{[^}]*font-size:\s*var\(--font-caption\);/u
)
})
@@ -891,6 +891,11 @@ export const settings = {
}
},
appearance: {
frostedGlass: {
label: 'Frosted glass effect',
description: 'Applies to the top bar and expanded runtime checklist. Takes effect as soon as it is saved.',
retry: 'Retry'
},
theme: {
title: 'Interface theme',
description: 'Applies immediately and is saved on this device',
@@ -895,7 +895,7 @@ export const settingsSections = {
diagnostics: {
label: 'Attach recent desktop diagnostics',
description:
'Off by default. When selected, this adds only timestamps, components, stages, stable error codes, error types, and fixed short messages from recent desktop diagnostics. It does not include conversations, prompts, credentials, file contents, paths, raw provider responses, or remote Agent logs.'
'Off by default. When selected, this adds timestamps, components, stages, error codes, error types, and fixed short messages from recent desktop diagnostics, plus tool connection failure categories, timing, HTTP status, attempt counts, tool kinds, and anonymous correlation IDs. It does not include conversations, prompts, credentials, file contents, paths, raw provider responses, or remote Agent logs.'
},
screenshot: {
title: 'Screenshot (optional)',
@@ -826,6 +826,11 @@ export const settings = {
}
},
appearance: {
frostedGlass: {
label: '透明磨砂特效',
description: '应用于顶栏和展开的运行待办卡片,保存后立即生效。',
retry: '重试'
},
theme: {
title: '界面主题',
description: '选择后立即应用,并保存在此设备',
@@ -840,7 +840,7 @@ export const settingsSections = {
diagnostics: {
label: '附加最近桌面诊断记录',
description:
'默认关闭。选中后仅附加最近桌面诊断的时间、组件、阶段、稳定错误码、错误类型和固定短消息;不包含对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。'
'默认关闭。选中后附加最近桌面诊断的时间、组件、阶段、错误码、错误类型和固定短消息,以及工具连接失败的类别、耗时、HTTP 状态、尝试次数、工具类型和匿名关联标识;不包含对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。'
},
screenshot: {
title: '截图(可选)',
+6
View File
@@ -74,6 +74,12 @@
font-size: var(--font-caption);
}
.runtime-checklist__content ol { margin: 0; padding: var(--space-2); list-style: none; }
:root[data-frosted-glass='true'] .runtime-checklist__content {
background: var(--surface-glass-panel);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
color: var(--text-primary);
}
.runtime-checklist__content li {
display: flex;
align-items: baseline;
+45 -1
View File
@@ -5,6 +5,9 @@
--surface-raised: #fff;
--surface-subtle: #f4f8fb;
--surface-muted: #e5edf3;
--surface-glass: rgb(255 255 255 / 65%);
--surface-glass-panel: rgb(255 255 255 / 75%);
--border-glass: rgb(116 138 159 / 16%);
--text-primary: #17263a;
--text-secondary: #4f6278;
--text-muted: #60748a;
@@ -4591,6 +4594,30 @@ textarea:focus-visible {
-webkit-app-region: no-drag;
}
.app-shell[data-frosted-glass='true'] .topbar {
position: relative;
z-index: 3;
background: var(--surface-glass);
border-bottom-color: var(--border-glass);
backdrop-filter: blur(18px);
}
/* Extend only the active chat; the workbar and other routes keep their grid row. */
.app-shell[data-frosted-glass='true'] .workspace:has(> [data-route='chat']:not([hidden]) .chat-history-pane[data-active='true']) {
margin-top: -58px;
}
.app-shell[data-frosted-glass='true'] .chat-history-context {
position: absolute;
z-index: 2;
top: 58px;
width: 100%;
}
.app-shell[data-frosted-glass='true'] .chat-content {
padding-top: calc(58px + var(--chat-context-height, 0px) + var(--page-gutter));
}
/*
* Overlays are portalled to document.body and cover the whole window, so they
* paint over the .brand and .topbar drag regions. Without no-drag the native
@@ -6216,10 +6243,16 @@ button > svg {
}
.chat-history-pane {
position: relative;
height: 100%;
min-height: 0;
}
.chat-history-context {
flex: 0 0 auto;
min-height: 0;
}
.chat-history-pane[hidden] {
display: none;
}
@@ -6253,6 +6286,14 @@ button > svg {
scrollbar-width: thin;
}
.chat-history-pane > .chat {
padding: 0;
}
.chat-content {
padding: var(--page-gutter) 0 10px;
}
.chat-scroll-to-bottom {
position: absolute;
z-index: 2;
@@ -6383,7 +6424,7 @@ button > svg {
line-height: 1.5;
}
.chat > .welcome,
.chat-content > .welcome,
.message-list {
/* Keep the scrollbar inside the outer gutter without adding trailing scroll padding. */
width: calc(100cqw - 2 * var(--chat-inline-padding));
@@ -17313,6 +17354,9 @@ textarea.field-control {
--surface-raised: #0d1d2c;
--surface-subtle: #12283a;
--surface-muted: #1a3448;
--surface-glass: rgb(7 16 31 / 70%);
--surface-glass-panel: rgb(7 16 31 / 75%);
--border-glass: rgb(143 166 185 / 18%);
--text-primary: #edf7ff;
--text-secondary: #b5c6d5;
--text-muted: #8fa6b9;