diff --git a/.github/pr-assets/sidebar-resource-navigation.png b/.github/pr-assets/sidebar-resource-navigation.png new file mode 100644 index 000000000..ccd5c6e1d Binary files /dev/null and b/.github/pr-assets/sidebar-resource-navigation.png differ diff --git a/.github/pr-assets/sidebar-resource-settings.png b/.github/pr-assets/sidebar-resource-settings.png new file mode 100644 index 000000000..261f3ed98 Binary files /dev/null and b/.github/pr-assets/sidebar-resource-settings.png differ diff --git a/crates/agent-gateway/web/src/app/GatewayApp.tsx b/crates/agent-gateway/web/src/app/GatewayApp.tsx index 687d90af6..096ab2b49 100644 --- a/crates/agent-gateway/web/src/app/GatewayApp.tsx +++ b/crates/agent-gateway/web/src/app/GatewayApp.tsx @@ -1134,8 +1134,7 @@ function useGatewayAppController() { handleSidebarConversationsRemoved, handleSidebarLocalDraftDeleted, handleSidebarNewConversation, - handleSidebarOpenMcpHub, - handleSidebarOpenSkillsHub, + handleSidebarOpenResourceHub, handleSidebarSelectConversation, startNewConversation, } = createGatewayConversationActions({ @@ -2072,8 +2071,7 @@ function useGatewayAppController() { handleSidebarConversationsRemoved: handleSidebarConversationsRemovedWithWorkbench, handleSidebarLocalDraftDeleted: handleSidebarLocalDraftDeletedWithWorkbench, handleSidebarNewConversation, - handleSidebarOpenMcpHub, - handleSidebarOpenSkillsHub, + handleSidebarOpenResourceHub, handleSidebarProjectsCollapsedChange, handleSidebarRecentCollapsedChange, handleSidebarSelectConversation, diff --git a/crates/agent-gateway/web/src/app/GatewayAppView.tsx b/crates/agent-gateway/web/src/app/GatewayAppView.tsx index cf523b9d0..9236dffa1 100644 --- a/crates/agent-gateway/web/src/app/GatewayAppView.tsx +++ b/crates/agent-gateway/web/src/app/GatewayAppView.tsx @@ -245,8 +245,7 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel } handleSidebarConversationsRemoved, handleSidebarLocalDraftDeleted, handleSidebarNewConversation, - handleSidebarOpenMcpHub, - handleSidebarOpenSkillsHub, + handleSidebarOpenResourceHub, handleSidebarProjectsCollapsedChange, handleSidebarRecentCollapsedChange, handleSidebarSelectConversation, @@ -1328,9 +1327,9 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel } onLocalDraftDeleted={handleSidebarLocalDraftDeleted} onConversationsRemoved={handleSidebarConversationsRemoved} onCloseSidebar={() => setSidebarOpen(false)} - onOpenSettings={() => openSettings()} - onOpenSkillsHub={handleSidebarOpenSkillsHub} - onOpenMcpHub={handleSidebarOpenMcpHub} + sidebarShortcuts={settings.customSettings.sidebarShortcuts} + onOpenSettings={openSettings} + onOpenResourceHub={handleSidebarOpenResourceHub} /> {shareConversation ? ( diff --git a/crates/agent-gateway/web/src/app/gatewayConversationActions.ts b/crates/agent-gateway/web/src/app/gatewayConversationActions.ts index d4e314916..e85fa511a 100644 --- a/crates/agent-gateway/web/src/app/gatewayConversationActions.ts +++ b/crates/agent-gateway/web/src/app/gatewayConversationActions.ts @@ -6,6 +6,7 @@ import type { import { createTextComposerDraft } from "@liveagent/ui/lib/chat/composerDraft"; import type { ConversationMentionReference } from "@liveagent/ui/lib/chat/mentionReferences"; import type { PendingUploadedFile } from "@liveagent/ui/lib/chat/uploadedFiles"; +import type { SidebarShortcutId } from "@liveagent/ui/lib/settings/sidebarShortcuts"; import type { ConversationOpenOptions } from "@liveagent/ui/lib/sidebar/openController"; import type { SidebarStore } from "@liveagent/ui/lib/sidebar/store"; import type { Dispatch, MutableRefObject, SetStateAction } from "react"; @@ -208,7 +209,7 @@ export function createGatewayConversationActions(options: CreateGatewayConversat }); } }; - const openHub = (view: "skills-hub" | "mcp-hub") => { + const openHub = (view: Exclude) => { options.setRightDockOpen(false); if (isMobileSidebarLayout()) options.setSidebarOpen(false); options.cacheVisibleComposerDraft(); @@ -284,8 +285,7 @@ export function createGatewayConversationActions(options: CreateGatewayConversat handleSidebarConversationsRemoved, handleSidebarLocalDraftDeleted, handleSidebarNewConversation, - handleSidebarOpenMcpHub: () => openHub("mcp-hub"), - handleSidebarOpenSkillsHub: () => openHub("skills-hub"), + handleSidebarOpenResourceHub: (resource: SidebarShortcutId) => openHub(`${resource}-hub`), handleSidebarSelectConversation, startNewConversation, }; diff --git a/crates/agent-gateway/web/src/app/hooks/useGatewayChatPresentation.tsx b/crates/agent-gateway/web/src/app/hooks/useGatewayChatPresentation.tsx index a11b6be9b..4d7456978 100644 --- a/crates/agent-gateway/web/src/app/hooks/useGatewayChatPresentation.tsx +++ b/crates/agent-gateway/web/src/app/hooks/useGatewayChatPresentation.tsx @@ -145,8 +145,10 @@ export function useGatewayChatPresentation({ if (!token.trim()) return DEFAULT_BROWSER_TITLE; if (activeView === "skills-hub") return SKILLS_HUB_BROWSER_TITLE; if (activeView === "mcp-hub") return MCP_HUB_BROWSER_TITLE; + if (activeView === "memory-hub") return translate("settings.navMemory", settings.locale); + if (activeView === "cron-hub") return translate("settings.navCron", settings.locale); return displayedConversationTitle || DEFAULT_BROWSER_TITLE; - }, [activeView, displayedConversationTitle, historyShareToken, token]); + }, [activeView, displayedConversationTitle, historyShareToken, token, settings.locale]); useEffect(() => { if (typeof document !== "undefined") document.title = browserTitle; }, [browserTitle]); diff --git a/crates/agent-gateway/web/src/app/hooks/usePendingUploads.ts b/crates/agent-gateway/web/src/app/hooks/usePendingUploads.ts index 8bc2fedd1..d4f3b06e0 100644 --- a/crates/agent-gateway/web/src/app/hooks/usePendingUploads.ts +++ b/crates/agent-gateway/web/src/app/hooks/usePendingUploads.ts @@ -1,3 +1,4 @@ +import type { ApplicationViewId } from "@liveagent/ui/application/ApplicationView"; import type { MentionComposerHandle } from "@liveagent/ui/components/chat/MentionComposer"; import type { NotifyItem } from "@liveagent/ui/components/chat/NotifyToast"; import { t as translate } from "@liveagent/ui/i18n/index"; @@ -38,7 +39,7 @@ type UsePendingUploadsParams = { historyShareToken: string | null; settingsSyncReady: boolean; settingsOpen: boolean; - activeView: "chat" | "skills-hub" | "mcp-hub"; + activeView: ApplicationViewId; locale: AppSettings["locale"]; executionMode: AppSettings["system"]["executionMode"]; conversationId: string; diff --git a/crates/agent-gateway/web/src/pages/settings/types.ts b/crates/agent-gateway/web/src/pages/settings/types.ts index eb0dba68f..d8bedffa6 100644 --- a/crates/agent-gateway/web/src/pages/settings/types.ts +++ b/crates/agent-gateway/web/src/pages/settings/types.ts @@ -6,6 +6,8 @@ export type SetSettingsFn = (updater: (prev: AppSettings) => AppSettings) => voi export type SectionId = | "system" + | "skills" + | "mcp" | "systemTools" | "stt" | "providers" diff --git a/crates/agent-gateway/web/test/resource-hub-navigation.test.mjs b/crates/agent-gateway/web/test/resource-hub-navigation.test.mjs new file mode 100644 index 000000000..381cbc48d --- /dev/null +++ b/crates/agent-gateway/web/test/resource-hub-navigation.test.mjs @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import { fileURLToPath } from "node:url"; +import test from "node:test"; +import { createWebModuleLoader } from "../../test/helpers/load-web-module.mjs"; + +const loader = createWebModuleLoader({ + rootDir: fileURLToPath(new URL("../", import.meta.url)), +}); +const { createGatewayConversationActions } = loader.loadModule("src/app/gatewayConversationActions.ts"); + +for (const resource of ["skills", "mcp", "memory", "cron"]) { + test(`${resource} opens in the content area while preserving the desktop sidebar and composer draft`, () => { + const calls = []; + const actions = createGatewayConversationActions({ + setRightDockOpen: (open) => calls.push(["rightDock", open]), + setSidebarOpen: () => assert.fail("Desktop sidebar must stay open"), + cacheVisibleComposerDraft: () => calls.push(["cacheDraft"]), + setActiveView: (view) => calls.push(["view", view]), + setSettingsOpen: () => assert.fail("Resource navigation must not open settings"), + clearCachedComposerDraft: () => assert.fail("Draft must be retained"), + }); + actions.handleSidebarOpenResourceHub(resource); + assert.deepEqual(calls, [["rightDock", false], ["cacheDraft"], ["view", `${resource}-hub`]]); + }); +} diff --git a/crates/agent-gui/src/lib/settings/storage.ts b/crates/agent-gui/src/lib/settings/storage.ts index d0825910f..8ba3c31e5 100644 --- a/crates/agent-gui/src/lib/settings/storage.ts +++ b/crates/agent-gui/src/lib/settings/storage.ts @@ -1,3 +1,4 @@ +import { normalizeSidebarShortcuts } from "@liveagent/ui/lib/settings/sidebarShortcuts"; import { buildGatewaySettingsSyncPayload, buildGatewaySettingsSyncUpdatePayload, @@ -126,6 +127,7 @@ function readLocalUiSettings(): { projectsCollapsed: chatSidebar.projectsCollapsed === true, recentCollapsed: chatSidebar.recentCollapsed === true, }, + sidebarShortcuts: normalizeSidebarShortcuts(obj.sidebarShortcuts), chatTranscript: normalizeChatTranscriptSettings(obj.chatTranscript), rightDock: normalizeRightDockSettings(obj.rightDock), // 三档枚举(与 normalizeCustomSettings 同口径):脏值/缺省落回统计状态栏。 diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index 0894fa0bc..b555e650c 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -3978,17 +3978,13 @@ export function ChatPage(props: ChatPageProps) { onShareConversation={handleOpenShareModal} onOpenSharedConversations={handleOpenSharedHistoryManager} onCloseSidebar={handleCloseSidebar} - onOpenSettings={() => onOpenSettings()} + sidebarShortcuts={settings.customSettings.sidebarShortcuts} + onOpenSettings={onOpenSettings} appUpdate={appUpdate} - onOpenSkillsHub={() => { - cacheActiveComposerDraft(); - setRightDockOpen(false); - setActiveView("skills-hub"); - }} - onOpenMcpHub={() => { + onOpenResourceHub={(resource) => { cacheActiveComposerDraft(); setRightDockOpen(false); - setActiveView("mcp-hub"); + setActiveView(`${resource}-hub`); }} /> diff --git a/crates/agent-gui/src/pages/chat/composer/useComposerDraftCache.ts b/crates/agent-gui/src/pages/chat/composer/useComposerDraftCache.ts index 33d4c6256..1b83f7ff1 100644 --- a/crates/agent-gui/src/pages/chat/composer/useComposerDraftCache.ts +++ b/crates/agent-gui/src/pages/chat/composer/useComposerDraftCache.ts @@ -1,3 +1,4 @@ +import type { ApplicationViewId } from "@liveagent/ui/application/ApplicationView"; import type { MentionComposerHandle } from "@liveagent/ui/components/chat/MentionComposer"; import { type MutableRefObject, useCallback, useEffect, useRef } from "react"; import type { ConversationDraftStore } from "../conversations/conversationDraftStore"; @@ -5,7 +6,7 @@ import type { ConversationDraftStore } from "../conversations/conversationDraftS type UseComposerDraftCacheParams = { composerRef: MutableRefObject; currentConversationIdRef: MutableRefObject; - activeView: "chat" | "skills-hub" | "mcp-hub"; + activeView: ApplicationViewId; currentConversationId: string; draftStore: ConversationDraftStore; }; diff --git a/crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts b/crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts index 2c1d9c161..fda26c9a5 100644 --- a/crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts +++ b/crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts @@ -1,3 +1,4 @@ +import type { ApplicationViewId } from "@liveagent/ui/application/ApplicationView"; import { createUuid } from "@liveagent/ui/lib/shared/id"; import { sidebarScopeKey } from "@liveagent/ui/lib/sidebar/scope"; import type { SidebarStore } from "@liveagent/ui/lib/sidebar/store"; @@ -45,7 +46,7 @@ type UseWorkspaceProjectsParams = { workdir: string; t: (key: string) => string; setErrorMessage: Dispatch>; - setActiveView: Dispatch>; + setActiveView: Dispatch>; setRightDockOpen: Dispatch>; startNewConversationActionRef: MutableRefObject<(options?: { workdir?: string }) => string>; prepareComposerForConversationChangeActionRef: MutableRefObject<() => void>; diff --git a/crates/agent-gui/src/pages/settings/types.ts b/crates/agent-gui/src/pages/settings/types.ts index c969bf54f..92a79c6c8 100644 --- a/crates/agent-gui/src/pages/settings/types.ts +++ b/crates/agent-gui/src/pages/settings/types.ts @@ -8,6 +8,8 @@ export type SetSettingsFn = (updater: (prev: AppSettings) => AppSettings) => voi export type SectionId = | "system" | "shortcuts" + | "skills" + | "mcp" | "systemTools" | "stt" | "providers" diff --git a/crates/agent-gui/test/chat/search-sidebar-reveal.test.mjs b/crates/agent-gui/test/chat/search-sidebar-reveal.test.mjs index 342614385..c07679de6 100644 --- a/crates/agent-gui/test/chat/search-sidebar-reveal.test.mjs +++ b/crates/agent-gui/test/chat/search-sidebar-reveal.test.mjs @@ -14,10 +14,12 @@ const virtualizer = { const icons = [ "AlertCircle", "Blend", + "Brain", "Cable", "Check", "ChevronRight", "CirclePlus", + "Clock3", "Folder", "FolderClosed", "FolderOpen", diff --git a/crates/agent-gui/test/settings/normalization.test.mjs b/crates/agent-gui/test/settings/normalization.test.mjs index d8b14e8d2..3a7ff567d 100644 --- a/crates/agent-gui/test/settings/normalization.test.mjs +++ b/crates/agent-gui/test/settings/normalization.test.mjs @@ -3542,3 +3542,63 @@ test("workspace resource overflow uses locale-independent Unicode code-point ord assert.ok(normalized["/repo/a"]); assert.equal(normalized["/repo/ä"], undefined); }); + + +test("sidebar shortcut migration tolerates partial and malformed preferences", () => { + for (const input of [null, [], "hidden", {}]) { + assert.deepEqual(settings.normalizeCustomSettings({ sidebarShortcuts: input }, []).sidebarShortcuts, + { skills: true, mcp: true, cron: true, memory: true }); + } + assert.deepEqual(settings.normalizeCustomSettings({ sidebarShortcuts: { + skills: false, mcp: "false", cron: null, memory: true, + } }, []).sidebarShortcuts, { skills: false, mcp: true, cron: true, memory: true }); +}); + +test("desktop snapshots synchronize sidebar shortcuts to WebUI", () => { + const desktop = settings.normalizeSettings({ customSettings: { + sidebarShortcuts: { skills: false, mcp: true, cron: false, memory: true }, + } }); + const web = settings.normalizeSettings({ customSettings: { + sidebarShortcuts: { skills: true, mcp: false, cron: true, memory: false }, + } }); + const snapshot = sync.buildGatewaySettingsSyncPayload(desktop); + assert.deepEqual(snapshot.customSettings.sidebarShortcuts, desktop.customSettings.sidebarShortcuts); + const hydrated = sync.applyGatewaySettingsSyncPayload(web, snapshot); + assert.deepEqual(hydrated.customSettings.sidebarShortcuts, desktop.customSettings.sidebarShortcuts); + assert.equal(sync.buildGatewaySettingsSyncUpdatePayload(desktop, hydrated).customSettings, undefined); +}); + +for (const resource of ["skills", "mcp", "cron", "memory"]) { + test(`WebUI ${resource} shortcut edits synchronize in both directions without changing resources`, () => { + let desktop = settings.normalizeSettings({}); + let web = sync.applyGatewaySettingsSyncPayload( + settings.normalizeSettings({}), sync.buildGatewaySettingsSyncPayload(desktop), + ); + for (const enabled of [false, true]) { + const edited = settings.updateCustomSettings(web, { + sidebarShortcuts: { ...web.customSettings.sidebarShortcuts, [resource]: enabled }, + }); + const update = sync.buildGatewaySettingsSyncUpdatePayload(web, edited); + assert.deepEqual(Object.keys(update), ["customSettings"]); + assert.equal(update.customSettings.sidebarShortcuts[resource], enabled); + const received = sync.applyGatewaySettingsSyncPayload(desktop, update); + assert.deepEqual(received.customSettings.sidebarShortcuts, edited.customSettings.sidebarShortcuts); + for (const key of ["skills", "mcp", "memory"]) { + assert.deepEqual(received[key], desktop[key]); + } + desktop = received; + web = sync.applyGatewaySettingsSyncPayload(edited, sync.buildGatewaySettingsSyncPayload(desktop)); + assert.deepEqual(sync.buildGatewaySettingsSyncUpdatePayload(edited, web), {}); + } + }); +} + +test("legacy gateway messages without sidebar shortcuts preserve the current preference", () => { + const current = settings.normalizeSettings({ customSettings: { + sidebarShortcuts: { skills: false, mcp: false, cron: false, memory: false }, + } }); + for (const incoming of [{}, { customSettings: {} }, { customSettings: { promptClarifyEnabled: false } }]) { + const applied = sync.applyGatewaySettingsSyncPayload(current, incoming); + assert.deepEqual(applied.customSettings.sidebarShortcuts, current.customSettings.sidebarShortcuts); + } +}); diff --git a/crates/agent-gui/test/settings/storage.test.mjs b/crates/agent-gui/test/settings/storage.test.mjs index f8197ebfe..bb465cdcf 100644 --- a/crates/agent-gui/test/settings/storage.test.mjs +++ b/crates/agent-gui/test/settings/storage.test.mjs @@ -128,3 +128,32 @@ test("a missing retryErrorSettings field falls back to all presets (legacy snaps }); }); }); + + +test("sidebar shortcuts survive a local save and reload without changing resource activation", async () => { + await withGlobal("localStorage", createMemoryLocalStorage(), async () => { + const commands = []; + const loader = createTsModuleLoader({ + mocks: { "@tauri-apps/api/core": { invoke: async (command) => { + commands.push(command); + return {}; + } } }, + }); + const storage = loader.loadModule("src/lib/settings/storage.ts"); + const settings = loader.loadModule("src/lib/settings/index.ts"); + const initial = await storage.loadPersistedSettings(); + assert.deepEqual(initial.customSettings.sidebarShortcuts, { + skills: true, mcp: true, cron: true, memory: true, + }); + const hidden = { skills: false, mcp: true, cron: false, memory: false }; + const next = settings.updateCustomSettings(initial, { sidebarShortcuts: hidden }); + commands.length = 0; + await storage.persistSettings(initial, next); + assert.deepEqual(commands, []); + const loaded = await storage.loadPersistedSettings(); + assert.deepEqual(loaded.customSettings.sidebarShortcuts, hidden); + for (const key of ["skills", "mcp", "memory"]) { + assert.deepEqual(loaded[key], initial[key]); + } + }); +}); diff --git a/crates/agent-ui/src/application/ApplicationView.tsx b/crates/agent-ui/src/application/ApplicationView.tsx index 880158596..0690b20cb 100644 --- a/crates/agent-ui/src/application/ApplicationView.tsx +++ b/crates/agent-ui/src/application/ApplicationView.tsx @@ -1,11 +1,13 @@ import type { AppSettings } from "@liveagent/app/lib/settings"; import type { CSSProperties, HTMLAttributes, ReactNode } from "react"; +import type { SidebarShortcutId } from "../lib/settings/sidebarShortcuts"; import { cn } from "../lib/shared/utils"; import type { SkillSummary } from "../lib/skills/index"; import { McpHubPage } from "../pages/mcp-hub/McpHubPage"; +import { ResourceManagementPage } from "../pages/resources/ResourceManagementPage"; import { SkillsHubPage } from "../pages/skills-hub/SkillsHubPage"; -export type ApplicationViewId = "chat" | "skills-hub" | "mcp-hub"; +export type ApplicationViewId = "chat" | `${SidebarShortcutId}-hub`; type ApplicationChatViewProps = { containerProps?: Omit, "children">; @@ -56,6 +58,14 @@ export function ApplicationView(props: ApplicationViewProps) { content = ( ); + } else if (activeView === "memory-hub" || activeView === "cron-hub") { + content = ( + + ); } else { const { containerProps, content: chatContent } = chat; content = ( diff --git a/crates/agent-ui/src/components/chat/ChatHistorySidebar.tsx b/crates/agent-ui/src/components/chat/ChatHistorySidebar.tsx index 12f0380b3..9b3a69ab4 100644 --- a/crates/agent-ui/src/components/chat/ChatHistorySidebar.tsx +++ b/crates/agent-ui/src/components/chat/ChatHistorySidebar.tsx @@ -2,10 +2,12 @@ import { type WorkspaceProject, workspaceProjectPathKey } from "@liveagent/app/l import { AlertCircle, Blend, + Brain, Cable, Check, ChevronRight, CirclePlus, + Clock3, Folder, FolderClosed, FolderOpen, @@ -50,6 +52,7 @@ import { useRef, useState, } from "react"; +import { normalizeSidebarShortcuts, SIDEBAR_SHORTCUTS } from "../../lib/settings/sidebarShortcuts"; import type { ConversationOpenOptions } from "../../lib/sidebar/openController"; import type { SidebarConversation } from "../../lib/sidebar/types"; import { @@ -235,9 +238,9 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi onDeleteConversations, onLoadMore, onCloseSidebar, + sidebarShortcuts = normalizeSidebarShortcuts(undefined), onOpenSettings, - onOpenSkillsHub, - onOpenMcpHub, + onOpenResourceHub, headerTop, brand, hideCloseButton = false, @@ -1377,46 +1380,30 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi {t("chat.searchConversations")} - - + {SIDEBAR_SHORTCUTS.filter(({ id }) => sidebarShortcuts[id]).map(({ id, labelKey }) => { + const Icon = { skills: Blend, mcp: Cable, cron: Clock3, memory: Brain }[id]; + const active = activeView === `${id}-hub`; + return ( + + ); + })} @@ -2157,7 +2144,7 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi // 不必靠文案或坐标去猜。可读名走 i18n——屏幕阅读器念给用户听的 // 东西不该为了脚本方便固定成英文。 data-testid="open-settings" - onClick={onOpenSettings} + onClick={() => onOpenSettings()} className="h-8 w-full justify-start gap-2.5 rounded-lg px-2.5 text-[calc(13px*var(--zone-font-scale,1))] font-normal text-foreground/85 shadow-none hover:bg-foreground/[0.08] hover:text-foreground" title={t("tooltip.settings")} > diff --git a/crates/agent-ui/src/components/chat/ChatHistorySidebarTypes.ts b/crates/agent-ui/src/components/chat/ChatHistorySidebarTypes.ts index f2b8721f3..63469c7f8 100644 --- a/crates/agent-ui/src/components/chat/ChatHistorySidebarTypes.ts +++ b/crates/agent-ui/src/components/chat/ChatHistorySidebarTypes.ts @@ -1,9 +1,11 @@ import type { WorkspaceProject } from "@liveagent/app/lib/settings"; +import type { ApplicationViewId } from "@liveagent/ui/application/ApplicationView"; import type { SidebarBatchDeleteOptions, SidebarBatchDeleteResult, } from "@liveagent/ui/lib/sidebar/batchDelete"; import type { ReactNode } from "react"; +import type { SidebarShortcutId, SidebarShortcuts } from "../../lib/settings/sidebarShortcuts"; import type { ConversationOpenOptions } from "../../lib/sidebar/openController"; import type { SidebarConversation } from "../../lib/sidebar/types"; import type { WorkspaceProjectGroup } from "../../lib/workspaceProjectTypes"; @@ -57,7 +59,7 @@ export type ChatHistorySidebarProps = { fontScale?: number; /** Incremented by the desktop host when its configured search shortcut fires. */ conversationSearchRequestKey?: number; - activeView?: "chat" | "skills-hub" | "mcp-hub"; + activeView?: ApplicationViewId; showProjects?: boolean; // Pre-sorted by the container (pinned/running/activity); rendered as-is. projects?: WorkspaceProject[]; @@ -140,9 +142,9 @@ export type ChatHistorySidebarProps = { ) => Promise; onLoadMore: () => void; onCloseSidebar: () => void; + sidebarShortcuts?: SidebarShortcuts; onOpenSettings: () => void; - onOpenSkillsHub?: () => void; - onOpenMcpHub?: () => void; + onOpenResourceHub: (resource: SidebarShortcutId) => void; headerTop?: ReactNode; brand?: ReactNode; hideCloseButton?: boolean; @@ -211,9 +213,10 @@ export type ChatHistorySidebarContainerSource = Required< | "onShareConversation" | "onOpenSharedConversations" | "onCloseSidebar" + | "sidebarShortcuts" | "onOpenSettings" > & - Required> & { + Required> & { projects: WorkspaceProject[]; }; @@ -226,9 +229,9 @@ type ChatHistorySidebarConversationSource = Pick< | "onShareConversation" | "onOpenSharedConversations" | "onCloseSidebar" + | "sidebarShortcuts" | "onOpenSettings" - | "onOpenSkillsHub" - | "onOpenMcpHub" + | "onOpenResourceHub" >; type ChatHistorySidebarConversationHandlers = Pick< @@ -310,8 +313,8 @@ export function buildChatHistorySidebarConversationProps( onOpenSharedConversations: source.onOpenSharedConversations, onCloseSidebar: source.onCloseSidebar, onOpenSettings: source.onOpenSettings, - onOpenSkillsHub: source.onOpenSkillsHub, - onOpenMcpHub: source.onOpenMcpHub, + sidebarShortcuts: source.sidebarShortcuts, + onOpenResourceHub: source.onOpenResourceHub, }; } diff --git a/crates/agent-ui/src/components/hub/HubChrome.tsx b/crates/agent-ui/src/components/hub/HubChrome.tsx index 3580ff6d0..68421b17f 100644 --- a/crates/agent-ui/src/components/hub/HubChrome.tsx +++ b/crates/agent-ui/src/components/hub/HubChrome.tsx @@ -1,4 +1,3 @@ -import { HubTitleBar } from "@liveagent/adapters/hubChrome"; import type { ReactNode } from "react"; import { cn } from "../../lib/shared/utils"; @@ -32,6 +31,7 @@ export function HubBackdrop(props: { tone?: "amber" | "violet" | "neutral" }) { // 侧栏开关不在这里渲染:AppWorkbenchChrome(ChatHeader)常驻于所有视图之上, // 侧栏收起时已经提供了同一个按钮。Hub 自己再画一个就会在窄屏上叠出两枚 // PanelLeft(#501 之前 Hub 页面没有顶栏,才需要自带一枚)。 +// 顶栏也已预留 macOS 窗口控件空间,Hub 内容无需再次添加标题栏占位。 export function HubHeader(props: { icon?: ReactNode; title: string; @@ -39,29 +39,30 @@ export function HubHeader(props: { tone?: "amber" | "violet" | "neutral"; actions?: ReactNode; prominent?: boolean; + embedded?: boolean; }) { - const { icon, title, subtitle, actions, prominent = false } = props; + const { icon, title, subtitle, actions, prominent = false, embedded = false } = props; return ( - <> - +
-
- {icon ? ( -
- {icon} -
- ) : null} -
+ {icon ? ( +
+ {icon} +
+ ) : null} +
+ {!embedded ? (

{title}

- {subtitle ? ( -

- {subtitle} -

- ) : null} -
- {actions ?
{actions}
: null} + ) : null} + {subtitle ? ( +

+ {subtitle} +

+ ) : null}
+ {actions ?
{actions}
: null}
- +
); } diff --git a/crates/agent-ui/src/i18n/translations/enUSSettings.ts b/crates/agent-ui/src/i18n/translations/enUSSettings.ts index 62a54c508..3aaec7d1b 100644 --- a/crates/agent-ui/src/i18n/translations/enUSSettings.ts +++ b/crates/agent-ui/src/i18n/translations/enUSSettings.ts @@ -1,4 +1,8 @@ export const EN_US_SETTINGS_TRANSLATIONS = { + "settings.navMcp": "MCP", + "settings.groupResources": "Resources & automation", + "settings.sidebarShortcuts": "Sidebar shortcuts", + "settings.showSidebarShortcut": "Show {name} in sidebar", "settings.navSystem": "System", "settings.navSystemTools": "System Tools", "settings.navStt": "Speech to Text", @@ -10,7 +14,7 @@ export const EN_US_SETTINGS_TRANSLATIONS = { "settings.navHooks": "Hooks", "settings.navAgents": "Prompt Templates", "settings.navSsh": "SSH", - "settings.navCron": "Cron", + "settings.navCron": "Scheduled tasks", "settings.navRemote": "Remote", "settings.navSkills": "Skills", "settings.navMemory": "Memory", diff --git a/crates/agent-ui/src/i18n/translations/zhCNSettings.ts b/crates/agent-ui/src/i18n/translations/zhCNSettings.ts index ab9e98876..3456a9346 100644 --- a/crates/agent-ui/src/i18n/translations/zhCNSettings.ts +++ b/crates/agent-ui/src/i18n/translations/zhCNSettings.ts @@ -1,4 +1,8 @@ export const ZH_CN_SETTINGS_TRANSLATIONS = { + "settings.navMcp": "MCP", + "settings.groupResources": "资源与自动化", + "settings.sidebarShortcuts": "侧栏快捷入口", + "settings.showSidebarShortcut": "在侧栏显示 {name}", "settings.navSystem": "系统设置", "settings.navSystemTools": "系统工具", "settings.navStt": "语音输入", @@ -10,7 +14,7 @@ export const ZH_CN_SETTINGS_TRANSLATIONS = { "settings.navHooks": "Hooks", "settings.navAgents": "提示词模板", "settings.navSsh": "SSH", - "settings.navCron": "Cron", + "settings.navCron": "定时任务", "settings.navRemote": "Remote", "settings.navSkills": "Skills", "settings.navMemory": "记忆", diff --git a/crates/agent-ui/src/lib/settings/index.ts b/crates/agent-ui/src/lib/settings/index.ts index 61ff9d485..ba0080a29 100644 --- a/crates/agent-ui/src/lib/settings/index.ts +++ b/crates/agent-ui/src/lib/settings/index.ts @@ -52,6 +52,7 @@ import { normalizeRightDockSettings, RIGHT_DOCK_SINGLETON_TAB_IDS, } from "./rightDockNormalization"; +import { normalizeSidebarShortcuts } from "./sidebarShortcuts"; import { computeNextMemoryOrganizerRunAt, normalizeMemoryOrganizerMode, @@ -1620,6 +1621,7 @@ export function normalizeCustomSettings( projectsCollapsed: chatSidebar.projectsCollapsed === true, recentCollapsed: chatSidebar.recentCollapsed === true, }, + sidebarShortcuts: normalizeSidebarShortcuts(obj.sidebarShortcuts), chatTranscript: normalizeChatTranscriptSettings(obj.chatTranscript), rightDock: normalizeRightDockSettings(obj.rightDock), // 三档枚举:历史配置无此字段或值不合法(含曾设想过的 "auto")一律落回默认的统计状态栏。 diff --git a/crates/agent-ui/src/lib/settings/sidebarShortcuts.ts b/crates/agent-ui/src/lib/settings/sidebarShortcuts.ts new file mode 100644 index 000000000..590b7e634 --- /dev/null +++ b/crates/agent-ui/src/lib/settings/sidebarShortcuts.ts @@ -0,0 +1,20 @@ +/** Navigation visibility only; these switches never disable the underlying resources. */ +export const SIDEBAR_SHORTCUTS = [ + { id: "skills", labelKey: "settings.navSkills" }, + { id: "mcp", labelKey: "settings.navMcp" }, + { id: "cron", labelKey: "settings.navCron" }, + { id: "memory", labelKey: "settings.navMemory" }, +] as const; + +export type SidebarShortcutId = (typeof SIDEBAR_SHORTCUTS)[number]["id"]; +export type SidebarShortcuts = Record; + +export function normalizeSidebarShortcuts(input: unknown): SidebarShortcuts { + const value = (input && typeof input === "object" ? input : {}) as Record; + return { + skills: value.skills !== false, + mcp: value.mcp !== false, + cron: value.cron !== false, + memory: value.memory !== false, + }; +} diff --git a/crates/agent-ui/src/lib/settings/sync.ts b/crates/agent-ui/src/lib/settings/sync.ts index 39507bbb3..4de1ffef8 100644 --- a/crates/agent-ui/src/lib/settings/sync.ts +++ b/crates/agent-ui/src/lib/settings/sync.ts @@ -1328,6 +1328,9 @@ export function applyGatewaySettingsSyncPayload( ) : current.customSettings.rightDock, chatSidebar: current.customSettings.chatSidebar, + // 入口可见性跨端同步;旧端未携带此字段时保留当前状态。 + sidebarShortcuts: + incomingCustomSettings.sidebarShortcuts ?? current.customSettings.sidebarShortcuts, // 展示样式是全局偏好,随同步走;老对端的 payload 没有该字段时保留本地值, // 不得被重置回默认。 composerContextDisplay: diff --git a/crates/agent-ui/src/lib/settings/types.ts b/crates/agent-ui/src/lib/settings/types.ts index 458c1188c..25332decf 100644 --- a/crates/agent-ui/src/lib/settings/types.ts +++ b/crates/agent-ui/src/lib/settings/types.ts @@ -2,6 +2,8 @@ import type { Locale } from "@liveagent/app/i18n/config"; import type { ThinkingLevel } from "@liveagent/ui/lib/models/modelThinking"; import type { WorkspaceProjectGroup } from "@liveagent/ui/lib/workspaceProjectTypes"; +import type { SidebarShortcuts } from "./sidebarShortcuts"; + export type ProviderId = "codex" | "claude_code" | "gemini" | "xai" | "deepseek"; export type ExecutionMode = "text" | "tools" | "agent-dev"; @@ -162,6 +164,7 @@ export type ChatTranscriptSettings = { export type ComposerContextDisplayMode = "statsBar" | "both" | "ring"; export type CustomSettings = { + sidebarShortcuts: SidebarShortcuts; conversationTitleModel?: SelectedModel; // AI commit-message generation in the Git review dock. Unset means "follow // the current conversation model"; a stored selection whose provider/model diff --git a/crates/agent-ui/src/pages/mcp-hub/McpHubPage.tsx b/crates/agent-ui/src/pages/mcp-hub/McpHubPage.tsx index dff931683..957abb39e 100644 --- a/crates/agent-ui/src/pages/mcp-hub/McpHubPage.tsx +++ b/crates/agent-ui/src/pages/mcp-hub/McpHubPage.tsx @@ -17,6 +17,7 @@ type McpHubPageProps = { settings: AppSettings; setSettings: (updater: (prev: AppSettings) => AppSettings) => void; isAgentMode: boolean; + embedded?: boolean; }; type McpHubView = "installed" | "store" | "import"; @@ -81,6 +82,7 @@ export function McpHubPage(props: McpHubPageProps) {
-
+
+ +
+
+ {memory ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/crates/agent-ui/src/pages/settings/ResourceHubSection.tsx b/crates/agent-ui/src/pages/settings/ResourceHubSection.tsx new file mode 100644 index 000000000..f7c347d69 --- /dev/null +++ b/crates/agent-ui/src/pages/settings/ResourceHubSection.tsx @@ -0,0 +1,19 @@ +import type { SettingsSectionProps } from "@liveagent/app/pages/settings/types"; +import { McpHubPage } from "../mcp-hub/McpHubPage"; +import { SkillsHubPage } from "../skills-hub/SkillsHubPage"; + +export function ResourceHubSection({ + resource, + settings, + setSettings, +}: SettingsSectionProps & { resource: "skills" | "mcp" }) { + const Hub = resource === "skills" ? SkillsHubPage : McpHubPage; + return ( + + ); +} diff --git a/crates/agent-ui/src/pages/settings/SettingsPage.tsx b/crates/agent-ui/src/pages/settings/SettingsPage.tsx index 20699dca5..99a03dbf7 100644 --- a/crates/agent-ui/src/pages/settings/SettingsPage.tsx +++ b/crates/agent-ui/src/pages/settings/SettingsPage.tsx @@ -2,8 +2,10 @@ import { createSettingsExtension } from "@liveagent/adapters/settingsExtension"; import type { SttProviderId } from "@liveagent/app/lib/settings"; import type { SettingsPageProps } from "@liveagent/app/pages/settings/types"; import { + Blend, BookOpen, Brain, + Cable, Clock3, Cloud, Cpu, @@ -23,6 +25,7 @@ import { HooksSection } from "./HooksSection"; import { MemoryPanel } from "./memory/MemoryPanel"; import { ProvidersSection } from "./ProvidersSection"; import { RemoteSection } from "./RemoteSection"; +import { ResourceHubSection } from "./ResourceHubSection"; import { SettingsShell } from "./SettingsShell"; import { SshSection } from "./SshSection"; import { SttSection } from "./SttSection"; @@ -111,11 +114,28 @@ export function SettingsPage(props: SettingsPageProps) { icon: , render: () => , }, + ...(["skills", "mcp"] as const).map((id, index) => ({ + id, + groupKey: "settings.groupResources", + groupOrder: 20, + order: (index + 1) * 10, + labelKey: id === "skills" ? "settings.navSkills" : "settings.navMcp", + icon: + id === "skills" ? ( + + ) : ( + + ), + contentMode: "fill" as const, + render: () => ( + + ), + })), { id: "memory", - groupKey: "settings.groupIntelligence", + groupKey: "settings.groupResources", groupOrder: 20, - order: 10, + order: 40, labelKey: "settings.navMemory", icon: , contentMode: "fill", @@ -130,7 +150,7 @@ export function SettingsPage(props: SettingsPageProps) { { id: "systemTools", groupKey: "settings.groupIntelligence", - groupOrder: 20, + groupOrder: 25, order: 20, labelKey: "settings.navSystemTools", icon: , @@ -142,7 +162,7 @@ export function SettingsPage(props: SettingsPageProps) { // 在桌面主机那台机器上完成的动作在 web 面收起——判定交给组件的 surface。 id: "cua", groupKey: "settings.groupIntelligence", - groupOrder: 20, + groupOrder: 25, order: 25, labelKey: "settings.navCua", icon: , @@ -157,7 +177,7 @@ export function SettingsPage(props: SettingsPageProps) { { id: "stt", groupKey: "settings.groupIntelligence", - groupOrder: 20, + groupOrder: 25, order: 30, labelKey: "settings.navStt", icon: , @@ -173,9 +193,9 @@ export function SettingsPage(props: SettingsPageProps) { }, { id: "hooks", - groupKey: "settings.groupAutomation", - groupOrder: 30, - order: 10, + groupKey: "settings.groupResources", + groupOrder: 20, + order: 50, labelKey: "settings.navHooks", icon: , contentMode: "fill", @@ -183,9 +203,9 @@ export function SettingsPage(props: SettingsPageProps) { }, { id: "cron", - groupKey: "settings.groupAutomation", - groupOrder: 30, - order: 20, + groupKey: "settings.groupResources", + groupOrder: 20, + order: 30, labelKey: "settings.navCron", icon: , render: () => , diff --git a/crates/agent-ui/src/pages/settings/SidebarShortcutsSection.tsx b/crates/agent-ui/src/pages/settings/SidebarShortcutsSection.tsx new file mode 100644 index 000000000..fbf56b640 --- /dev/null +++ b/crates/agent-ui/src/pages/settings/SidebarShortcutsSection.tsx @@ -0,0 +1,37 @@ +import { updateCustomSettings } from "@liveagent/app/lib/settings"; +import type { SettingsSectionProps } from "@liveagent/app/pages/settings/types"; +import { useLocale } from "@liveagent/ui/i18n"; +import { SIDEBAR_SHORTCUTS } from "../../lib/settings/sidebarShortcuts"; +import { AgentActivationSwitch, SettingsGroup, SettingsRow } from "./shared"; + +export function SidebarShortcutsSection({ settings, setSettings }: SettingsSectionProps) { + const { t } = useLocale(); + return ( +
+ + {SIDEBAR_SHORTCUTS.map(({ id, labelKey }) => ( + + setSettings((prev) => + updateCustomSettings(prev, { + sidebarShortcuts: { + ...prev.customSettings.sidebarShortcuts, + [id]: !prev.customSettings.sidebarShortcuts[id], + }, + }), + ) + } + /> + } + /> + ))} + +
+ ); +} diff --git a/crates/agent-ui/src/pages/settings/SystemSettingsForm.tsx b/crates/agent-ui/src/pages/settings/SystemSettingsForm.tsx index d28da3938..bd7a33804 100644 --- a/crates/agent-ui/src/pages/settings/SystemSettingsForm.tsx +++ b/crates/agent-ui/src/pages/settings/SystemSettingsForm.tsx @@ -49,6 +49,8 @@ import { } from "@liveagent/ui/pages/settings/shared"; import { type ComponentProps, type ReactNode, useEffect, useMemo, useState } from "react"; +import { SidebarShortcutsSection } from "./SidebarShortcutsSection"; + const FONT_SCALE_OPTIONS = [0.9, 1, 1.1, 1.2] as const; type FontFamilySettingKey = "interfaceFontFamily" | "chatFontFamily" | "codeFontFamily"; @@ -437,6 +439,8 @@ export function SystemSettingsForm(props: SettingsSectionProps) {
+ +
-
+