Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/pr-assets/sidebar-resource-navigation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/pr-assets/sidebar-resource-settings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 2 additions & 4 deletions crates/agent-gateway/web/src/app/GatewayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1134,8 +1134,7 @@ function useGatewayAppController() {
handleSidebarConversationsRemoved,
handleSidebarLocalDraftDeleted,
handleSidebarNewConversation,
handleSidebarOpenMcpHub,
handleSidebarOpenSkillsHub,
handleSidebarOpenResourceHub,
handleSidebarSelectConversation,
startNewConversation,
} = createGatewayConversationActions({
Expand Down Expand Up @@ -2072,8 +2071,7 @@ function useGatewayAppController() {
handleSidebarConversationsRemoved: handleSidebarConversationsRemovedWithWorkbench,
handleSidebarLocalDraftDeleted: handleSidebarLocalDraftDeletedWithWorkbench,
handleSidebarNewConversation,
handleSidebarOpenMcpHub,
handleSidebarOpenSkillsHub,
handleSidebarOpenResourceHub,
handleSidebarProjectsCollapsedChange,
handleSidebarRecentCollapsedChange,
handleSidebarSelectConversation,
Expand Down
9 changes: 4 additions & 5 deletions crates/agent-gateway/web/src/app/GatewayAppView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,7 @@ export function GatewayAppView({ viewModel }: { viewModel: GatewayAppViewModel }
handleSidebarConversationsRemoved,
handleSidebarLocalDraftDeleted,
handleSidebarNewConversation,
handleSidebarOpenMcpHub,
handleSidebarOpenSkillsHub,
handleSidebarOpenResourceHub,
handleSidebarProjectsCollapsedChange,
handleSidebarRecentCollapsedChange,
handleSidebarSelectConversation,
Expand Down Expand Up @@ -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 ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -208,7 +209,7 @@ export function createGatewayConversationActions(options: CreateGatewayConversat
});
}
};
const openHub = (view: "skills-hub" | "mcp-hub") => {
const openHub = (view: Exclude<ApplicationViewId, "chat">) => {
options.setRightDockOpen(false);
if (isMobileSidebarLayout()) options.setSidebarOpen(false);
options.cacheVisibleComposerDraft();
Expand Down Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
3 changes: 2 additions & 1 deletion crates/agent-gateway/web/src/app/hooks/usePendingUploads.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions crates/agent-gateway/web/src/pages/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export type SetSettingsFn = (updater: (prev: AppSettings) => AppSettings) => voi

export type SectionId =
| "system"
| "skills"
| "mcp"
| "systemTools"
| "stt"
| "providers"
Expand Down
25 changes: 25 additions & 0 deletions crates/agent-gateway/web/test/resource-hub-navigation.test.mjs
Original file line number Diff line number Diff line change
@@ -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`]]);
});
}
2 changes: 2 additions & 0 deletions crates/agent-gui/src/lib/settings/storage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { normalizeSidebarShortcuts } from "@liveagent/ui/lib/settings/sidebarShortcuts";
import {
buildGatewaySettingsSyncPayload,
buildGatewaySettingsSyncUpdatePayload,
Expand Down Expand Up @@ -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 同口径):脏值/缺省落回统计状态栏。
Expand Down
12 changes: 4 additions & 8 deletions crates/agent-gui/src/pages/ChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}}
/>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
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";

type UseComposerDraftCacheParams = {
composerRef: MutableRefObject<MentionComposerHandle | null>;
currentConversationIdRef: MutableRefObject<string>;
activeView: "chat" | "skills-hub" | "mcp-hub";
activeView: ApplicationViewId;
currentConversationId: string;
draftStore: ConversationDraftStore;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -45,7 +46,7 @@ type UseWorkspaceProjectsParams = {
workdir: string;
t: (key: string) => string;
setErrorMessage: Dispatch<SetStateAction<string | null>>;
setActiveView: Dispatch<SetStateAction<"chat" | "skills-hub" | "mcp-hub">>;
setActiveView: Dispatch<SetStateAction<ApplicationViewId>>;
setRightDockOpen: Dispatch<SetStateAction<boolean>>;
startNewConversationActionRef: MutableRefObject<(options?: { workdir?: string }) => string>;
prepareComposerForConversationChangeActionRef: MutableRefObject<() => void>;
Expand Down
2 changes: 2 additions & 0 deletions crates/agent-gui/src/pages/settings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type SetSettingsFn = (updater: (prev: AppSettings) => AppSettings) => voi
export type SectionId =
| "system"
| "shortcuts"
| "skills"
| "mcp"
| "systemTools"
| "stt"
| "providers"
Expand Down
2 changes: 2 additions & 0 deletions crates/agent-gui/test/chat/search-sidebar-reveal.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ const virtualizer = {
const icons = [
"AlertCircle",
"Blend",
"Brain",
"Cable",
"Check",
"ChevronRight",
"CirclePlus",
"Clock3",
"Folder",
"FolderClosed",
"FolderOpen",
Expand Down
60 changes: 60 additions & 0 deletions crates/agent-gui/test/settings/normalization.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
29 changes: 29 additions & 0 deletions crates/agent-gui/test/settings/storage.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
});
});
12 changes: 11 additions & 1 deletion crates/agent-ui/src/application/ApplicationView.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAttributes<HTMLDivElement>, "children">;
Expand Down Expand Up @@ -56,6 +58,14 @@ export function ApplicationView(props: ApplicationViewProps) {
content = (
<McpHubPage settings={settings} setSettings={setSettings} isAgentMode={isAgentMode} />
);
} else if (activeView === "memory-hub" || activeView === "cron-hub") {
content = (
<ResourceManagementPage
resource={activeView === "memory-hub" ? "memory" : "cron"}
settings={settings}
setSettings={setSettings}
/>
);
} else {
const { containerProps, content: chatContent } = chat;
content = (
Expand Down
Loading
Loading