feat(workbench): refine operational interface

This commit is contained in:
2026-07-14 16:22:21 +08:00
parent ed60a13f12
commit 2b768c2c06
25 changed files with 512 additions and 153 deletions
@@ -52,7 +52,7 @@ import {
agentPanelSectionVariants
} from "./agent-motion";
import { AgentHistoryPanel } from "./agent-history-panel";
import { AgentMessageDetails } from "./agent-message-details";
import { AgentMessageDetails, AgentMessageProgress } from "./agent-message-details";
import { AgentOperationalBrief } from "./agent-operational-brief";
import { AgentUiEnvelopeRenderer } from "./agent-ui-envelope-renderer";
import { StreamingTokenResponse } from "./streaming-token-response";
@@ -175,7 +175,7 @@ export function AgentCommandPanel({
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
<p className="shrink-0 text-xs font-semibold uppercase text-slate-500"></p>
<span className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-lg border border-slate-200/70 bg-white/75 px-2 text-xs font-semibold text-slate-600">
<span className="inline-flex h-6 shrink-0 items-center gap-1.5 rounded-lg border border-slate-200/70 bg-[var(--glass-readable)] px-2 text-xs font-semibold text-slate-600">
<span className={cn("h-1.5 w-1.5 rounded-full", getAgentConnectionClassName(statusLabel))} />
{getAgentConnectionLabel(statusLabel)}
</span>
@@ -313,15 +313,17 @@ export function AgentCommandPanel({
</AgentContent>
<div className="agent-panel-band space-y-2 border-t border-white/70 p-3">
<Suggestions aria-label="推荐问题" role="group">
<Suggestions aria-label="推荐问题" role="group" className="px-0.5">
{AGENT_PROMPT_SUGGESTIONS.map((suggestion) => (
<Suggestion
key={suggestion}
suggestion={suggestion}
disabled={streaming}
onClick={submitPrompt}
className="border-slate-200/80 bg-white/75 text-slate-600 shadow-none hover:border-blue-200 hover:bg-blue-50 hover:text-blue-700"
/>
className="max-w-full border-slate-200/80 bg-[var(--glass-readable)] text-slate-600 shadow-none hover:border-blue-200 hover:bg-blue-50 hover:text-blue-700"
>
<span className="truncate">{suggestion}</span>
</Suggestion>
))}
</Suggestions>
<PromptInput
@@ -520,7 +522,7 @@ function AgentModelSelect({
key={model.id}
className={cn(
"rounded-lg border border-transparent px-2.5 py-2 focus:bg-slate-50",
selected && "border-violet-200 bg-violet-50/80 focus:bg-violet-50"
selected && "glass-agent border-violet-200 focus:bg-violet-50"
)}
onSelect={() => onValueChange?.(model.id)}
>
@@ -612,6 +614,12 @@ function BackendMessageList({
)}
>
<div className="space-y-3">
{message.role === "assistant" ? (
<AgentMessageProgress
progress={message.progress}
running={streaming && index === messages.length - 1}
/>
) : null}
{message.content ? (
message.role === "assistant" ? (
<StreamingTokenResponse
@@ -2,10 +2,11 @@
import {
CheckCircle2,
ChevronDown,
HelpCircle,
ListChecks,
ListTree,
LockKeyhole,
Loader2,
ShieldCheck,
XCircle,
type LucideIcon
@@ -106,8 +107,7 @@ export function AgentMessageDetails({
onRejectQuestion?: (request: AgentQuestionRequest) => Promise<void> | void;
}) {
const hasDetails = Boolean(
message.progress?.length ||
message.todos?.todos.length ||
message.todos?.todos.length ||
message.permissions?.length ||
message.questions?.length
);
@@ -119,11 +119,6 @@ export function AgentMessageDetails({
return (
<motion.div className="space-y-2" layout variants={agentPanelListVariants} animate="animate">
<AnimatePresence initial={false} mode="popLayout">
{message.progress?.length ? (
<motion.div key="progress" layout initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
<ProgressList progress={message.progress} />
</motion.div>
) : null}
{message.todos ? (
<motion.div key="todos" layout initial="initial" animate="animate" exit="exit" variants={agentPanelItemVariants}>
<TodoCard todoUpdate={message.todos} />
@@ -148,6 +143,20 @@ export function AgentMessageDetails({
);
}
export function AgentMessageProgress({
progress,
running
}: {
progress?: AgentChatProgress[];
running: boolean;
}) {
if (!progress?.length) {
return null;
}
return <ProgressList progress={progress} running={running} />;
}
function AgentNestedBlock({
icon: Icon,
iconClassName,
@@ -185,44 +194,76 @@ function AnimatedDetailItem({ children }: { children: ReactNode }) {
);
}
function ProgressList({ progress }: { progress: AgentChatProgress[] }) {
function ProgressList({ progress, running }: { progress: AgentChatProgress[]; running: boolean }) {
const [expanded, setExpanded] = useState(false);
const recentProgress = progress.slice(-3);
const visibleProgress = expanded ? progress : running ? recentProgress : [];
return (
<AgentNestedBlock icon={Loader2} iconClassName="text-blue-600" title="执行进度">
<motion.ol className="space-y-1.5" variants={agentPanelListVariants} animate="animate">
<AnimatePresence initial={false} mode="popLayout">
{progress.slice(-6).map((item) => (
<motion.li
key={item.id}
layout
className="grid grid-cols-[16px_1fr_auto] items-start gap-2 text-xs"
initial="initial"
animate="animate"
exit="exit"
variants={agentPanelItemVariants}
transition={agentLayoutTransition}
>
<span
className={cn(
"mt-0.5 h-3.5 w-3.5 rounded-full border",
progressStatusMeta[item.status].dotClassName
)}
/>
<span className="min-w-0">
<span className="block truncate font-semibold text-slate-800" title={item.title}>
{item.title}
<div className="agent-panel-nested rounded-xl p-2.5">
<button
type="button"
className="flex w-full items-center gap-2 text-left text-xs font-semibold text-slate-600"
aria-expanded={expanded}
onClick={() => setExpanded((current) => !current)}
>
<ListTree size={16} className="shrink-0 text-blue-700" aria-hidden="true" />
<span></span>
<span className="flex-1" />
<span className="shrink-0 font-normal text-slate-400">
{expanded
? `全部 ${progress.length}`
: running
? `最近 ${recentProgress.length}`
: `${progress.length}`}
</span>
<ChevronDown
size={14}
className={cn("shrink-0 text-slate-400 transition-transform", expanded && "rotate-180")}
aria-hidden="true"
/>
</button>
<AnimatePresence initial={false}>
{visibleProgress.length ? (
<motion.ol
className="mt-2 space-y-1.5 overflow-hidden"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={agentLayoutTransition}
>
{visibleProgress.map((item) => (
<li key={item.id} className="grid grid-cols-[16px_1fr_auto] items-start gap-2 text-xs">
<span
className={cn(
"mt-0.5 h-3.5 w-3.5 rounded-full border",
progressStatusMeta[item.status].dotClassName
)}
/>
<span className="min-w-0">
<span className="block truncate font-semibold text-slate-800" title={item.title}>
{item.title}
</span>
{item.detail ? (
<span className="block line-clamp-2 whitespace-pre-wrap leading-5 text-slate-500">
{item.detail}
</span>
) : null}
</span>
{item.detail ? (
<span className="block line-clamp-2 whitespace-pre-wrap leading-5 text-slate-500">{item.detail}</span>
) : null}
</span>
<span className={cn("rounded-full px-2 py-0.5 font-semibold", progressStatusMeta[item.status].className)}>
{progressStatusMeta[item.status].label}
</span>
</motion.li>
))}
</AnimatePresence>
</motion.ol>
</AgentNestedBlock>
<span
className={cn(
"rounded-full px-2 py-0.5 font-semibold",
progressStatusMeta[item.status].className
)}
>
{progressStatusMeta[item.status].label}
</span>
</li>
))}
</motion.ol>
) : null}
</AnimatePresence>
</div>
);
}
@@ -310,7 +351,7 @@ function PermissionRequestCard({
};
return (
<div className="rounded-lg bg-white/90 px-2.5 py-2 text-xs">
<div className="rounded-lg bg-[var(--glass-menu)] px-2.5 py-2 text-xs">
<div className="grid grid-cols-[1fr_auto] gap-2">
<div className="min-w-0">
<p className="truncate font-semibold text-slate-800" title={permission.target ?? permission.permission}>
@@ -438,7 +479,7 @@ function QuestionRequestCard({
};
return (
<div className="rounded-lg bg-white/90 px-2.5 py-2 text-xs">
<div className="rounded-lg bg-[var(--glass-menu)] px-2.5 py-2 text-xs">
<div className="mb-2 flex items-center justify-between gap-2">
<span className="min-w-0 truncate font-semibold text-slate-800">
{request.questions[0]?.header || "问题"}
@@ -556,7 +597,7 @@ function QuestionInput({
return (
<label
key={option.label}
className="flex cursor-pointer items-start gap-2 rounded-md border border-slate-200 bg-white/90 px-2 py-1.5 text-slate-600"
className="flex cursor-pointer items-start gap-2 rounded-md border border-slate-200 bg-[var(--surface-solid)] px-2 py-1.5 text-slate-600"
>
<input
type={question.multiple ? "checkbox" : "radio"}
@@ -579,7 +620,7 @@ function QuestionInput({
) : null}
{showCustomInput ? (
<textarea
className="min-h-16 w-full resize-none rounded-md border border-slate-200 bg-white/90 px-2 py-1.5 text-xs leading-5 text-slate-700 outline-none transition focus:border-blue-300 focus:ring-2 focus:ring-blue-100 disabled:opacity-60"
className="min-h-16 w-full resize-none rounded-md border border-slate-200 bg-[var(--surface-solid)] px-2 py-1.5 text-xs leading-5 text-slate-700 outline-none transition focus:border-blue-300 focus:ring-2 focus:ring-blue-100 disabled:opacity-60"
placeholder="输入回答"
value={value.custom}
disabled={disabled}
@@ -61,7 +61,7 @@ export function AgentOperationalBrief({
</button>
<button
type="button"
className="flex h-9 items-center justify-center gap-1.5 rounded-xl border border-slate-200 bg-white/90 px-3 text-xs font-semibold text-slate-700 transition hover:border-blue-200 hover:bg-blue-50 hover:text-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
className="flex h-9 items-center justify-center gap-1.5 rounded-xl border border-slate-200 bg-[var(--glass-menu)] px-3 text-xs font-semibold text-slate-700 transition hover:border-blue-200 hover:bg-blue-50 hover:text-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
disabled={streaming}
onClick={() => onSubmitCommand(brief.secondaryCommand)}
>
@@ -303,7 +303,7 @@ function PropRows({ props, labels }: { props: unknown; labels: Record<string, st
return (
<dl className="grid grid-cols-2 gap-2">
{rows.map((row) => (
<div key={row.label} className="min-w-0 rounded-lg bg-white/90 px-2.5 py-2">
<div key={row.label} className="min-w-0 rounded-lg bg-[var(--glass-menu)] px-2.5 py-2">
<dt className="truncate text-xs text-slate-500">{row.label}</dt>
<dd className="mt-1 truncate text-sm font-semibold text-slate-900" title={row.value}>
{row.value}