fix: refine team selector panel

This commit is contained in:
coso
2026-03-21 00:14:09 +08:00
parent f57be31311
commit 34adb1cc0a
4 changed files with 573 additions and 300 deletions
+1
View File
@@ -4,6 +4,7 @@
- **Team Workspace 正式成型**:Agent 聊天页新增 Team Workspace 主工作台、建议栏、Dock 与 Home Shell,围绕多代理协作视图重组交互结构
- **Team 配置与发布稳定性收尾**:补齐 Team Selector 自定义 Team 配置链路、当前 Team 展示与相关测试,并修复通知、Provider Runtime 与前端类型兼容问题,确保 `v0.92.0` 可稳定构建发布
- **Team Selector 交互继续打磨**:扩展 Team 选择弹窗宽度,重构为模板列表 + 详情检查器布局,并补充执行会话失效后的恢复测试,减少工作台切换与继续执行时的状态错乱
- **运行态与工具可视化增强**:`ToolCallDisplay`、Harness 状态面板、Runtime Strip、执行日志与子代理时间线继续增强,工具调用与运行态反馈更完整
- **Aster Agent 运行时继续收口**:Rust 侧补齐 session store、subagent control、agent tools inventory / execution、runtime queue 及命令桥接,统一现役 Agent Runtime 路径
- **治理与测试基建升级**:本地校验脚本、命令契约检查、workspace smoke 与治理报告继续完善,发布前自检链路更清晰
@@ -78,7 +78,7 @@ export const TeamSelector: React.FC<TeamSelectorProps> = ({
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-6xl overflow-hidden border-slate-200/80 bg-white p-0">
<DialogContent className="w-[min(1120px,calc(100vw-32px))] max-w-[1120px] overflow-hidden border-slate-200/80 bg-white p-0 shadow-2xl">
{open ? (
<Suspense
fallback={
@@ -433,315 +433,319 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
recommendedTeam && selectedTeam?.id === recommendedTeam.id,
);
return (
<div
className="max-h-[min(76vh,720px)] w-[min(520px,calc(100vw-24px))] overflow-y-auto bg-white"
data-testid="team-selector-panel"
>
<div className="border-b border-slate-200/80 bg-[linear-gradient(180deg,rgb(255,255,255)_0%,rgb(248,250,252)_100%)] px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[11px] font-semibold tracking-[0.08em] text-slate-500">
TEAM 配置
</div>
<div className="mt-1 text-sm text-slate-700">
只在当前任务适合拆分协作时,为主代理提供团队结构参考。
</div>
</div>
{selectedTeam ? (
<button
type="button"
className="inline-flex h-8 w-8 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-100 hover:text-slate-700"
onClick={handleClearSelection}
title="清除 Team 选择"
aria-label="清除 Team 选择"
>
<X className="h-4 w-4" />
</button>
) : null}
</div>
{selectedTeam ? (
<div
className="mt-3 rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2.5"
data-testid="team-selector-current"
>
<div className="flex items-center gap-2 text-xs font-medium text-slate-500">
<Users className="h-3.5 w-3.5" />
当前已选 Team
</div>
<div className="mt-1 text-sm font-semibold text-slate-900">
{selectedTeam.label}
</div>
{currentSelectionSummary ? (
<div className="mt-1 text-xs leading-5 text-slate-600">
{currentSelectionSummary}
</div>
) : null}
</div>
) : null}
</div>
const inspectorTeam = useMemo(() => {
const teamById = new Map<string, TeamDefinition>();
<div className="space-y-4 p-4">
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="搜索 Team、角色或职责"
className="border-slate-200 bg-white"
/>
<div className="flex flex-wrap gap-2">
<Button
type="button"
className="bg-slate-900 text-white hover:bg-slate-800"
onClick={() =>
handleStartCreate(
selectedTeam || recommendedTeam || builtinTeams[0] || null,
)
}
>
<Plus className="mr-1.5 h-4 w-4" />
新建自定义 Team
</Button>
{selectedTeam ? (
for (const candidate of [
selectedTeam,
recommendedTeam,
...filteredCustomTeams,
...builtinTeams,
]) {
if (candidate && !teamById.has(candidate.id)) {
teamById.set(candidate.id, candidate);
}
}
const preferredId =
expandedTeamId ||
selectedTeam?.id ||
recommendedTeam?.id ||
filteredCustomTeams[0]?.id ||
builtinTeams[0]?.id;
if (!preferredId) {
return null;
}
return teamById.get(preferredId) || null;
}, [
builtinTeams,
expandedTeamId,
filteredCustomTeams,
recommendedTeam,
selectedTeam,
]);
const renderInspectorPanel = () => {
if (!inspectorTeam) {
return (
<div className="rounded-3xl border border-dashed border-slate-200 bg-white px-5 py-6 text-sm text-slate-500">
还没有可预览的 Team。你可以从左侧模板中查看详情,或新建一个自定义 Team。
</div>
);
}
const isCurrent = selectedTeam?.id === inspectorTeam.id;
const isCustom = inspectorTeam.source === "custom";
const badgeLabel =
inspectorTeam.id === recommendedTeam?.id
? "按任务推荐"
: isCustom
? "自定义"
: "系统模板";
return (
<div
className="space-y-4 rounded-[28px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-950/5"
data-testid="team-selector-inspector"
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<span className="text-lg font-semibold text-slate-900">
{inspectorTeam.label}
</span>
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[11px] font-medium text-slate-500">
{badgeLabel}
</span>
{isCurrent ? (
<span className="inline-flex items-center gap-1 rounded-full border border-sky-200 bg-sky-50 px-2 py-0.5 text-[11px] font-medium text-sky-700">
<Check className="h-3.5 w-3.5" />
当前选择
</span>
) : null}
</div>
<div className="mt-2 text-sm leading-6 text-slate-600">
{inspectorTeam.description || "该 Team 将作为主代理拆分子任务时的协作参考。"}
</div>
</div>
<div className="flex flex-wrap gap-2">
{!isCurrent ? (
<Button
type="button"
className="bg-slate-900 text-white hover:bg-slate-800"
onClick={() => handleSelect(inspectorTeam)}
>
采用这个 Team
</Button>
) : null}
<Button
type="button"
variant="outline"
className="border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
onClick={handleClearSelection}
onClick={() => handleStartCreate(inspectorTeam)}
>
清除当前选择
<Copy className="mr-1.5 h-4 w-4" />
复制为自定义
</Button>
) : null}
{isCustom ? (
<Button
type="button"
variant="outline"
className="border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
onClick={() => handleStartEdit(inspectorTeam)}
>
<Pencil className="mr-1.5 h-4 w-4" />
编辑
</Button>
) : null}
</div>
</div>
{recommendedTeam ? (
<section className="space-y-2">
<div className="flex items-center gap-2 text-[11px] font-semibold tracking-[0.08em] text-slate-500">
<Sparkles className="h-3.5 w-3.5" />
推荐 Team
<div className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
<div className="text-xs font-semibold tracking-[0.08em] text-slate-500">
TEAM 概览
</div>
<div className="mt-2 text-sm leading-6 text-slate-700">
{buildTeamDefinitionSummary(inspectorTeam)}
</div>
</div>
<div className="space-y-3">
<div className="text-xs font-semibold tracking-[0.08em] text-slate-500">
角色分工
</div>
<div className="grid gap-3 xl:grid-cols-2">
{inspectorTeam.roles.map((role) => (
<div
key={`${inspectorTeam.id}-inspector-${role.id}`}
className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3"
>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold text-slate-900">
{role.label}
</span>
{role.profileId ? (
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[11px] text-slate-500">
画像 ·{" "}
{getBuiltinTeamProfileOption(role.profileId)?.label ||
role.profileId}
</span>
) : null}
{role.roleKey ? (
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[11px] text-slate-500">
Role · {role.roleKey}
</span>
) : null}
</div>
<div className="mt-2 text-sm leading-6 text-slate-600">
{role.summary}
</div>
{role.skillIds?.length ? (
<div className="mt-3 flex flex-wrap gap-1.5">
{role.skillIds.map((skillId) => (
<span
key={`${inspectorTeam.id}-${role.id}-${skillId}`}
className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[11px] text-slate-500"
>
技能 ·{" "}
{getBuiltinTeamSkillOption(skillId)?.label || skillId}
</span>
))}
</div>
) : null}
</div>
))}
</div>
</div>
{isCustom ? (
<div className="flex justify-end">
<Button
type="button"
variant="outline"
className="border-rose-200 bg-white text-rose-600 hover:bg-rose-50"
onClick={() => handleDeleteCustom(inspectorTeam)}
>
<Trash2 className="mr-1.5 h-4 w-4" />
删除这个 Team
</Button>
</div>
) : null}
</div>
);
};
const renderDraftEditor = () => {
if (!draft) {
return renderInspectorPanel();
}
return (
<section
className="space-y-4 rounded-[28px] border border-slate-200 bg-white p-5 shadow-sm shadow-slate-950/5"
data-testid="team-selector-editor"
>
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-base font-semibold text-slate-900">
{draft.id ? "编辑自定义 Team" : "新建自定义 Team"}
</div>
<TeamCard
team={recommendedTeam}
selected={recommendedSelected}
expanded={expandedTeamId === recommendedTeam.id}
selectedLabel="已采用推荐"
badgeLabel="按任务推荐"
onSelect={() => handleSelect(recommendedTeam)}
onToggleDetail={() =>
setExpandedTeamId((currentId) =>
currentId === recommendedTeam.id ? null : recommendedTeam.id,
<div className="mt-1 text-sm text-slate-500">
在右侧完整配置 Team 角色、画像和技能,左侧列表用于浏览与切换模板。
</div>
</div>
<button
type="button"
className="inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-100 hover:text-slate-700"
onClick={() => setDraft(null)}
aria-label="关闭编辑器"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="grid gap-3 md:grid-cols-2">
<div className="grid gap-2">
<label className="text-xs font-medium text-slate-600">
Team 名称
</label>
<Input
value={draft.label}
onChange={(event) =>
setDraft((current) =>
current
? {
...current,
label: event.target.value,
}
: current,
)
}
onCopy={() => handleStartCreate(recommendedTeam)}
placeholder="例如:前端联调团队"
className="border-slate-200 bg-white"
/>
</section>
) : null}
</div>
<section className="space-y-2">
<div className="flex items-center justify-between gap-2">
<div className="text-[11px] font-semibold tracking-[0.08em] text-slate-500">
我的 Team
</div>
<div className="grid gap-2 md:col-span-2">
<label className="text-xs font-medium text-slate-600">
Team 描述
</label>
<Textarea
value={draft.description}
onChange={(event) =>
setDraft((current) =>
current
? {
...current,
description: event.target.value,
}
: current,
)
}
placeholder="说明这个 Team 适合什么类型任务。"
className="min-h-[88px] border-slate-200 bg-white"
/>
</div>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<label className="text-xs font-medium text-slate-600">
角色分工
</label>
<Button
type="button"
variant="outline"
size="sm"
className="h-8 border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
onClick={() =>
handleStartCreate(
selectedTeam || recommendedTeam || builtinTeams[0] || null,
setDraft((current) =>
current
? {
...current,
roles: [
...current.roles,
{
id: `role-${current.roles.length + 1}`,
label: "",
summary: "",
skillIds: [],
},
],
}
: current,
)
}
>
<Plus className="mr-1.5 h-3.5 w-3.5" />
新建自定义
添加角色
</Button>
</div>
{filteredCustomTeams.length > 0 ? (
<div className="space-y-2">
{filteredCustomTeams.map((team) => (
<TeamCard
key={team.id}
team={team}
selected={selectedTeam?.id === team.id}
expanded={expandedTeamId === team.id}
badgeLabel="自定义"
onSelect={() => handleSelect(team)}
onToggleDetail={() =>
setExpandedTeamId((currentId) =>
currentId === team.id ? null : team.id,
)
}
onCopy={() => handleStartCreate(team)}
onEdit={() => handleStartEdit(team)}
onDelete={() => handleDeleteCustom(team)}
/>
))}
</div>
) : (
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-sm text-slate-500">
<div>还没有自定义 Team。可以从推荐方案或系统模板复制一份后再改。</div>
<Button
type="button"
variant="outline"
size="sm"
className="mt-3 border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
onClick={() =>
handleStartCreate(
selectedTeam || recommendedTeam || builtinTeams[0] || null,
)
}
<div className="space-y-3">
{draft.roles.map((role, index) => (
<div
key={`${role.id}-${index}`}
className="rounded-2xl border border-slate-200 bg-slate-50 p-4"
>
<Plus className="mr-1.5 h-3.5 w-3.5" />
立即创建
</Button>
</div>
)}
</section>
{(() => {
const selectedProfile = getBuiltinTeamProfileOption(
role.profileId,
);
const resolvedSkillIds = role.skillIds || [];
const suggestedSkillIds = selectedProfile?.skillIds || [];
<section className="space-y-2">
<div className="text-[11px] font-semibold tracking-[0.08em] text-slate-500">
系统模板
</div>
<div className="space-y-2">
{builtinTeams.map((team) => (
<TeamCard
key={team.id}
team={team}
selected={selectedTeam?.id === team.id}
expanded={expandedTeamId === team.id}
badgeLabel="系统"
onSelect={() => handleSelect(team)}
onToggleDetail={() =>
setExpandedTeamId((currentId) =>
currentId === team.id ? null : team.id,
)
}
onCopy={() => handleStartCreate(team)}
/>
))}
</div>
</section>
{draft ? (
<section className="space-y-3 rounded-[22px] border border-slate-200 bg-slate-50 p-3">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-sm font-semibold text-slate-900">
{draft.id ? "编辑自定义 Team" : "新建自定义 Team"}
</div>
<div className="mt-1 text-xs text-slate-500">
用于当前 Team mode 的角色分工建议,不会影响普通单代理任务。
</div>
</div>
<button
type="button"
className="inline-flex h-8 w-8 items-center justify-center rounded-full text-slate-400 transition hover:bg-white hover:text-slate-700"
onClick={() => setDraft(null)}
aria-label="关闭编辑器"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="grid gap-3">
<div className="grid gap-2">
<label className="text-xs font-medium text-slate-600">
Team 名称
</label>
<Input
value={draft.label}
onChange={(event) =>
setDraft((current) =>
current
? {
...current,
label: event.target.value,
}
: current,
)
}
placeholder="例如:前端联调团队"
className="border-slate-200 bg-white"
/>
</div>
<div className="grid gap-2">
<label className="text-xs font-medium text-slate-600">
Team 描述
</label>
<Textarea
value={draft.description}
onChange={(event) =>
setDraft((current) =>
current
? {
...current,
description: event.target.value,
}
: current,
)
}
placeholder="说明这个 Team 适合什么类型任务。"
className="min-h-[84px] border-slate-200 bg-white"
/>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<label className="text-xs font-medium text-slate-600">
角色分工
</label>
<Button
type="button"
variant="outline"
size="sm"
className="h-8 border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
onClick={() =>
setDraft((current) =>
current
? {
...current,
roles: [
...current.roles,
{
id: `role-${current.roles.length + 1}`,
label: "",
summary: "",
skillIds: [],
},
],
}
: current,
)
}
>
<Plus className="mr-1.5 h-3.5 w-3.5" />
添加角色
</Button>
</div>
<div className="space-y-3">
{draft.roles.map((role, index) => (
<div
key={`${role.id}-${index}`}
className="rounded-2xl border border-slate-200 bg-white p-3"
>
{(() => {
const selectedProfile = getBuiltinTeamProfileOption(
role.profileId,
);
const resolvedSkillIds = role.skillIds || [];
const suggestedSkillIds = selectedProfile?.skillIds || [];
return (
<>
<div className="mb-2 flex items-center justify-between gap-3">
return (
<>
<div className="mb-3 flex items-center justify-between gap-3">
<div className="text-xs font-medium text-slate-500">
角色 {index + 1}
</div>
<button
type="button"
className="inline-flex h-7 w-7 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-100 hover:text-slate-700"
className="inline-flex h-7 w-7 items-center justify-center rounded-full text-slate-400 transition hover:bg-white hover:text-slate-700"
onClick={() =>
setDraft((current) =>
current
@@ -780,9 +784,9 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
}))
}
placeholder="说明这个角色负责什么。"
className="min-h-[76px] border-slate-200 bg-white"
className="min-h-[84px] border-slate-200 bg-white"
/>
<div className="grid gap-3 md:grid-cols-2">
<div className="grid gap-3 xl:grid-cols-2">
<div className="grid gap-2">
<label className="text-xs font-medium text-slate-600">
内置画像
@@ -819,7 +823,7 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
</option>
))}
</select>
<div className="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] leading-5 text-slate-500">
<div className="rounded-xl border border-slate-200 bg-white px-3 py-2 text-[11px] leading-5 text-slate-500">
{selectedProfile ? (
<>
<span className="font-medium text-slate-700">
@@ -849,7 +853,7 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
placeholder="例如:explorer / executor / reviewer"
className="border-slate-200 bg-white"
/>
<div className="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] leading-5 text-slate-500">
<div className="rounded-xl border border-slate-200 bg-white px-3 py-2 text-[11px] leading-5 text-slate-500">
用于运行时和工作台标记角色职责;建议与所选画像保持一致。
</div>
</div>
@@ -897,12 +901,9 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
placeholder="多个 skill id 用逗号分隔,例如:source-grounding, structured-writing"
className="border-slate-200 bg-white"
/>
<div className="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-[11px] leading-5 text-slate-500">
<div className="rounded-xl border border-slate-200 bg-white px-3 py-2 text-[11px] leading-5 text-slate-500">
{selectedProfile && suggestedSkillIds.length > 0 ? (
<>
推荐技能:
{suggestedSkillIds.join("、")}
</>
<>推荐技能:{suggestedSkillIds.join("、")}</>
) : (
"skillIds 会透传给运行时,用于约束子代理的技能集。"
)}
@@ -910,33 +911,234 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
</div>
</div>
</>
);
})()}
</div>
))}
</div>
);
})()}
</div>
))}
</div>
</div>
<div className="flex items-center justify-end gap-2">
<div className="flex items-center justify-end gap-2">
<Button
type="button"
variant="outline"
className="border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
onClick={() => setDraft(null)}
>
取消
</Button>
<Button
type="button"
className="bg-slate-900 text-white hover:bg-slate-800"
onClick={handleSaveDraft}
>
保存 Team
</Button>
</div>
</section>
);
};
return (
<div
className="flex h-[min(78vh,760px)] w-full flex-col bg-white"
data-testid="team-selector-panel"
>
<div className="border-b border-slate-200/80 bg-[linear-gradient(180deg,rgb(255,255,255)_0%,rgb(248,250,252)_100%)] px-4 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[11px] font-semibold tracking-[0.08em] text-slate-500">
TEAM 配置
</div>
<div className="mt-1 text-sm text-slate-700">
只在当前任务适合拆分协作时,为主代理提供团队结构参考。
</div>
</div>
{selectedTeam ? (
<button
type="button"
className="inline-flex h-8 w-8 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-100 hover:text-slate-700"
onClick={handleClearSelection}
title="清除 Team 选择"
aria-label="清除 Team 选择"
>
<X className="h-4 w-4" />
</button>
) : null}
</div>
{selectedTeam ? (
<div
className="mt-3 rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2.5"
data-testid="team-selector-current"
>
<div className="flex items-center gap-2 text-xs font-medium text-slate-500">
<Users className="h-3.5 w-3.5" />
当前已选 Team
</div>
<div className="mt-1 text-sm font-semibold text-slate-900">
{selectedTeam.label}
</div>
{currentSelectionSummary ? (
<div className="mt-1 text-xs leading-5 text-slate-600">
{currentSelectionSummary}
</div>
) : null}
</div>
) : null}
</div>
<div className="grid min-h-0 flex-1 lg:grid-cols-[minmax(340px,420px)_minmax(0,1fr)]">
<div className="min-h-0 overflow-y-auto border-b border-slate-200/80 bg-white p-4 lg:border-b-0 lg:border-r">
<div className="space-y-4">
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="搜索 Team、角色或职责"
className="border-slate-200 bg-white"
/>
<div className="flex flex-wrap gap-2">
<Button
type="button"
className="bg-slate-900 text-white hover:bg-slate-800"
onClick={() =>
handleStartCreate(
selectedTeam || recommendedTeam || builtinTeams[0] || null,
)
}
>
<Plus className="mr-1.5 h-4 w-4" />
新建自定义 Team
</Button>
{selectedTeam ? (
<Button
type="button"
variant="outline"
className="border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
onClick={() => setDraft(null)}
onClick={handleClearSelection}
>
取消
清除当前选择
</Button>
) : null}
</div>
{recommendedTeam ? (
<section className="space-y-2">
<div className="flex items-center gap-2 text-[11px] font-semibold tracking-[0.08em] text-slate-500">
<Sparkles className="h-3.5 w-3.5" />
推荐 Team
</div>
<TeamCard
team={recommendedTeam}
selected={recommendedSelected}
expanded={expandedTeamId === recommendedTeam.id}
selectedLabel="已采用推荐"
badgeLabel="按任务推荐"
onSelect={() => handleSelect(recommendedTeam)}
onToggleDetail={() =>
setExpandedTeamId((currentId) =>
currentId === recommendedTeam.id
? null
: recommendedTeam.id,
)
}
onCopy={() => handleStartCreate(recommendedTeam)}
/>
</section>
) : null}
<section className="space-y-2">
<div className="flex items-center justify-between gap-2">
<div className="text-[11px] font-semibold tracking-[0.08em] text-slate-500">
我的 Team
</div>
<Button
type="button"
className="bg-slate-900 text-white hover:bg-slate-800"
onClick={handleSaveDraft}
variant="outline"
size="sm"
className="h-8 border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
onClick={() =>
handleStartCreate(
selectedTeam || recommendedTeam || builtinTeams[0] || null,
)
}
>
保存 Team
<Plus className="mr-1.5 h-3.5 w-3.5" />
新建自定义
</Button>
</div>
</div>
</section>
) : null}
{filteredCustomTeams.length > 0 ? (
<div className="space-y-2">
{filteredCustomTeams.map((team) => (
<TeamCard
key={team.id}
team={team}
selected={selectedTeam?.id === team.id}
expanded={expandedTeamId === team.id}
badgeLabel="自定义"
onSelect={() => handleSelect(team)}
onToggleDetail={() =>
setExpandedTeamId((currentId) =>
currentId === team.id ? null : team.id,
)
}
onCopy={() => handleStartCreate(team)}
onEdit={() => handleStartEdit(team)}
onDelete={() => handleDeleteCustom(team)}
/>
))}
</div>
) : (
<div className="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-3 py-4 text-sm text-slate-500">
<div>
还没有自定义 Team。可以从推荐方案或系统模板复制一份后再改。
</div>
<Button
type="button"
variant="outline"
size="sm"
className="mt-3 border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
onClick={() =>
handleStartCreate(
selectedTeam || recommendedTeam || builtinTeams[0] || null,
)
}
>
<Plus className="mr-1.5 h-3.5 w-3.5" />
立即创建
</Button>
</div>
)}
</section>
<section className="space-y-2">
<div className="text-[11px] font-semibold tracking-[0.08em] text-slate-500">
系统模板
</div>
<div className="space-y-2">
{builtinTeams.map((team) => (
<TeamCard
key={team.id}
team={team}
selected={selectedTeam?.id === team.id}
expanded={expandedTeamId === team.id}
badgeLabel="系统"
onSelect={() => handleSelect(team)}
onToggleDetail={() =>
setExpandedTeamId((currentId) =>
currentId === team.id ? null : team.id,
)
}
onCopy={() => handleStartCreate(team)}
/>
))}
</div>
</section>
</div>
</div>
<div className="min-h-0 overflow-y-auto bg-slate-50 p-5">
{renderDraftEditor()}
</div>
</div>
</div>
);
@@ -385,6 +385,76 @@ describe("useAsterAgentChat 首页新会话", () => {
harness.unmount();
}
});
it("当前执行会话确认不存在后应清空失效执行态并恢复到有效会话", async () => {
const workspaceId = "ws-topic-missing-not-found";
const missingSessionId = "session-live-gone";
const activeSessionId = "session-existing";
mockCreateAgentRuntimeSession.mockResolvedValue(missingSessionId);
mockListAgentRuntimeSessions
.mockResolvedValueOnce([])
.mockResolvedValueOnce([
{
id: activeSessionId,
name: "既有任务",
created_at: 1700000100,
updated_at: 1700000101,
messages_count: 1,
},
])
.mockResolvedValue([
{
id: activeSessionId,
name: "既有任务",
created_at: 1700000100,
updated_at: 1700000101,
messages_count: 1,
},
]);
mockGetAgentRuntimeSession.mockImplementation(async (sessionId: string) => {
if (sessionId === missingSessionId) {
throw new Error(`Session not found: ${missingSessionId}`);
}
return {
id: activeSessionId,
name: "既有任务",
created_at: 1700000100,
updated_at: 1700000101,
messages: [],
turns: [],
items: [],
queued_turns: [],
};
});
const harness = mountHook(workspaceId);
try {
await flushEffects();
await act(async () => {
await harness
.getValue()
.sendMessage("继续执行当前任务", [], false, false, false, "react");
});
await flushEffects();
await flushEffects();
await flushEffects();
await flushEffects();
expect(mockGetAgentRuntimeSession).toHaveBeenCalledWith(missingSessionId);
expect(harness.getValue().sessionId).toBe(activeSessionId);
expect(
harness.getValue().topics.some((topic) => topic.id === missingSessionId),
).toBe(false);
expect(harness.getValue().messages).toHaveLength(0);
} finally {
harness.unmount();
}
});
});
describe("useAsterAgentChat 任务快照", () => {