feat: 优化 Agent Studio 试运行交互

- 将内置工具配置拆分为独立页签

- 支持试运行面板平滑居中展开和快捷收起

- 运行和审批期间禁用会话清理
This commit is contained in:
2026-08-20 11:18:37 +08:00
parent fa07134cf8
commit 7d654c3302
5 changed files with 405 additions and 276 deletions

View File

@@ -1,24 +1,45 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import baseFormSource from './components/AgentBaseForm.vue?raw'; import builtinToolsFormSource from './components/AgentBuiltinToolsForm.vue?raw';
import inspectorSource from './components/AgentInspectorPanel.vue?raw'; import inspectorSource from './components/AgentInspectorPanel.vue?raw';
import tryoutPanelSource from './components/AgentTryoutPanel.vue?raw';
describe('agent Studio responsive layout contract', () => { describe('agent Studio responsive layout contract', () => {
it('keeps the inspector usable at the 768px breakpoint', () => { it('keeps the inspector usable at the 768px breakpoint', () => {
expect(inspectorSource).toContain('@media (max-width: 900px)'); expect(inspectorSource).toContain('@media (max-width: 900px)');
expect(inspectorSource).toMatch( expect(inspectorSource).toMatch(
/@media \(max-width: 900px\)[\s\S]*?left: var\(--space-4\);[\s\S]*?width: auto;/, /@media \(max-width: 900px\)[\s\S]*?inset:[\s\S]*?var\(--space-4\)[\s\S]*?96px;/,
); );
}); });
it('keeps the inspector and builtin tool controls usable at 375px', () => { it('keeps the inspector and builtin tool controls usable at 375px', () => {
expect(inspectorSource).toMatch( expect(inspectorSource).toMatch(
/@media \(max-width: 480px\)[\s\S]*?right: var\(--space-2\);[\s\S]*?left: var\(--space-2\);/, /@media \(max-width: 480px\)[\s\S]*?inset:[\s\S]*?var\(--space-2\)[\s\S]*?96px;/,
); );
expect(baseFormSource).toMatch( expect(builtinToolsFormSource).toMatch(
/@media \(max-width: 480px\)[\s\S]*?--agent-tool-approval-column: 72px;/, /@media \(max-width: 480px\)[\s\S]*?--agent-tool-approval-column: 72px;/,
); );
expect(baseFormSource).toContain('white-space: normal;'); expect(builtinToolsFormSource).toContain('white-space: normal;');
expect(baseFormSource).toContain('-webkit-line-clamp: 2;'); expect(builtinToolsFormSource).toContain('-webkit-line-clamp: 2;');
});
it('supports a centered animated tryout surface without remounting it', () => {
expect(inspectorSource).toContain('label="内置工具"');
expect(inspectorSource).toContain(':expanded="tryoutExpanded"');
expect(inspectorSource).toContain("'is-tryout-expanded'");
expect(inspectorSource).toContain('--motion-duration-medium');
expect(inspectorSource).toContain(
'@media (prefers-reduced-motion: reduce)',
);
expect(tryoutPanelSource).toContain(':aria-pressed="Boolean(expanded)"');
});
it('keeps clear disabled throughout active output and async cleanup', () => {
expect(tryoutPanelSource).toContain(
':disabled="loading || approvalLoading || clearing"',
);
expect(tryoutPanelSource).toContain(
'if (loading.value || approvalLoading.value || clearing.value)',
);
}); });
}); });

View File

@@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
/* eslint-disable vue/no-mutating-props */ /* eslint-disable vue/no-mutating-props */
import type { AgentBuiltinToolKey } from '../builtin-tools';
import type { AgentInfo, AgentOption, AgentToolBinding } from '../types'; import type { AgentInfo, AgentOption, AgentToolBinding } from '../types';
import { InfoFilled } from '@element-plus/icons-vue'; import { InfoFilled } from '@element-plus/icons-vue';
@@ -10,7 +9,6 @@ import {
ElIcon, ElIcon,
ElInput, ElInput,
ElInputNumber, ElInputNumber,
ElMessageBox,
ElOption, ElOption,
ElSelect, ElSelect,
ElSwitch, ElSwitch,
@@ -21,7 +19,6 @@ import { resolveAgentCompressionTokenThreshold } from '../compression-threshold'
const props = defineProps<{ const props = defineProps<{
agent: AgentInfo; agent: AgentInfo;
canDisableShellApproval: boolean;
categories: AgentOption[]; categories: AgentOption[];
models: AgentOption[]; models: AgentOption[];
toolBindings: AgentToolBinding[]; toolBindings: AgentToolBinding[];
@@ -35,92 +32,6 @@ const visibilityScopeOptions = [
{ label: '公开', value: 'PUBLIC' }, { label: '公开', value: 'PUBLIC' },
]; ];
const builtinToolOptions: Array<{
description: string;
key: AgentBuiltinToolKey;
label: string;
}> = [
{ description: '读取工作区内的 UTF-8 文本', key: 'read', label: '读取文件' },
{ description: '在工作区内写入或插入文本', key: 'write', label: '写入文件' },
{
description: '以补丁方式安全修改工作区文件',
key: 'patch',
label: '补丁修改',
},
{ description: '执行平台白名单中的受控命令', key: 'shell', label: 'Shell' },
{
description: '将工作区文件发布为可下载产物',
key: 'artifactPublish',
label: '发布产物',
},
];
function builtinTool(key: AgentBuiltinToolKey) {
return props.agent.executionConfigJson!.builtinTools![key];
}
function handleBuiltinToolEnabledChange(key: AgentBuiltinToolKey) {
if (key === 'shell' && !props.canDisableShellApproval) {
builtinTool('shell').approvalRequired = true;
delete props.agent.executionConfigJson!.builtinTools!
.shellApprovalRiskConfirmed;
}
emit('change');
}
async function confirmUnsafeShellExecution() {
try {
await ElMessageBox.confirm(
'关闭后,命令和脚本将以后端服务权限在宿主环境运行,当前没有沙箱隔离。确认继续?',
'关闭 Shell 调用确认',
{
cancelButtonText: '保持开启',
confirmButtonText: '确认关闭',
type: 'warning',
},
);
props.agent.executionConfigJson!.builtinTools!.shellApprovalRiskConfirmed = true;
return true;
} catch {
return false;
}
}
async function confirmBuiltinToolEnabledChange(key: AgentBuiltinToolKey) {
if (
key !== 'shell' ||
builtinTool('shell').enabled ||
builtinTool('shell').approvalRequired
) {
return true;
}
if (!props.canDisableShellApproval) {
builtinTool('shell').approvalRequired = true;
return true;
}
return confirmUnsafeShellExecution();
}
async function confirmShellApprovalChange() {
if (!props.canDisableShellApproval) {
return false;
}
if (!builtinTool('shell').approvalRequired) {
delete props.agent.executionConfigJson!.builtinTools!
.shellApprovalRiskConfirmed;
return true;
}
return confirmUnsafeShellExecution();
}
function handleApprovalChange(key: AgentBuiltinToolKey, value: unknown) {
if (key === 'shell' && value === true) {
delete props.agent.executionConfigJson!.builtinTools!
.shellApprovalRiskConfirmed;
}
emit('change');
}
function handleModelChange(modelId: AgentInfo['modelId']) { function handleModelChange(modelId: AgentInfo['modelId']) {
const selectedModel = props.models.find( const selectedModel = props.models.find(
(model) => model.value === String(modelId ?? ''), (model) => model.value === String(modelId ?? ''),
@@ -268,66 +179,6 @@ function handleModelChange(modelId: AgentInfo['modelId']) {
@input="emit('change')" @input="emit('change')"
/> />
</ElFormItem> </ElFormItem>
<section
class="agent-form__builtin-tools"
aria-labelledby="builtin-tools-title"
>
<div class="agent-form__section-head">
<div>
<div id="builtin-tools-title" class="agent-form__section-title">
内置工具
</div>
<div class="agent-form__section-description">
为当前智能体配置工作区能力
</div>
</div>
<div class="agent-form__tool-columns" aria-hidden="true">
<span>启用</span>
<span>调用前确认</span>
</div>
</div>
<div class="agent-form__tool-list">
<div
v-for="item in builtinToolOptions"
:key="item.key"
class="agent-form__tool-row"
>
<div class="agent-form__tool-copy">
<div class="agent-form__tool-name">{{ item.label }}</div>
<div class="agent-form__tool-description">
{{ item.description }}
</div>
</div>
<ElSwitch
v-model="builtinTool(item.key).enabled"
:aria-label="`启用${item.label}`"
:before-change="() => confirmBuiltinToolEnabledChange(item.key)"
@change="handleBuiltinToolEnabledChange(item.key)"
/>
<ElTooltip
:disabled="item.key !== 'shell' || canDisableShellApproval"
content="Shell 调用确认仅平台超级管理员可以关闭"
effect="light"
placement="top"
>
<span class="agent-form__tool-approval">
<ElSwitch
v-model="builtinTool(item.key).approvalRequired"
:aria-label="`${item.label}调用前确认`"
:before-change="
item.key === 'shell' ? confirmShellApprovalChange : undefined
"
:disabled="
!builtinTool(item.key).enabled ||
(item.key === 'shell' && !canDisableShellApproval)
"
@change="handleApprovalChange(item.key, $event)"
/>
</span>
</ElTooltip>
</div>
</div>
</section>
<div class="agent-form__grid"> <div class="agent-form__grid">
<ElFormItem> <ElFormItem>
<template #label> <template #label>
@@ -396,89 +247,6 @@ function handleModelChange(modelId: AgentInfo['modelId']) {
gap: 8px; gap: 8px;
} }
.agent-form__builtin-tools {
--agent-tool-approval-column: 96px;
--agent-tool-enabled-column: 48px;
margin-bottom: var(--space-4);
}
.agent-form__section-head {
display: grid;
grid-template-columns:
minmax(0, 1fr)
calc(var(--agent-tool-enabled-column) + var(--agent-tool-approval-column));
gap: var(--space-3);
align-items: end;
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--el-border-color-lighter);
}
.agent-form__section-title {
font-size: 14px;
font-weight: 600;
line-height: 22px;
color: var(--el-text-color-primary);
}
.agent-form__section-description,
.agent-form__tool-description,
.agent-form__tool-columns {
font-size: 12px;
line-height: 18px;
color: var(--el-text-color-secondary);
}
.agent-form__tool-columns {
display: grid;
grid-template-columns: var(--agent-tool-enabled-column) var(
--agent-tool-approval-column
);
text-align: center;
}
.agent-form__tool-list {
display: flex;
flex-direction: column;
}
.agent-form__tool-row {
display: grid;
grid-template-columns:
minmax(0, 1fr) var(--agent-tool-enabled-column)
var(--agent-tool-approval-column);
gap: var(--space-3);
align-items: center;
min-height: 56px;
border-bottom: 1px solid var(--el-border-color-extra-light);
}
.agent-form__tool-copy {
min-width: 0;
}
.agent-form__tool-name {
font-size: 13px;
font-weight: 500;
line-height: 20px;
color: var(--el-text-color-primary);
}
.agent-form__tool-description {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-form__tool-row > :deep(.el-switch),
.agent-form__tool-approval {
justify-self: center;
}
.agent-form__tool-approval {
display: inline-flex;
}
.agent-form__label { .agent-form__label {
display: inline-flex; display: inline-flex;
gap: 4px; gap: 4px;
@@ -510,26 +278,5 @@ function handleModelChange(modelId: AgentInfo['modelId']) {
.agent-form { .agent-form {
padding: var(--space-3); padding: var(--space-3);
} }
.agent-form__builtin-tools {
--agent-tool-approval-column: 72px;
}
.agent-form__section-head,
.agent-form__tool-row {
gap: var(--space-2);
}
.agent-form__tool-row {
min-height: calc(var(--space-8) * 2);
}
.agent-form__tool-description {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
}
} }
</style> </style>

View File

@@ -0,0 +1,261 @@
<script setup lang="ts">
import type { AgentBuiltinToolKey } from '../builtin-tools';
import type { AgentInfo } from '../types';
import { ElForm, ElMessageBox, ElSwitch, ElTooltip } from 'element-plus';
const props = defineProps<{
agent: AgentInfo;
canDisableShellApproval: boolean;
}>();
const emit = defineEmits<{ change: [] }>();
const builtinToolOptions: Array<{
description: string;
key: AgentBuiltinToolKey;
label: string;
}> = [
{ description: '读取工作区内的 UTF-8 文本', key: 'read', label: '读取文件' },
{ description: '在工作区内写入或插入文本', key: 'write', label: '写入文件' },
{
description: '以补丁方式安全修改工作区文件',
key: 'patch',
label: '补丁修改',
},
{ description: '执行平台白名单中的受控命令', key: 'shell', label: 'Shell' },
{
description: '将工作区文件发布为可下载产物',
key: 'artifactPublish',
label: '发布产物',
},
];
function builtinTool(key: AgentBuiltinToolKey) {
return props.agent.executionConfigJson!.builtinTools![key];
}
function handleBuiltinToolEnabledChange(key: AgentBuiltinToolKey) {
if (key === 'shell' && !props.canDisableShellApproval) {
builtinTool('shell').approvalRequired = true;
delete props.agent.executionConfigJson!.builtinTools!
.shellApprovalRiskConfirmed;
}
emit('change');
}
async function confirmUnsafeShellExecution() {
try {
await ElMessageBox.confirm(
'关闭后,命令和脚本将以后端服务权限在宿主环境运行,当前没有沙箱隔离。确认继续?',
'关闭 Shell 调用确认',
{
cancelButtonText: '保持开启',
confirmButtonText: '确认关闭',
type: 'warning',
},
);
props.agent.executionConfigJson!.builtinTools!.shellApprovalRiskConfirmed = true;
return true;
} catch {
return false;
}
}
async function confirmBuiltinToolEnabledChange(key: AgentBuiltinToolKey) {
if (
key !== 'shell' ||
builtinTool('shell').enabled ||
builtinTool('shell').approvalRequired
) {
return true;
}
if (!props.canDisableShellApproval) {
builtinTool('shell').approvalRequired = true;
return true;
}
return confirmUnsafeShellExecution();
}
async function confirmShellApprovalChange() {
if (!props.canDisableShellApproval) {
return false;
}
if (!builtinTool('shell').approvalRequired) {
delete props.agent.executionConfigJson!.builtinTools!
.shellApprovalRiskConfirmed;
return true;
}
return confirmUnsafeShellExecution();
}
function handleApprovalChange(key: AgentBuiltinToolKey, value: unknown) {
if (key === 'shell' && value === true) {
delete props.agent.executionConfigJson!.builtinTools!
.shellApprovalRiskConfirmed;
}
emit('change');
}
</script>
<template>
<ElForm class="agent-builtin-tools">
<section aria-labelledby="builtin-tools-description">
<div class="agent-builtin-tools__head">
<div
id="builtin-tools-description"
class="agent-builtin-tools__description"
>
为当前智能体配置工作区能力
</div>
<div class="agent-builtin-tools__columns" aria-hidden="true">
<span>启用</span>
<span>调用前确认</span>
</div>
</div>
<div class="agent-builtin-tools__list">
<div
v-for="item in builtinToolOptions"
:key="item.key"
class="agent-builtin-tools__row"
>
<div class="agent-builtin-tools__copy">
<div class="agent-builtin-tools__name">{{ item.label }}</div>
<div class="agent-builtin-tools__description">
{{ item.description }}
</div>
</div>
<ElSwitch
v-model="builtinTool(item.key).enabled"
:aria-label="`启用${item.label}`"
:before-change="() => confirmBuiltinToolEnabledChange(item.key)"
@change="handleBuiltinToolEnabledChange(item.key)"
/>
<ElTooltip
:disabled="item.key !== 'shell' || canDisableShellApproval"
content="Shell 调用确认仅平台超级管理员可以关闭"
effect="light"
placement="top"
>
<span class="agent-builtin-tools__approval">
<ElSwitch
v-model="builtinTool(item.key).approvalRequired"
:aria-label="`${item.label}调用前确认`"
:before-change="
item.key === 'shell' ? confirmShellApprovalChange : undefined
"
:disabled="
!builtinTool(item.key).enabled ||
(item.key === 'shell' && !canDisableShellApproval)
"
@change="handleApprovalChange(item.key, $event)"
/>
</span>
</ElTooltip>
</div>
</div>
</section>
</ElForm>
</template>
<style scoped>
.agent-builtin-tools {
--agent-tool-approval-column: 96px;
--agent-tool-enabled-column: 48px;
padding: var(--space-4);
}
.agent-builtin-tools__head {
display: grid;
grid-template-columns:
minmax(0, 1fr)
calc(var(--agent-tool-enabled-column) + var(--agent-tool-approval-column));
gap: var(--space-3);
align-items: end;
padding-bottom: var(--space-2);
border-bottom: 1px solid var(--el-border-color-lighter);
}
.agent-builtin-tools__description,
.agent-builtin-tools__columns {
font-size: 12px;
line-height: 18px;
color: var(--el-text-color-secondary);
}
.agent-builtin-tools__columns {
display: grid;
grid-template-columns: var(--agent-tool-enabled-column) var(
--agent-tool-approval-column
);
text-align: center;
}
.agent-builtin-tools__list {
display: flex;
flex-direction: column;
}
.agent-builtin-tools__row {
display: grid;
grid-template-columns:
minmax(0, 1fr) var(--agent-tool-enabled-column)
var(--agent-tool-approval-column);
gap: var(--space-3);
align-items: center;
min-height: 56px;
border-bottom: 1px solid var(--el-border-color-extra-light);
}
.agent-builtin-tools__copy {
min-width: 0;
}
.agent-builtin-tools__name {
font-size: 13px;
font-weight: 500;
line-height: 20px;
color: var(--el-text-color-primary);
}
.agent-builtin-tools__copy .agent-builtin-tools__description {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-builtin-tools__row > :deep(.el-switch),
.agent-builtin-tools__approval {
justify-self: center;
}
.agent-builtin-tools__approval {
display: inline-flex;
}
@media (max-width: 480px) {
.agent-builtin-tools {
--agent-tool-approval-column: 72px;
padding: var(--space-3);
}
.agent-builtin-tools__head,
.agent-builtin-tools__row {
gap: var(--space-2);
}
.agent-builtin-tools__row {
min-height: calc(var(--space-8) * 2);
}
.agent-builtin-tools__copy .agent-builtin-tools__description {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
}
}
</style>

View File

@@ -44,11 +44,13 @@ const emit = defineEmits<{
selectIssue: [nodeId: string]; selectIssue: [nodeId: string];
}>(); }>();
const loadAgentBuiltinToolsForm = () => import('./AgentBuiltinToolsForm.vue');
const loadAgentInteractionForm = () => import('./AgentInteractionForm.vue'); const loadAgentInteractionForm = () => import('./AgentInteractionForm.vue');
const loadAgentKnowledgeForm = () => import('./AgentKnowledgeForm.vue'); const loadAgentKnowledgeForm = () => import('./AgentKnowledgeForm.vue');
const loadAgentSkillInspector = () => import('./AgentSkillInspector.vue'); const loadAgentSkillInspector = () => import('./AgentSkillInspector.vue');
const loadAgentToolForm = () => import('./AgentToolForm.vue'); const loadAgentToolForm = () => import('./AgentToolForm.vue');
const loadAgentTryoutPanel = () => import('./AgentTryoutPanel.vue'); const loadAgentTryoutPanel = () => import('./AgentTryoutPanel.vue');
const AgentBuiltinToolsForm = defineAsyncComponent(loadAgentBuiltinToolsForm);
const AgentInteractionForm = defineAsyncComponent(loadAgentInteractionForm); const AgentInteractionForm = defineAsyncComponent(loadAgentInteractionForm);
const AgentKnowledgeForm = defineAsyncComponent(loadAgentKnowledgeForm); const AgentKnowledgeForm = defineAsyncComponent(loadAgentKnowledgeForm);
const AgentSkillInspector = defineAsyncComponent(loadAgentSkillInspector); const AgentSkillInspector = defineAsyncComponent(loadAgentSkillInspector);
@@ -65,8 +67,9 @@ const selectedKnowledge = computed(() => {
return props.state.knowledgeBindings.find((item) => item.localId === localId); return props.state.knowledgeBindings.find((item) => item.localId === localId);
}); });
const activeBaseTab = ref<'basic' | 'interaction'>('basic'); const activeBaseTab = ref<'basic' | 'builtinTools' | 'interaction'>('basic');
const interactionForm = ref<AgentInteractionFormExpose>(); const interactionForm = ref<AgentInteractionFormExpose>();
const tryoutExpanded = ref(false);
const selectedModel = computed(() => const selectedModel = computed(() =>
props.models.find((item) => item.value === String(props.state.agent.modelId)), props.models.find((item) => item.value === String(props.state.agent.modelId)),
); );
@@ -96,6 +99,19 @@ function handleMoveSkill(skillId: number | string | undefined, offset: -1 | 1) {
emit('moveSkill', skillId, offset); emit('moveSkill', skillId, offset);
} }
function handleCloseTryout() {
tryoutExpanded.value = false;
emit('closeTryout');
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key !== 'Escape' || !tryoutExpanded.value) {
return;
}
event.preventDefault();
tryoutExpanded.value = false;
}
watch( watch(
() => props.issues, () => props.issues,
(issues) => { (issues) => {
@@ -105,6 +121,16 @@ watch(
{ deep: true }, { deep: true },
); );
watch(
() => props.state.panelMode,
(panelMode) => {
if (panelMode !== 'tryout') {
tryoutExpanded.value = false;
}
},
{ flush: 'sync' },
);
const selectedTool = computed(() => { const selectedTool = computed(() => {
if (!props.state.selectedNodeId.startsWith('tool:')) return; if (!props.state.selectedNodeId.startsWith('tool:')) return;
const localId = props.state.selectedNodeId.slice('tool:'.length); const localId = props.state.selectedNodeId.slice('tool:'.length);
@@ -141,8 +167,10 @@ let prefetchHandle: number | undefined;
let prefetchTimer: number | undefined; let prefetchTimer: number | undefined;
onMounted(() => { onMounted(() => {
window.addEventListener('keydown', handleWindowKeydown);
const prefetchHiddenPanels = () => { const prefetchHiddenPanels = () => {
void Promise.allSettled([ void Promise.allSettled([
loadAgentBuiltinToolsForm(),
loadAgentInteractionForm(), loadAgentInteractionForm(),
loadAgentKnowledgeForm(), loadAgentKnowledgeForm(),
loadAgentSkillInspector(), loadAgentSkillInspector(),
@@ -160,6 +188,7 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('keydown', handleWindowKeydown);
if (prefetchHandle !== undefined) { if (prefetchHandle !== undefined) {
window.cancelIdleCallback(prefetchHandle); window.cancelIdleCallback(prefetchHandle);
} }
@@ -170,7 +199,13 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<aside class="agent-inspector"> <aside
class="agent-inspector"
:class="{
'is-tryout': state.panelMode === 'tryout',
'is-tryout-expanded': state.panelMode === 'tryout' && tryoutExpanded,
}"
>
<template v-if="state.panelMode === 'tryout'"> <template v-if="state.panelMode === 'tryout'">
<AgentTryoutPanel <AgentTryoutPanel
:agent="state.agent" :agent="state.agent"
@@ -178,7 +213,9 @@ onBeforeUnmount(() => {
:skill-bindings="state.skillBindings" :skill-bindings="state.skillBindings"
:tool-bindings="state.toolBindings" :tool-bindings="state.toolBindings"
:knowledge-bindings="state.knowledgeBindings" :knowledge-bindings="state.knowledgeBindings"
@close="emit('closeTryout')" :expanded="tryoutExpanded"
@close="handleCloseTryout"
@update:expanded="tryoutExpanded = $event"
/> />
</template> </template>
<template v-else> <template v-else>
@@ -221,7 +258,6 @@ onBeforeUnmount(() => {
<ElTabPane label="基础设置" name="basic"> <ElTabPane label="基础设置" name="basic">
<AgentBaseForm <AgentBaseForm
:agent="state.agent" :agent="state.agent"
:can-disable-shell-approval="canDisableShellApproval"
:categories="categories" :categories="categories"
:models="models" :models="models"
:tool-bindings="state.toolBindings" :tool-bindings="state.toolBindings"
@@ -235,6 +271,13 @@ onBeforeUnmount(() => {
@change="emit('change')" @change="emit('change')"
/> />
</ElTabPane> </ElTabPane>
<ElTabPane label="内置工具" name="builtinTools">
<AgentBuiltinToolsForm
:agent="state.agent"
:can-disable-shell-approval="canDisableShellApproval"
@change="emit('change')"
/>
</ElTabPane>
</ElTabs> </ElTabs>
</template> </template>
<AgentKnowledgeForm <AgentKnowledgeForm
@@ -267,14 +310,16 @@ onBeforeUnmount(() => {
<style scoped> <style scoped>
.agent-inspector { .agent-inspector {
--agent-inspector-expanded-width: 960px;
--agent-inspector-width: min(420px, calc(100vw - 320px));
position: absolute; position: absolute;
top: var(--space-6); inset: var(--space-6) var(--space-6) 96px
right: var(--space-6); calc(100% - var(--space-6) - var(--agent-inspector-width));
bottom: 96px;
z-index: 20; z-index: 20;
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: min(420px, calc(100vw - 320px));
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
background: var(--el-bg-color); background: var(--el-bg-color);
@@ -285,6 +330,27 @@ onBeforeUnmount(() => {
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
} }
.agent-inspector.is-tryout {
transition:
top var(--motion-duration-medium) var(--motion-ease-standard),
right var(--motion-duration-medium) var(--motion-ease-standard),
bottom var(--motion-duration-medium) var(--motion-ease-standard),
left var(--motion-duration-medium) var(--motion-ease-standard),
border-radius var(--motion-duration-medium) var(--motion-ease-standard),
box-shadow var(--motion-duration-medium) var(--motion-ease-standard);
}
.agent-inspector.is-tryout-expanded {
inset: var(--space-6)
max(
var(--space-6),
calc((100% - var(--agent-inspector-expanded-width)) / 2)
);
z-index: 40;
border-radius: var(--radius-panel);
box-shadow: var(--shadow-float);
}
.agent-inspector__header { .agent-inspector__header {
padding: var(--space-4); padding: var(--space-4);
border-bottom: 1px solid var(--el-border-color-lighter); border-bottom: 1px solid var(--el-border-color-lighter);
@@ -346,17 +412,23 @@ onBeforeUnmount(() => {
@media (max-width: 900px) { @media (max-width: 900px) {
.agent-inspector { .agent-inspector {
top: calc(var(--space-8) + var(--space-8) + var(--space-2)); inset: calc(var(--space-8) + var(--space-8) + var(--space-2)) var(--space-4)
right: var(--space-4); 96px;
left: var(--space-4); }
width: auto;
.agent-inspector.is-tryout-expanded {
inset: var(--space-4);
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
.agent-inspector { .agent-inspector {
right: var(--space-2); inset: calc(var(--space-8) + var(--space-8) + var(--space-2)) var(--space-2)
left: var(--space-2); 96px;
}
.agent-inspector.is-tryout-expanded {
inset: var(--space-2);
} }
.agent-inspector__header { .agent-inspector__header {
@@ -367,4 +439,10 @@ onBeforeUnmount(() => {
padding: var(--space-3) var(--space-3) 0; padding: var(--space-3) var(--space-3) 0;
} }
} }
@media (prefers-reduced-motion: reduce) {
.agent-inspector.is-tryout {
transition: none;
}
}
</style> </style>

View File

@@ -23,6 +23,7 @@ import {
import { BrushCleaning } from '@easyflow/icons'; import { BrushCleaning } from '@easyflow/icons';
import { copyTextToClipboard } from '@easyflow/utils'; import { copyTextToClipboard } from '@easyflow/utils';
import { FullScreen } from '@element-plus/icons-vue';
import { ElButton, ElMessage } from 'element-plus'; import { ElButton, ElMessage } from 'element-plus';
import AiChatPanel from '#/components/ai-chat/AiChatPanel.vue'; import AiChatPanel from '#/components/ai-chat/AiChatPanel.vue';
@@ -41,13 +42,17 @@ import AgentWelcomeState from './AgentWelcomeState.vue';
const props = defineProps<{ const props = defineProps<{
agent: AgentInfo; agent: AgentInfo;
expanded?: boolean;
imageEnabled?: boolean; imageEnabled?: boolean;
knowledgeBindings: AgentKnowledgeBinding[]; knowledgeBindings: AgentKnowledgeBinding[];
skillBindings: AgentSkillBinding[]; skillBindings: AgentSkillBinding[];
toolBindings: AgentToolBinding[]; toolBindings: AgentToolBinding[];
}>(); }>();
const emit = defineEmits<{ close: [] }>(); const emit = defineEmits<{
close: [];
'update:expanded': [value: boolean];
}>();
const { const {
loading, loading,
@@ -63,6 +68,7 @@ const {
stop, stop,
} = useAgentTryoutStream(); } = useAgentTryoutStream();
const approvalLoading = ref(false); const approvalLoading = ref(false);
const clearing = ref(false);
const composer = useAgentComposerDraft('DRAFT'); const composer = useAgentComposerDraft('DRAFT');
const loadDraftAgentArtifact = createAgentArtifactLoader(() => ({ const loadDraftAgentArtifact = createAgentArtifactLoader(() => ({
agentId: String(props.agent.id || ''), agentId: String(props.agent.id || ''),
@@ -225,6 +231,10 @@ function handleSelectNextVariant(item: ChatTimelineMessageItem) {
} }
async function handleClearSession() { async function handleClearSession() {
if (loading.value || approvalLoading.value || clearing.value) {
return;
}
clearing.value = true;
try { try {
await clearDraftSession(); await clearDraftSession();
await composer.clear(); await composer.clear();
@@ -232,6 +242,8 @@ async function handleClearSession() {
ElMessage.success('已清理会话'); ElMessage.success('已清理会话');
} catch (error) { } catch (error) {
ElMessage.error(error instanceof Error ? error.message : '清理会话失败'); ElMessage.error(error instanceof Error ? error.message : '清理会话失败');
} finally {
clearing.value = false;
} }
} }
@@ -396,13 +408,23 @@ async function handleReject(payload: ChatTimelineToolApprovalPayload) {
@close="emit('close')" @close="emit('close')"
> >
<template #headerActions> <template #headerActions>
<ElButton
:icon="FullScreen"
circle
text
:aria-label="expanded ? '收起试运行' : '展开试运行'"
:aria-pressed="Boolean(expanded)"
:title="expanded ? '收起试运行' : '展开试运行'"
@click="emit('update:expanded', !expanded)"
/>
<ElButton <ElButton
:icon="BrushCleaning" :icon="BrushCleaning"
circle circle
text text
:disabled="approvalLoading" :disabled="loading || approvalLoading || clearing"
:loading="clearing"
aria-label="清理会话" aria-label="清理会话"
title="清理会话" :title="loading ? '输出结束后可清理会话' : '清理会话'"
@click="handleClearSession" @click="handleClearSession"
/> />
</template> </template>