Skip to content
Open
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
40 changes: 37 additions & 3 deletions crates/agent-gateway/web/src/app/GatewayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
getRightDockFileTreeState,
getRightDockProjectState,
getSshProjectHostIds,
getWorkModeDefinition,
isAgentDevMode,
isRightDockSingletonTabOpen,
isThinkingAlwaysOnForModel,
Expand All @@ -85,6 +86,7 @@ import {
resolveEffectiveTheme,
resolveWorkspaceProjects,
type SelectedModel,
setActiveWorkMode,
setSelectedModel,
updateChatRuntimeControlsForProvider,
updateCustomSettings,
Expand All @@ -94,6 +96,8 @@ import {
updateSkills,
updateSshProjectHostIds,
updateSystem,
WORK_MODE_IDS,
type WorkModeId,
type WorkspaceProject,
workspaceProjectPathKey,
} from "@/lib/settings";
Expand Down Expand Up @@ -3442,6 +3446,31 @@ export default function GatewayApp() {
[displayedConversationId, setSettings],
);

const activeWorkMode = useMemo(
() => getWorkModeDefinition(settings.customSettings.workMode.activeModeId),
[settings.customSettings.workMode.activeModeId],
);
const handleWorkModeChange = useCallback(
(modeId: WorkModeId) => {
setSettings((prev) => setActiveWorkMode(prev, modeId));
},
[setSettings],
);
// Ctrl/⌘+Alt+1/2/3 快速切换工作模式(避开浏览器 Ctrl+数字 的标签页快捷键)。
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (!(event.ctrlKey || event.metaKey) || !event.altKey || event.repeat) return;
const index = ["Digit1", "Digit2", "Digit3"].indexOf(event.code);
if (index < 0) return;
const modeId = WORK_MODE_IDS[index];
if (!modeId) return;
event.preventDefault();
handleWorkModeChange(modeId);
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [handleWorkModeChange]);

const skillsEnabled = settings.skills.enabled && isAgentMode;
const selectedSkillNames = useMemo(
() => (skillsEnabled ? mergeAlwaysEnabledSkillNames(settings.skills.selected) : []),
Expand Down Expand Up @@ -3855,9 +3884,11 @@ export default function GatewayApp() {
? translate("chat.compactingContextWait", settings.locale)
: historyDetailLoading
? "正在加载会话历史,请稍候..."
: enabledComposerSkills.length > 0
? translate("chat.inputHintWithSkills", settings.locale)
: translate("chat.inputHint", settings.locale);
: activeWorkMode.composerHintKey
? translate(activeWorkMode.composerHintKey, settings.locale)
: enabledComposerSkills.length > 0
? translate("chat.inputHintWithSkills", settings.locale)
: translate("chat.inputHint", settings.locale);
const canDropUpload =
status?.online === true &&
isAgentMode &&
Expand Down Expand Up @@ -4038,6 +4069,8 @@ export default function GatewayApp() {
onConversationsRemoved={handleSidebarConversationsRemoved}
onCloseSidebar={() => setSidebarOpen(false)}
onOpenSettings={() => openSettings()}
activeWorkModeId={settings.customSettings.workMode.activeModeId}
onWorkModeChange={handleWorkModeChange}
onOpenSkillsHub={handleSidebarOpenSkillsHub}
onOpenMcpHub={handleSidebarOpenMcpHub}
/>
Expand Down Expand Up @@ -4240,6 +4273,7 @@ export default function GatewayApp() {
branchPendingMessageId={branchPendingMessageId}
onSuggestionSelect={handleEmptyStateSuggestion}
suggestionsDisabled={isSuggestionTyping}
workMode={activeWorkMode}
/>
</ScrollArea>
{displayedTranscriptRowCount > 0 && !conversationOpenState.showOverlay ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ChatHistorySidebar } from "@/components/chat/ChatHistorySidebar";
import { useLocale } from "@/i18n";
import type { ChatHistorySummary } from "@/lib/chat/chatHistory";
import type { WorkspaceProject } from "@/lib/settings";
import type { WorkModeId, WorkspaceProject } from "@/lib/settings";
import {
selectConversations,
selectListState,
Expand Down Expand Up @@ -125,6 +125,8 @@ export type GatewaySidebarContainerProps = {
onConversationsRemoved: (ids: readonly string[]) => void;
onCloseSidebar: () => void;
onOpenSettings: () => void;
activeWorkModeId: WorkModeId;
onWorkModeChange: (modeId: WorkModeId) => void;
onOpenSkillsHub: () => void;
onOpenMcpHub: () => void;
};
Expand Down Expand Up @@ -353,6 +355,8 @@ export function GatewaySidebarContainer(props: GatewaySidebarContainerProps) {
onLoadMore={handleLoadMore}
onCloseSidebar={props.onCloseSidebar}
onOpenSettings={props.onOpenSettings}
activeWorkModeId={props.activeWorkModeId}
onWorkModeChange={props.onWorkModeChange}
onOpenSkillsHub={props.onOpenSkillsHub}
onOpenMcpHub={props.onOpenMcpHub}
/>
Expand Down
5 changes: 5 additions & 0 deletions crates/agent-gateway/web/src/components/GatewayTranscript.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
import type { RetryAttemptRecord, TranscriptRow } from "../lib/chat/transcript/types";

import type { GatewayTranscriptRound } from "../lib/chatUi";
import type { WorkModeDefinition } from "../lib/settings";
import type { SectionId } from "../pages/settings/types";
import { ChatEmptyState } from "./chat/ChatEmptyState";
import {
Expand Down Expand Up @@ -122,6 +123,8 @@ type GatewayTranscriptProps = {
branchPendingMessageId?: string | null;
onSuggestionSelect?: (text: string) => void;
suggestionsDisabled?: boolean;
/** 当前工作模式:驱动空态问候语、建议卡片与光晕色。 */
workMode?: WorkModeDefinition;
readOnly?: boolean;
redactToolContent?: boolean;
};
Expand Down Expand Up @@ -1747,6 +1750,7 @@ export function GatewayTranscript({
branchPendingMessageId,
onSuggestionSelect,
suggestionsDisabled = false,
workMode,
readOnly = false,
redactToolContent = false,
}: GatewayTranscriptProps) {
Expand Down Expand Up @@ -1785,6 +1789,7 @@ export function GatewayTranscript({
onOpenSettings={onOpenSettings}
onSuggestionSelect={readOnly ? undefined : onSuggestionSelect}
suggestionsDisabled={suggestionsDisabled}
workMode={workMode}
/>
</div>
</div>
Expand Down
111 changes: 66 additions & 45 deletions crates/agent-gateway/web/src/components/chat/ChatEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { type CSSProperties, useEffect, useState } from "react";

import { FolderTree, Lightbulb, Settings, Wrench } from "@/components/icons";
import {
BookOpen,
Edit3,
FolderTree,
GitBranch,
LayoutGrid,
Lightbulb,
ListChecks,
Palette,
Settings,
Wrench,
} from "@/components/icons";
import { useLocale } from "@/i18n/LocaleContext";
import {
getWorkModeDefinition,
type WorkModeDefinition,
type WorkModeSuggestionIconId,
} from "@/lib/settings";
import { cn } from "@/lib/shared/utils";
import type { SectionId } from "@/pages/settings/types";

type GreetingPeriod = "morning" | "noon" | "afternoon" | "evening" | "night";
Expand Down Expand Up @@ -37,46 +54,40 @@ function useGreetingPeriod() {
return period;
}

const SUGGESTION_CARDS = [
{
key: "explore",
icon: FolderTree,
chipClassName: "text-sky-600 dark:text-sky-400",
titleKey: "chat.suggestExploreTitle",
promptKey: "chat.suggestExplorePrompt",
},
{
key: "fix",
icon: Wrench,
chipClassName: "text-amber-600 dark:text-amber-400",
titleKey: "chat.suggestFixTitle",
promptKey: "chat.suggestFixPrompt",
},
{
key: "ideate",
icon: Lightbulb,
chipClassName: "text-emerald-600 dark:text-emerald-400",
titleKey: "chat.suggestIdeateTitle",
promptKey: "chat.suggestIdeatePrompt",
},
] as const;
// 工作模式建议卡片图标 id → 图标组件(模式定义位于镜像的 workModes.ts,
// 不能直接携带各端的组件引用)。
const SUGGESTION_ICONS: Record<WorkModeSuggestionIconId, typeof FolderTree> = {
folderTree: FolderTree,
wrench: Wrench,
lightbulb: Lightbulb,
listChecks: ListChecks,
penLine: Edit3,
bookOpen: BookOpen,
layoutGrid: LayoutGrid,
gitBranch: GitBranch,
palette: Palette,
};

export type ChatEmptyStateProps = {
variant: "no-models" | "start-chat";
onOpenSettings?: (section?: SectionId) => void;
onSuggestionSelect?: (text: string) => void;
/** Locks the suggestion cards while a picked prompt is still typing in. */
suggestionsDisabled?: boolean;
/** 当前工作模式:驱动问候语、建议卡片与光晕色;缺省为编程模式。 */
workMode?: WorkModeDefinition;
};

export function ChatEmptyState({
variant,
onOpenSettings,
onSuggestionSelect,
suggestionsDisabled = false,
workMode,
}: ChatEmptyStateProps) {
const { t } = useLocale();
const period = useGreetingPeriod();
const mode = workMode ?? getWorkModeDefinition("coding");

return (
<div className="relative flex w-full flex-col items-center">
Expand All @@ -86,7 +97,10 @@ export function ChatEmptyState({
<div className="chat-hero-logo-float relative flex h-full w-full items-center justify-center">
<div
aria-hidden="true"
className="chat-hero-halo-breathe absolute inset-1 rounded-full bg-sky-500/10 blur-xl dark:bg-sky-400/10"
className={cn(
"chat-hero-halo-breathe absolute inset-1 rounded-full blur-xl",
mode.haloClassName,
)}
/>
<img
src="/icon-simple.png"
Expand Down Expand Up @@ -123,29 +137,36 @@ export function ChatEmptyState({
) : (
<>
<div className="chat-hero-title-enter whitespace-nowrap text-center text-[calc(20px*var(--zone-font-scale,1))] font-semibold leading-7 tracking-tight text-foreground">
{t(GREETING_KEYS[period])},{t("chat.greetingSubtitle")}
{t(GREETING_KEYS[period])},{t(mode.greetingSubtitleKey)}
</div>
{onSuggestionSelect ? (
<div className="mt-7 grid w-full max-w-[520px] grid-cols-1 gap-2 px-6 sm:grid-cols-3 sm:px-4">
{SUGGESTION_CARDS.map((card, index) => (
<button
key={card.key}
type="button"
disabled={suggestionsDisabled}
onClick={() => onSuggestionSelect(t(card.promptKey))}
style={{ "--chat-hero-delay": `${0.26 + index * 0.08}s` } as CSSProperties}
className="chat-hero-card-enter flex h-11 items-center gap-2 rounded-lg bg-foreground/[0.025] px-2.5 text-left text-foreground/85 transition-colors hover:bg-foreground/[0.055] hover:text-foreground focus-visible:bg-foreground/[0.055] focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
>
<span
className={`flex h-7 w-7 shrink-0 items-center justify-center ${card.chipClassName}`}
// Keyed per mode so the card entrance replays on mode switches.
<div
key={mode.id}
className="mt-7 grid w-full max-w-[520px] grid-cols-1 gap-2 px-6 sm:grid-cols-3 sm:px-4"
>
{mode.suggestions.map((card, index) => {
const Icon = SUGGESTION_ICONS[card.icon];
return (
<button
key={card.key}
type="button"
disabled={suggestionsDisabled}
onClick={() => onSuggestionSelect(t(card.promptKey))}
style={{ "--chat-hero-delay": `${0.26 + index * 0.08}s` } as CSSProperties}
className="chat-hero-card-enter flex h-11 items-center gap-2 rounded-lg bg-foreground/[0.025] px-2.5 text-left text-foreground/85 transition-colors hover:bg-foreground/[0.055] hover:text-foreground focus-visible:bg-foreground/[0.055] focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
>
<card.icon className="h-4 w-4" />
</span>
<span className="min-w-0 truncate text-[calc(14px*var(--zone-font-scale,1))] font-medium leading-5 text-foreground/90">
{t(card.titleKey)}
</span>
</button>
))}
<span
className={`flex h-7 w-7 shrink-0 items-center justify-center ${card.chipClassName}`}
>
<Icon className="h-4 w-4" />
</span>
<span className="min-w-0 truncate text-[calc(14px*var(--zone-font-scale,1))] font-medium leading-5 text-foreground/90">
{t(card.titleKey)}
</span>
</button>
);
})}
</div>
) : null}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { useLocale } from "../../i18n";
import type { ChatHistorySummary } from "../../lib/chat/chatHistory";
import {
DEFAULT_WORKSPACE_PROJECT_ID,
WORK_MODES,
type WorkModeId,
type WorkspaceProject,
workspaceProjectPathKey,
} from "../../lib/settings";
Expand Down Expand Up @@ -123,6 +125,8 @@ type ChatHistorySidebarProps = {
onLoadMore: () => void;
onCloseSidebar: () => void;
onOpenSettings: () => void;
activeWorkModeId: WorkModeId;
onWorkModeChange: (modeId: WorkModeId) => void;
onOpenSkillsHub?: () => void;
onOpenMcpHub?: () => void;
};
Expand Down Expand Up @@ -1286,6 +1290,8 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi
onLoadMore,
onCloseSidebar,
onOpenSettings,
activeWorkModeId,
onWorkModeChange,
onOpenSkillsHub,
onOpenMcpHub,
} = props;
Expand Down Expand Up @@ -1910,13 +1916,40 @@ export const ChatHistorySidebar = memo(function ChatHistorySidebar(props: ChatHi
<div className="flex min-w-0 -translate-y-0.5 items-center gap-2">
<img
src="/icon-simple.png"
alt=""
aria-hidden="true"
alt="Live Agent"
title="Live Agent"
draggable={false}
className="h-8 w-8 shrink-0 select-none rounded-xl object-contain"
/>
<div className="min-w-0">
<div className="truncate font-semibold tracking-tight">Live Agent</div>
{/* 工作模式切换器:品牌文字让位给一键模式切换(编程/写作/设计)。 */}
<div className="flex min-w-0 items-center rounded-xl border border-border/40 bg-background/60 p-0.5 shadow-[0_1px_0_rgba(255,255,255,0.5)_inset] dark:border-white/[0.06] dark:bg-white/[0.04] dark:shadow-[0_1px_0_rgba(255,255,255,0.04)_inset]">
{WORK_MODES.map((mode) => {
const isActiveMode = mode.id === activeWorkModeId;
return (
<button
key={mode.id}
type="button"
onClick={() => onWorkModeChange(mode.id)}
title={t(mode.descriptionKey)}
aria-pressed={isActiveMode}
className={cn(
"flex h-7 min-w-0 items-center gap-1.5 rounded-[10px] px-2 text-[calc(12px*var(--zone-font-scale,1))] font-medium leading-none transition-all",
isActiveMode
? "bg-background/85 text-foreground shadow-[0_1px_0_rgba(255,255,255,0.55)_inset] ring-1 ring-border/45 dark:bg-white/[0.08] dark:ring-white/[0.09] dark:shadow-[0_1px_0_rgba(255,255,255,0.06)_inset]"
: "text-muted-foreground hover:text-foreground",
)}
>
<span
aria-hidden="true"
className={cn(
"h-1.5 w-1.5 shrink-0 rounded-full transition-colors",
isActiveMode ? mode.accentClassName : "bg-foreground/15",
)}
/>
<span className="truncate">{t(mode.labelKey)}</span>
</button>
);
})}
</div>
</div>

Expand Down
2 changes: 2 additions & 0 deletions crates/agent-gateway/web/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import Minimize2Source from "~icons/lucide/minimize-2";
import MinusSource from "~icons/lucide/minus";
import MonitorSmartphoneSource from "~icons/lucide/monitor-smartphone";
import MoonSource from "~icons/lucide/moon";
import PaletteSource from "~icons/lucide/palette";
import PanelLeftSource from "~icons/lucide/panel-left";
import PanelLeftCloseSource from "~icons/lucide/panel-left-close";
import PanelRightCloseSource from "~icons/lucide/panel-right-close";
Expand Down Expand Up @@ -525,6 +526,7 @@ export const PanelLeftClose = createIcon(PanelLeftCloseSource);
export const PanelRightClose = createIcon(PanelRightCloseSource);
export const PanelRightOpen = createIcon(PanelRightOpenSource);
export const Paperclip = createIcon(PaperclipSource);
export const Palette = createIcon(PaletteSource);
export const Pencil = createIcon(PencilSource);
export const Pin = createIcon(PinSource);
export const PinOff = createIcon(PinOffSource);
Expand Down
Loading
Loading