refactor(ui): unify employee selector into shared AgentPickerDialog

This commit is contained in:
matevip 2026-05-16 22:17:58 +08:00
parent 033854a3ff
commit 09eb28fb63
14 changed files with 602 additions and 577 deletions

View File

@ -39,58 +39,13 @@
</div>
<div class="form-group">
<label class="form-label">{{ t('channels.fields.bindAgent') }}</label>
<!-- Custom dropdown so SkillIcon renders pi:* glyphs; native
<select><option> can't host the component. Mirrors the
ChannelOnboardingWizard step-3 pattern. -->
<div class="agent-select" :class="{ open: agentDropdownOpen }">
<button
type="button"
class="agent-select-trigger"
@click="agentDropdownOpen = !agentDropdownOpen"
>
<span class="agent-select-trigger__icon" :style="{ color: agentIconColor(selectedAgent?.icon) }">
<SkillIcon :value="selectedAgent?.icon" :size="20" :fallback="'🤖'" />
</span>
<span class="agent-select-trigger__name">{{
selectedAgent?.name || t('channels.placeholders.selectAgent')
}}</span>
<svg
class="agent-select-trigger__arrow"
:class="{ open: agentDropdownOpen }"
width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
><polyline points="6 9 12 15 18 9"/></svg>
</button>
<Transition name="fade">
<div
v-if="agentDropdownOpen"
class="agent-dropdown-backdrop"
@click="agentDropdownOpen = false"
></div>
</Transition>
<Transition name="agent-dropdown">
<div v-if="agentDropdownOpen" class="agent-dropdown">
<div
v-for="a in agents"
:key="a.id"
class="agent-dropdown-item"
:class="{ active: String(a.id) === String(form.agentId) }"
@click="onSelectAgent(a)"
>
<span class="agent-dropdown-item__icon" :style="{ color: agentIconColor(a.icon) }">
<SkillIcon :value="a.icon" :size="18" :fallback="'🤖'" />
</span>
<span class="agent-dropdown-item__name">{{ a.name }}</span>
<span v-if="String(a.id) === String(form.agentId)" class="agent-dropdown-item__check">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</span>
</div>
<div v-if="agents.length === 0" class="agent-dropdown-empty">
{{ t('chat.loadingAgents') }}
</div>
</div>
</Transition>
</div>
<AgentPickerDialog
block
:model-value="form.agentId ?? null"
:agents="props.agents"
:placeholder="t('channels.placeholders.selectAgent')"
@update:model-value="(v) => (form.agentId = v ?? undefined)"
/>
</div>
</div>
@ -487,8 +442,7 @@ import { useWeixinQrcodePoll } from '@/composables/channels/useWeixinQrcodePoll'
import { useWecomBotAuth } from '@/composables/channels/useWecomBotAuth'
import { useFeishuAppRegister } from '@/composables/channels/useFeishuAppRegister'
import { useDingTalkAppRegister } from '@/composables/channels/useDingTalkAppRegister'
import SkillIcon from '@/components/common/SkillIcon.vue'
import { agentIconColor } from '@/utils/agentIconColor'
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
interface Props {
modelValue: boolean
@ -538,20 +492,6 @@ const showAdvanced = ref(false)
const accessControl = ref<AccessControlValue>(defaultAccessControl())
const renderConfig = ref<RenderConfigValue>(defaultRenderConfig())
// ========== Custom agent dropdown ==========
// Native <select><option> can't render <SkillIcon> for pi:* glyphs;
// mirror the ChatConsole.vue / ChannelOnboardingWizard.vue pattern.
const agentDropdownOpen = ref(false)
const selectedAgent = computed<Agent | null>(() => {
const id = form.value.agentId
if (id == null) return null
return props.agents.find(a => String(a.id) === String(id)) ?? null
})
function onSelectAgent(a: Agent) {
form.value.agentId = a.id as any
agentDropdownOpen.value = false
}
// ========== Auth composables ==========
const weixin = useWeixinQrcodePoll(({ botToken, baseUrl }) => {
@ -1019,69 +959,6 @@ function save() {
.json-hint { font-size: 12px; color: var(--mc-text-tertiary); margin: 0 0 8px; }
.json-editor { font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; font-size: 13px; line-height: 1.5; tab-size: 2; }
/* ===== Agent custom dropdown (mirrors ChatConsole.vue / wizard pattern) =====
Native <select><option> can't host <SkillIcon> for pi:* glyphs.
z-index has to clear the modal-overlay (1000). */
.agent-select { position: relative; }
.agent-select-trigger {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border: 1px solid var(--mc-border);
border-radius: 10px;
font-size: 14px;
color: var(--mc-text-primary);
background: var(--mc-bg-elevated);
cursor: pointer;
outline: none;
transition: border-color 0.15s;
box-sizing: border-box;
}
.agent-select-trigger:hover { border-color: var(--mc-primary); }
.agent-select.open .agent-select-trigger { border-color: var(--mc-primary); box-shadow: 0 0 0 3px rgba(217,119,87,0.12); }
.agent-select-trigger__icon { font-size: 18px; line-height: 1; flex-shrink: 0; display: inline-flex; }
.agent-select-trigger__name { flex: 1; text-align: left; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-select-trigger__arrow { flex-shrink: 0; color: var(--mc-text-tertiary); transition: transform 0.2s; }
.agent-select-trigger__arrow.open { transform: rotate(180deg); }
.agent-dropdown-backdrop { position: fixed; inset: 0; z-index: 1099; }
.agent-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 1100;
background: var(--mc-bg-elevated);
border: 1px solid var(--mc-border);
border-radius: 12px;
padding: 6px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
max-height: 280px;
overflow-y: auto;
}
.agent-dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.12s;
}
.agent-dropdown-item:hover { background: var(--mc-bg-sunken); }
.agent-dropdown-item.active { background: var(--mc-primary-bg); }
.agent-dropdown-item__icon { font-size: 20px; line-height: 1; flex-shrink: 0; display: inline-flex; }
.agent-dropdown-item__name { flex: 1; font-size: 14px; font-weight: 500; color: var(--mc-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-dropdown-item__check { flex-shrink: 0; color: var(--mc-primary); }
.agent-dropdown-empty { padding: 16px; text-align: center; font-size: 13px; color: var(--mc-text-tertiary); }
.agent-dropdown-enter-active { transition: all 0.15s ease-out; }
.agent-dropdown-leave-active { transition: all 0.1s ease-in; }
.agent-dropdown-enter-from { opacity: 0; transform: translateY(-6px) scale(0.97); }
.agent-dropdown-leave-to { opacity: 0; transform: translateY(-4px) scale(0.98); }
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

View File

@ -219,59 +219,16 @@
</div>
</div>
<!-- Bind agent (custom dropdown so SkillIcon renders pi:* glyphs;
native <select><option> cannot host components) -->
<!-- Bind agent -->
<div class="form-group">
<label class="form-label">{{ t('channels.wizard.bindAgentLabel') }}</label>
<div class="agent-select" :class="{ open: agentDropdownOpen }">
<button
type="button"
class="agent-select-trigger"
@click="agentDropdownOpen = !agentDropdownOpen"
>
<span class="agent-select-trigger__icon" :style="{ color: agentIconColor(selectedAgent?.icon) }">
<SkillIcon :value="selectedAgent?.icon" :size="20" :fallback="'🤖'" />
</span>
<span class="agent-select-trigger__name">{{
selectedAgent?.name || t('channels.placeholders.selectAgent')
}}</span>
<svg
class="agent-select-trigger__arrow"
:class="{ open: agentDropdownOpen }"
width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
><polyline points="6 9 12 15 18 9"/></svg>
</button>
<Transition name="fade">
<div
v-if="agentDropdownOpen"
class="agent-dropdown-backdrop"
@click="agentDropdownOpen = false"
></div>
</Transition>
<Transition name="agent-dropdown">
<div v-if="agentDropdownOpen" class="agent-dropdown">
<div
v-for="a in agents"
:key="a.id"
class="agent-dropdown-item"
:class="{ active: String(a.id) === String(form.agentId) }"
@click="onSelectAgent(a)"
>
<span class="agent-dropdown-item__icon" :style="{ color: agentIconColor(a.icon) }">
<SkillIcon :value="a.icon" :size="18" :fallback="'🤖'" />
</span>
<span class="agent-dropdown-item__name">{{ a.name }}</span>
<span v-if="String(a.id) === String(form.agentId)" class="agent-dropdown-item__check">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</span>
</div>
<div v-if="agents.length === 0" class="agent-dropdown-empty">
{{ t('chat.loadingAgents') }}
</div>
</div>
</Transition>
</div>
<AgentPickerDialog
block
:model-value="form.agentId ?? null"
:agents="props.agents"
:placeholder="t('channels.placeholders.selectAgent')"
@update:model-value="(v) => (form.agentId = v ?? undefined)"
/>
</div>
<p class="ready-hint">{{ t('channels.wizard.sendTestHint', { service: serviceName }) }}</p>
@ -324,8 +281,7 @@ import { useWecomBotAuth } from '@/composables/channels/useWecomBotAuth'
import { useWeixinQrcodePoll } from '@/composables/channels/useWeixinQrcodePoll'
import { useDingTalkAppRegister } from '@/composables/channels/useDingTalkAppRegister'
import { useFeishuAppRegister } from '@/composables/channels/useFeishuAppRegister'
import SkillIcon from '@/components/common/SkillIcon.vue'
import { agentIconColor } from '@/utils/agentIconColor'
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
interface Props {
modelValue: boolean
@ -377,19 +333,6 @@ const channelConfig = ref<Record<string, any>>({})
const visibleFields = ref<Record<string, boolean>>({})
const showAdvanced = ref(false)
// Custom agent dropdown native <select><option> can't render SkillIcon
// component for pi:* glyphs; mirror ChatConsole.vue's pattern.
const agentDropdownOpen = ref(false)
const selectedAgent = computed<Agent | null>(() => {
const id = form.value.agentId
if (id == null) return null
return props.agents.find(a => String(a.id) === String(id)) ?? null
})
function onSelectAgent(a: Agent) {
form.value.agentId = a.id as any
agentDropdownOpen.value = false
}
const verifying = ref(false)
const verifyResult = ref<VerifyResult | null>(null)
const invalidField = ref<string | null>(null)
@ -879,75 +822,6 @@ function formatIdentityKey(key: string): string {
.btn-secondary { padding: 10px 18px; background: var(--mc-bg-elevated); color: var(--mc-text-primary); border: 1px solid var(--mc-border); border-radius: 12px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.15s; }
.btn-secondary:hover { background: var(--mc-bg-sunken); }
/* ===== Agent custom dropdown (mirrors ChatConsole.vue pattern) =====
Native <select><option> can't render the <SkillIcon> component
needed for pi:* glyphs, so step-3 uses a custom dropdown.
z-index has to clear the wizard modal-overlay (z-index: 1000). */
.agent-select { position: relative; }
.agent-select-trigger {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border: 1px solid var(--mc-border);
border-radius: 10px;
font-size: 14px;
color: var(--mc-text-primary);
background: var(--mc-bg-elevated);
cursor: pointer;
outline: none;
transition: border-color 0.15s;
box-sizing: border-box;
}
.agent-select-trigger:hover { border-color: var(--mc-primary); }
.agent-select.open .agent-select-trigger { border-color: var(--mc-primary); box-shadow: 0 0 0 3px rgba(217,119,87,0.12); }
.agent-select-trigger__icon { font-size: 18px; line-height: 1; flex-shrink: 0; display: inline-flex; }
.agent-select-trigger__name { flex: 1; text-align: left; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-select-trigger__arrow { flex-shrink: 0; color: var(--mc-text-tertiary); transition: transform 0.2s; }
.agent-select-trigger__arrow.open { transform: rotate(180deg); }
.agent-dropdown-backdrop {
position: fixed;
inset: 0;
/* must clear modal-overlay (1000) so clicks fall through to backdrop, not modal */
z-index: 1099;
}
.agent-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 1100;
background: var(--mc-bg-elevated);
border: 1px solid var(--mc-border);
border-radius: 12px;
padding: 6px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
max-height: 280px;
overflow-y: auto;
}
.agent-dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.12s;
}
.agent-dropdown-item:hover { background: var(--mc-bg-sunken); }
.agent-dropdown-item.active { background: var(--mc-primary-bg); }
.agent-dropdown-item__icon { font-size: 20px; line-height: 1; flex-shrink: 0; display: inline-flex; }
.agent-dropdown-item__name { flex: 1; font-size: 14px; font-weight: 500; color: var(--mc-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-dropdown-item__check { flex-shrink: 0; color: var(--mc-primary); }
.agent-dropdown-empty { padding: 16px; text-align: center; font-size: 13px; color: var(--mc-text-tertiary); }
.agent-dropdown-enter-active { transition: all 0.15s ease-out; }
.agent-dropdown-leave-active { transition: all 0.1s ease-in; }
.agent-dropdown-enter-from { opacity: 0; transform: translateY(-6px) scale(0.97); }
.agent-dropdown-leave-to { opacity: 0; transform: translateY(-4px) scale(0.98); }
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

View File

@ -0,0 +1,514 @@
<template>
<div
class="mc-agent-picker"
:class="{ 'is-block': block, 'is-disabled': disabled }"
>
<button
type="button"
class="mc-agent-picker__trigger"
:class="{
'is-compact': compact,
'is-empty': !selectedAgent && !isUnknown,
'is-unknown': isUnknown,
'is-disabled': disabled,
}"
:disabled="disabled"
:title="triggerLabel"
@click="openDialog"
>
<SkillIcon
class="mc-agent-picker__trigger-icon"
:value="selectedAgent?.icon"
:size="compact ? 24 : 22"
fallback="🤖"
/>
<template v-if="!compact">
<span class="mc-agent-picker__trigger-name">{{ triggerLabel }}</span>
<span
v-if="clearable && (selectedAgent || isUnknown) && !disabled"
class="mc-agent-picker__clear"
role="button"
:aria-label="t('common.clear')"
@click.stop="clear"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</span>
<svg
class="mc-agent-picker__caret"
width="12" height="12" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</template>
</button>
<Teleport to="body">
<Transition name="mc-agent-picker-fade">
<div v-if="open" class="mc-agent-picker__overlay" @click.self="close">
<div class="mc-agent-picker__panel" role="dialog" aria-modal="true">
<div class="mc-agent-picker__head">
<h2 class="mc-agent-picker__title">{{ t('agentContext.selectAgent') }}</h2>
<button class="mc-agent-picker__close" :aria-label="t('common.close')" @click="close">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<div v-if="searchable" class="mc-agent-picker__search">
<svg
class="mc-agent-picker__search-icon"
width="14" height="14" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
<input
ref="searchRef"
v-model="query"
class="mc-agent-picker__search-input"
:placeholder="t('common.search')"
@keydown.esc.stop="close"
/>
</div>
<div class="mc-agent-picker__body">
<button
v-if="clearable && !query.trim()"
type="button"
class="mc-agent-picker__item mc-agent-picker__item--none"
:class="{ active: !selectedAgent && !isUnknown }"
@click="clear"
>
<span class="mc-agent-picker__none-icon"></span>
<div class="mc-agent-picker__item-info">
<span class="mc-agent-picker__item-name">{{ t('common.none') }}</span>
</div>
<svg
v-if="!selectedAgent && !isUnknown"
class="mc-agent-picker__check"
width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</button>
<button
v-for="agent in filteredAgents"
:key="agent.id"
type="button"
class="mc-agent-picker__item"
:class="{ active: isSelected(agent) }"
@click="select(agent)"
>
<SkillIcon
class="mc-agent-picker__item-icon"
:value="agent.icon"
:size="28"
fallback="🤖"
/>
<div class="mc-agent-picker__item-info">
<span class="mc-agent-picker__item-name">{{ agent.name }}</span>
<span v-if="subtitle(agent)" class="mc-agent-picker__item-desc">{{ subtitle(agent) }}</span>
</div>
<svg
v-if="isSelected(agent)"
class="mc-agent-picker__check"
width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2.5"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</button>
<div v-if="filteredAgents.length === 0" class="mc-agent-picker__empty">
{{ query.trim() ? t('common.noResults') : t('common.noOptions') }}
</div>
</div>
</div>
</div>
</Transition>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import SkillIcon from '@/components/common/SkillIcon.vue'
/** Minimal structural shape so this picker works with the full `Agent`
* entity as well as the lighter agent option lists (e.g. workflow steps). */
export interface PickableAgent {
id: string | number
name: string
icon?: string | null
description?: string
agentType?: string
title?: string
}
const props = withDefaults(defineProps<{
/** v-model value: the selected agent id. Kept as a string to preserve Snowflake precision. */
modelValue: string | number | null
/** Agents to choose from. */
agents: PickableAgent[]
/** Trigger label shown when no agent is selected. Defaults to agentContext.selectAgent. */
placeholder?: string
/** Render trigger in disabled state (no dialog). */
disabled?: boolean
/** Icon-only trigger — no name, no caret. For collapsed sidebars. */
compact?: boolean
/** Full-width trigger, for form-field contexts. */
block?: boolean
/** Allow clearing the selection (shows a ✕ on the trigger and a "none" row). */
clearable?: boolean
/** Label shown when modelValue is set but the agent is not in `agents`
* (e.g. a workflow step referencing a since-deleted employee). */
unknownLabel?: string
}>(), {
disabled: false,
compact: false,
block: false,
clearable: false,
})
const emit = defineEmits<{
'update:modelValue': [value: string | number | null]
change: [value: string | number | null, agent: PickableAgent | null]
}>()
const { t } = useI18n()
const open = ref(false)
const query = ref('')
const searchRef = ref<HTMLInputElement | null>(null)
const hasValue = computed(() =>
props.modelValue !== '' && props.modelValue !== null && props.modelValue !== undefined,
)
const selectedAgent = computed<PickableAgent | null>(() => {
if (!hasValue.value) return null
return props.agents.find(a => String(a.id) === String(props.modelValue)) || null
})
/** modelValue is set but resolves to no known agent the referenced
* employee was likely renamed or removed. */
const isUnknown = computed(() => hasValue.value && !selectedAgent.value)
const triggerLabel = computed(() => {
if (selectedAgent.value) return selectedAgent.value.name
if (isUnknown.value) return props.unknownLabel || String(props.modelValue)
return props.placeholder || t('agentContext.selectAgent')
})
/** Show the search box once the list is long enough to warrant scanning. */
const searchable = computed(() => props.agents.length > 6)
const filteredAgents = computed<PickableAgent[]>(() => {
const q = query.value.trim().toLowerCase()
if (!q) return props.agents
return props.agents.filter(a =>
`${a.name} ${a.description || ''} ${a.title || ''} ${a.agentType || ''}`.toLowerCase().includes(q),
)
})
function subtitle(agent: PickableAgent): string {
return agent.description || agent.title || agent.agentType || ''
}
function isSelected(agent: PickableAgent): boolean {
return String(agent.id) === String(props.modelValue)
}
function openDialog() {
if (props.disabled) return
open.value = true
}
function close() {
open.value = false
}
function select(agent: PickableAgent) {
emit('update:modelValue', agent.id)
emit('change', agent.id, agent)
close()
}
function clear() {
emit('update:modelValue', null)
emit('change', null, null)
close()
}
function onKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') close()
}
watch(open, (val) => {
if (val) {
query.value = ''
window.addEventListener('keydown', onKeydown)
nextTick(() => searchRef.value?.focus())
} else {
window.removeEventListener('keydown', onKeydown)
}
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', onKeydown)
})
</script>
<style scoped>
.mc-agent-picker {
display: inline-flex;
min-width: 0;
}
.mc-agent-picker.is-block {
display: flex;
width: 100%;
}
/* ===== Trigger ===== */
.mc-agent-picker__trigger {
display: flex;
align-items: center;
gap: 8px;
min-width: 240px;
padding: 9px 12px;
border: 1px solid var(--mc-border);
border-radius: 12px;
background: var(--mc-bg-elevated);
color: var(--mc-text-primary);
font-size: 14px;
cursor: pointer;
text-align: left;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.is-block .mc-agent-picker__trigger {
width: 100%;
min-width: 0;
}
.mc-agent-picker__trigger.is-compact {
min-width: 0;
width: 100%;
justify-content: center;
padding: 8px;
}
.mc-agent-picker__trigger:hover:not(.is-disabled) {
border-color: color-mix(in srgb, var(--mc-primary) 50%, var(--mc-border));
}
.mc-agent-picker__trigger:focus-visible {
outline: none;
border-color: var(--mc-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mc-primary) 15%, transparent);
}
.mc-agent-picker__trigger.is-disabled {
opacity: 0.55;
cursor: not-allowed;
}
.mc-agent-picker__trigger-icon {
flex-shrink: 0;
}
.mc-agent-picker__trigger-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mc-agent-picker__trigger.is-empty .mc-agent-picker__trigger-name {
color: var(--mc-text-tertiary);
}
.mc-agent-picker__trigger.is-unknown .mc-agent-picker__trigger-name {
color: var(--mc-warning, #f59e0b);
}
.mc-agent-picker__clear {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
color: var(--mc-text-tertiary);
transition: background 0.12s ease, color 0.12s ease;
}
.mc-agent-picker__clear:hover {
background: var(--mc-bg-sunken);
color: var(--mc-text-primary);
}
.mc-agent-picker__caret {
flex-shrink: 0;
color: var(--mc-text-tertiary);
}
/* ===== Dialog ===== */
.mc-agent-picker__overlay {
position: fixed;
inset: 0;
z-index: 2400;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(0, 0, 0, 0.4);
}
.mc-agent-picker__panel {
width: 100%;
max-width: 420px;
max-height: 72vh;
display: flex;
flex-direction: column;
background: var(--mc-bg-elevated);
border: 1px solid var(--mc-border);
border-radius: 18px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22);
animation: mc-agent-picker-pop 0.26s cubic-bezier(0.32, 0.72, 0, 1.2);
}
@keyframes mc-agent-picker-pop {
from { opacity: 0; transform: scale(0.94) translateY(6px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.mc-agent-picker-fade-enter-active,
.mc-agent-picker-fade-leave-active { transition: opacity 0.18s ease; }
.mc-agent-picker-fade-enter-from,
.mc-agent-picker-fade-leave-to { opacity: 0; }
.mc-agent-picker__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 20px 12px;
border-bottom: 1px solid var(--mc-border-light);
}
.mc-agent-picker__title {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--mc-text-primary);
}
.mc-agent-picker__close {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
border: none;
border-radius: 9px;
background: transparent;
color: var(--mc-text-tertiary);
cursor: pointer;
flex-shrink: 0;
transition: background 0.18s ease, color 0.18s ease;
}
.mc-agent-picker__close:hover { background: var(--mc-bg-sunken); color: var(--mc-text-primary); }
.mc-agent-picker__search {
display: flex;
align-items: center;
gap: 8px;
margin: 12px 16px 0;
padding: 7px 10px;
border: 1px solid var(--mc-border);
border-radius: 9px;
background: var(--mc-bg-sunken);
}
.mc-agent-picker__search-icon { color: var(--mc-text-tertiary); flex-shrink: 0; }
.mc-agent-picker__search-input {
flex: 1;
border: none;
outline: none;
background: transparent;
font-size: 13px;
color: var(--mc-text-primary);
}
.mc-agent-picker__search-input::placeholder { color: var(--mc-text-tertiary); }
.mc-agent-picker__body {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px;
}
.mc-agent-picker__body::-webkit-scrollbar { width: 4px; }
.mc-agent-picker__body::-webkit-scrollbar-thumb { background: var(--mc-border); border-radius: 2px; }
.mc-agent-picker__item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 1px solid transparent;
border-radius: 10px;
background: transparent;
cursor: pointer;
text-align: left;
transition: background 0.12s ease;
}
.mc-agent-picker__item:hover { background: var(--mc-bg-sunken); }
.mc-agent-picker__item.active {
background: var(--mc-primary-bg);
}
.mc-agent-picker__item-icon { flex-shrink: 0; }
.mc-agent-picker__none-icon {
flex-shrink: 0;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 15px;
color: var(--mc-text-tertiary);
}
.mc-agent-picker__item-info {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 0;
flex: 1;
}
.mc-agent-picker__item-name {
font-size: 13px;
font-weight: 600;
color: var(--mc-text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mc-agent-picker__item-desc {
font-size: 11.5px;
color: var(--mc-text-tertiary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mc-agent-picker__check { flex-shrink: 0; color: var(--mc-primary); }
.mc-agent-picker__empty {
padding: 28px 12px;
text-align: center;
font-size: 13px;
color: var(--mc-text-tertiary);
}
@media (max-width: 600px) {
.mc-agent-picker__panel { max-width: 100%; max-height: 86vh; }
}
</style>

View File

@ -39,23 +39,17 @@
<label class="panel-field" v-if="modeNeedsAgent">
<span class="field-label">{{ t('workflows.canvas.nodeAgent') }}</span>
<!-- Digital employee picker. Agent steps persist agentId so the
workflow cannot drift when an employee is renamed. -->
<div class="agent-picker">
<select
class="mc-input"
:value="agentSelectValue"
@change="onAgentSelect"
>
<option value="">{{ t('workflows.canvas.fields.agentPlaceholder') }}</option>
<option v-for="a in availableAgents" :key="a.id" :value="String(a.id)">
{{ a.name }}<template v-if="a.title"> {{ a.title }}</template>
</option>
<option v-if="agentNotInList" :value="agentSelectValue" disabled>
{{ t('workflows.canvas.fields.agentMissing', { name: step.agentName || step.agentId }) }}
</option>
</select>
</div>
<!-- Agent steps persist agentId so the workflow cannot drift when an
employee is renamed. -->
<AgentPickerDialog
block
clearable
:model-value="agentModelValue"
:agents="availableAgents"
:placeholder="t('workflows.canvas.fields.agentPlaceholder')"
:unknown-label="t('workflows.canvas.fields.agentMissing', { name: step?.agentName || step?.agentId })"
@change="onAgentPicked"
/>
</label>
<label class="panel-field" v-if="modeNeedsAgent">
@ -280,6 +274,7 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import type { RawStep } from '@/composables/useWorkflowGraph'
import AgentPickerDialog, { type PickableAgent } from '@/components/common/AgentPickerDialog.vue'
/** Minimal agent shape the panel needs to render the picker kept
* inside the component so the panel doesn't have to import the
@ -288,6 +283,8 @@ export interface AgentOption {
id: number | string
name: string
title?: string
icon?: string
description?: string
}
export interface ChannelOption {
id: number | string
@ -387,32 +384,25 @@ function onDuplicate() {
emit('duplicate', { index: props.index })
}
const agentNotInList = computed(() => {
if (props.step?.agentId != null) {
return !props.availableAgents.some((a) => String(a.id) === String(props.step?.agentId))
}
const n = props.step?.agentName
if (!n) return false
return !props.availableAgents.some((a) => a.name === n)
})
const agentSelectValue = computed(() => {
if (props.step?.agentId != null) return String(props.step.agentId)
// Resolve the picker's selected id. Newer steps persist agentId; legacy
// steps stored only agentName, so fall back to a name lookup.
const agentModelValue = computed<string | number | null>(() => {
if (props.step?.agentId != null) return props.step.agentId
const byName = props.availableAgents.find((a) => a.name === props.step?.agentName)
return byName ? String(byName.id) : (props.step?.agentName ?? '')
if (byName) return byName.id
return props.step?.agentName ?? null
})
function onAgentSelect(e: Event) {
const v = (e.target as HTMLSelectElement).value
if (!v) {
function onAgentPicked(value: string | number | null, agent: PickableAgent | null) {
if (value == null) {
patch({ agentId: undefined, agentName: undefined })
return
}
// Keep the id as the raw string from the option value Snowflake IDs
// exceed Number.MAX_SAFE_INTEGER, so Number(v) would silently truncate.
const selected = props.availableAgents.find((a) => String(a.id) === v)
// Keep the id as a string Snowflake IDs exceed Number.MAX_SAFE_INTEGER,
// so a numeric round-trip would silently truncate.
patch({
agentId: v,
// Kept as a denormalized label for the canvas node; runtime resolves by agentId.
agentName: selected?.name,
agentId: String(value),
// Denormalized label for the canvas node; runtime resolves by agentId.
agentName: agent?.name,
})
}
@ -588,12 +578,6 @@ function onDispatchTargetInput(channelType: string, raw: string) {
font-family: 'JetBrains Mono', Consolas, monospace;
font-size: 11.5px;
}
.agent-picker {
display: flex;
align-items: center;
gap: 4px;
}
.agent-picker .mc-input { flex: 1; min-width: 0; }
.channel-picker {
display: grid;
gap: 6px;

View File

@ -26,6 +26,7 @@ export default {
search: 'Search',
noResults: 'No matches',
noOptions: 'No options',
none: 'None',
clear: 'Clear',
expandSidebar: 'Expand sidebar',
collapseSidebar: 'Collapse sidebar',
@ -994,7 +995,7 @@ export default {
kicker: 'Context Studio',
title: 'Agent Context',
desc: 'Manage prompt files and memory for agents',
selectAgent: 'Select Agent',
selectAgent: 'Select Employee',
noAgent: 'Please select an agent first',
files: 'Files',
coreFiles: 'Core Files',
@ -3258,7 +3259,7 @@ export default {
kicker: 'Memory',
title: 'Memory',
desc: 'What your agent thinks about, remembers, and lets go.',
selectAgent: 'Select Agent',
selectAgent: 'Select Employee',
selectAgentHint: 'Select an agent to view its memory',
tabTimeline: 'Timeline',
tabMemory: 'Long-term',

View File

@ -26,6 +26,7 @@ export default {
search: '搜索',
noResults: '没有匹配项',
noOptions: '暂无可选项',
none: '不指定',
clear: '清除',
expandSidebar: '展开侧边栏',
collapseSidebar: '折叠侧边栏',
@ -886,7 +887,7 @@ export default {
kicker: '上下文工作面',
title: '智能体上下文',
desc: '管理智能体的提示文件和记忆',
selectAgent: '选择智能体',
selectAgent: '选择员工',
noAgent: '请先选择一个智能体',
files: '文件列表',
coreFiles: '核心文件',
@ -3350,7 +3351,7 @@ export default {
kicker: '记忆',
title: '记忆',
desc: '你的智能体在想什么、记住了什么、遗忘了什么。',
selectAgent: '选择智能体',
selectAgent: '选择员工',
selectAgentHint: '请先选择一个智能体查看其记忆',
tabTimeline: '整理时间线',
tabMemory: '长期记忆',

View File

@ -4,7 +4,7 @@ import { http } from '@/api'
export interface DreamReportItem {
id: string
agentId: number
agentId: string | number
mode: string
topic: string | null
triggerSource: string
@ -28,7 +28,7 @@ export const useMemoryStore = defineStore('memory', () => {
const currentReport = ref<DreamReportItem | null>(null)
let pollTimer: ReturnType<typeof setInterval> | null = null
async function fetchReports(agentId: number, page = 1, size = 20) {
async function fetchReports(agentId: string | number, page = 1, size = 20) {
loading.value = true
error.value = null
try {
@ -46,7 +46,7 @@ export const useMemoryStore = defineStore('memory', () => {
}
}
async function fetchReport(agentId: number, reportId: string) {
async function fetchReport(agentId: string | number, reportId: string) {
loading.value = true
try {
const res = await http.get(`/memory/${agentId}/dream/reports/${reportId}`)
@ -65,7 +65,7 @@ export const useMemoryStore = defineStore('memory', () => {
* SSE via EventSource doesn't support Authorization headers,
* causing 401 errors. Polling every 15s is sufficient for dream events.
*/
function subscribeEvents(agentId: number) {
function subscribeEvents(agentId: string | number) {
unsubscribeEvents()
pollTimer = setInterval(() => {
fetchReports(agentId, 1, 20)

View File

@ -6,12 +6,7 @@
<div class="workspace-header workspace-header--compact">
<h1 class="page-title page-title--compact">{{ t('agentContext.title') }}</h1>
<div class="header-actions mc-surface-card">
<select v-model="selectedAgentId" class="agent-select" @change="onAgentChange">
<option value="" disabled>{{ t('agentContext.selectAgent') }}</option>
<option v-for="agent in agents" :key="agent.id" :value="agent.id">
{{ plainTextIcon(agent.icon) }} {{ agent.name }}
</option>
</select>
<AgentPickerDialog v-model="selectedAgentId" :agents="agents" />
</div>
</div>
@ -334,7 +329,7 @@ import { copyToClipboard } from '@/utils/clipboard'
import type { Agent, WorkspaceFile } from '@/types/index'
import { useMarkdownRenderer } from '@/composables/useMarkdownRenderer'
import { handleMermaidDownload } from '@/composables/useMermaidRenderer'
import { plainTextIcon } from '@/composables/usePixelarticons'
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
const { renderMarkdown } = useMarkdownRenderer()
@ -452,10 +447,6 @@ async function loadAgents() {
}
}
function onAgentChange() {
// watch
}
async function fetchFiles() {
if (!selectedAgentId.value) return
try {
@ -737,8 +728,6 @@ function formatTime(time?: string): string {
.page-title--compact { font-size: clamp(24px, 2.5vw, 32px); font-weight: 800; letter-spacing: -0.03em; margin: 0; }
.page-desc { font-size: 15px; color: var(--mc-text-secondary); margin: 0; line-height: 1.7; max-width: 720px; }
.header-actions { display: flex; gap: 8px; align-items: center; padding: 12px; border-radius: 18px; flex-shrink: 0; }
.agent-select { padding: 10px 14px; border: 1px solid var(--mc-border); border-radius: 12px; background: var(--mc-bg-elevated); color: var(--mc-text-primary); font-size: 14px; outline: none; min-width: 240px; cursor: pointer; }
.agent-select:focus { border-color: var(--mc-primary); }
/* 空状态 */
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; text-align: center; }
@ -1047,7 +1036,8 @@ function formatTime(time?: string): string {
min-width: 0;
}
.agent-select {
.header-actions :deep(.mc-agent-picker),
.header-actions :deep(.mc-agent-picker__trigger) {
width: 100%;
min-width: 0;
}

View File

@ -25,35 +25,14 @@
</button>
<div class="agent-selector">
<button class="agent-select-trigger" @click="agentDropdownOpen = !agentDropdownOpen" :title="$t('chat.selectAgent')">
<span class="agent-select-trigger__icon" :style="{ color: agentIconColor(currentAgent?.icon) }"><SkillIcon :value="currentAgent?.icon" :size="24" :fallback="'🤖'" /></span>
<span v-if="!convPanelCollapsed || isMobile" class="agent-select-trigger__name">{{ currentAgent?.name || $t('chat.selectAgent') }}</span>
<svg v-if="!convPanelCollapsed || isMobile" class="agent-select-trigger__arrow" :class="{ open: agentDropdownOpen }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<Transition name="fade">
<div v-if="agentDropdownOpen" class="agent-dropdown-backdrop" @click="agentDropdownOpen = false"></div>
</Transition>
<Transition name="agent-dropdown">
<div v-if="agentDropdownOpen" class="agent-dropdown">
<div
v-for="agent in agents"
:key="agent.id"
class="agent-dropdown-item"
:class="{ active: String(agent.id) === String(selectedAgentId) }"
@click="selectAgent(agent)"
>
<span class="agent-dropdown-item__icon" :style="{ color: agentIconColor(agent.icon) }"><SkillIcon :value="agent.icon" :size="18" :fallback="'🤖'" /></span>
<div class="agent-dropdown-item__info">
<span class="agent-dropdown-item__name">{{ agent.name }}</span>
<span class="agent-dropdown-item__desc">{{ agentTagline(agent) }}</span>
</div>
<span v-if="String(agent.id) === String(selectedAgentId)" class="agent-dropdown-item__check">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</span>
</div>
<div v-if="agents.length === 0" class="agent-dropdown-empty">{{ $t('chat.loadingAgents') }}</div>
</div>
</Transition>
<AgentPickerDialog
block
:model-value="selectedAgentId"
:agents="agents"
:compact="convPanelCollapsed && !isMobile"
:placeholder="$t('chat.selectAgent')"
@change="onAgentPicked"
/>
</div>
<div class="conversation-list">
@ -356,6 +335,7 @@ import type { Conversation, Agent, ModelConfig, ProviderInfo, ActiveModelsInfo,
import MessageList from '@/components/chat/MessageList.vue'
import RecoverableModelBanner from '@/components/chat/RecoverableModelBanner.vue'
import SkillIcon from '@/components/common/SkillIcon.vue'
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
import { parsePrompt, deriveTagline } from '@/utils/agentPromptProfile'
import { agentIconColor } from '@/utils/agentIconColor'
import ChatInput from '@/components/chat/ChatInput.vue'
@ -467,14 +447,12 @@ const thinkingLevel = computed(() => thinkingEnabled.value ? 'high' : 'off')
watch(thinkingEnabled, (v) => localStorage.setItem('mateclaw_thinking', v ? 'on' : 'off'))
// Dropdowns & menus
const agentDropdownOpen = ref(false)
const headerMenuOpen = ref(false)
function selectAgent(agent: Agent) {
agentDropdownOpen.value = false
if (String(agent.id) !== String(selectedAgentId.value)) {
selectedAgentId.value = agent.id
function onAgentPicked(value: string | number | null) {
if (value == null) return
if (String(value) !== String(selectedAgentId.value)) {
selectedAgentId.value = value
newConversation()
}
}
@ -1557,8 +1535,6 @@ async function clearMessages() {
}
}
// onAgentChange removed replaced by selectAgent()
// onModelChange removed replaced by selectModel()
function goToModelSettings(providerId?: string) {
@ -2090,19 +2066,6 @@ function handleCodeCopy(e: MouseEvent) {
padding: 10px 6px 12px;
}
.conversation-panel.conv-collapsed .agent-select-trigger {
justify-content: center;
padding: 8px;
}
.conversation-panel.conv-collapsed .agent-dropdown {
position: fixed;
top: auto;
left: 62px;
right: auto;
min-width: 260px;
}
.conversation-panel.conv-collapsed .conv-item {
justify-content: center;
padding: 10px 6px;
@ -2187,52 +2150,6 @@ function handleCodeCopy(e: MouseEvent) {
position: relative;
}
.agent-select-trigger {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid var(--mc-border);
border-radius: 12px;
font-size: 13px;
color: var(--mc-text-primary);
background: var(--mc-bg-sunken);
cursor: pointer;
outline: none;
transition: all 0.15s;
}
.agent-select-trigger:hover {
border-color: var(--mc-primary);
background: var(--mc-bg-elevated);
}
.agent-select-trigger__icon {
font-size: 18px;
line-height: 1;
}
.agent-select-trigger__name {
flex: 1;
text-align: left;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agent-select-trigger__arrow {
flex-shrink: 0;
color: var(--mc-text-tertiary);
transition: transform 0.2s;
}
.agent-select-trigger__arrow.open {
transform: rotate(180deg);
}
.agent-dropdown-backdrop,
.model-dropdown-backdrop,
.header-menu-backdrop {
position: fixed;
@ -2240,80 +2157,6 @@ function handleCodeCopy(e: MouseEvent) {
z-index: 99;
}
.agent-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 12px;
right: 12px;
min-width: 240px;
z-index: 100;
background: var(--mc-bg-elevated);
border: 1px solid var(--mc-border);
border-radius: 14px;
padding: 6px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
max-height: 320px;
overflow-y: auto;
}
.agent-dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 10px;
cursor: pointer;
transition: background 0.12s;
}
.agent-dropdown-item:hover {
background: var(--mc-bg-sunken);
}
.agent-dropdown-item.active {
background: var(--mc-primary-bg);
}
.agent-dropdown-item__icon {
font-size: 24px;
line-height: 1;
flex-shrink: 0;
}
.agent-dropdown-item__info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.agent-dropdown-item__name {
font-size: 13px;
font-weight: 600;
color: var(--mc-text-primary);
}
.agent-dropdown-item__desc {
font-size: 11px;
color: var(--mc-text-tertiary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agent-dropdown-item__check {
flex-shrink: 0;
color: var(--mc-primary);
}
.agent-dropdown-empty {
padding: 16px;
text-align: center;
font-size: 13px;
color: var(--mc-text-tertiary);
}
.agent-dropdown-enter-active {
transition: all 0.15s ease-out;
}

View File

@ -75,7 +75,7 @@ import { mcToast } from '@/composables/useMcToast'
import { http } from '@/api'
import FactTrustBar from './FactTrustBar.vue'
const props = defineProps<{ agentId: number }>()
const props = defineProps<{ agentId: string | number }>()
const { t } = useI18n()
const facts = ref<any[]>([])

View File

@ -66,7 +66,7 @@ import { useI18n } from 'vue-i18n'
import { mcToast } from '@/composables/useMcToast'
import { http, agentContextApi } from '@/api'
const props = defineProps<{ agentId: number }>()
const props = defineProps<{ agentId: string | number }>()
const { t } = useI18n()
interface FileInfo { filename: string; fileSize: number; enabled: boolean }

View File

@ -22,7 +22,7 @@ import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { http } from '@/api'
const props = defineProps<{ agentId: number }>()
const props = defineProps<{ agentId: string | number }>()
const { t } = useI18n()
const card = ref<any>(null)

View File

@ -14,36 +14,15 @@
</button>
</div>
<!-- Agent selector (ChatConsole pattern) -->
<!-- Employee selector -->
<div class="agent-selector">
<button class="agent-select-trigger" @click="agentDropdownOpen = !agentDropdownOpen">
<span class="agent-select-trigger__icon"><SkillIcon :value="currentAgent?.icon" :size="24" :fallback="'🧠'" /></span>
<span class="agent-select-trigger__name">{{ currentAgent?.name || t('memory.selectAgent') }}</span>
<svg class="agent-select-trigger__arrow" :class="{ open: agentDropdownOpen }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<Transition name="fade">
<div v-if="agentDropdownOpen" class="agent-dropdown-backdrop" @click="agentDropdownOpen = false"></div>
</Transition>
<Transition name="agent-dropdown">
<div v-if="agentDropdownOpen" class="agent-dropdown">
<div
v-for="agent in agents"
:key="agent.id"
class="agent-dropdown-item"
:class="{ active: agent.id === selectedAgentId }"
@click="selectAgent(agent)"
>
<span class="agent-dropdown-item__icon"><SkillIcon :value="agent.icon" :size="18" :fallback="'🤖'" /></span>
<div class="agent-dropdown-item__info">
<span class="agent-dropdown-item__name">{{ agent.name }}</span>
<span class="agent-dropdown-item__desc">{{ agent.description || agent.agentType }}</span>
</div>
<span v-if="agent.id === selectedAgentId" class="agent-dropdown-item__check">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</span>
</div>
</div>
</Transition>
<AgentPickerDialog
v-model="selectedAgentId"
:agents="agents"
block
:placeholder="t('memory.selectAgent')"
@change="onAgentPicked"
/>
</div>
<!-- Tab nav (segment control) -->
@ -184,7 +163,7 @@ import { mcToast } from '@/composables/useMcToast'
import { http } from '@/api'
import { useAgentStore } from '@/stores/useAgentStore'
import { useMemoryStore, type DreamReportItem } from '@/stores/useMemoryStore'
import SkillIcon from '@/components/common/SkillIcon.vue'
import AgentPickerDialog from '@/components/common/AgentPickerDialog.vue'
import MorningCard from './components/MorningCard.vue'
import FactList from './components/FactList.vue'
import MemoryBrowser from './components/MemoryBrowser.vue'
@ -194,9 +173,7 @@ const agentStore = useAgentStore()
const store = useMemoryStore()
const agents = ref<any[]>([])
const selectedAgentId = ref<number | null>(null)
const currentAgent = computed(() => agents.value.find(a => a.id === selectedAgentId.value))
const agentDropdownOpen = ref(false)
const selectedAgentId = ref<string | number>('')
const activeTab = ref('timeline')
const selectedReportId = ref<string | null>(null)
const currentPage = ref(1)
@ -227,9 +204,7 @@ watch(selectedAgentId, (id) => {
onUnmounted(() => store.unsubscribeEvents())
function selectAgent(agent: any) {
selectedAgentId.value = agent.id
agentDropdownOpen.value = false
function onAgentPicked() {
selectedReportId.value = null
currentPage.value = 1
store.currentReport = null
@ -332,36 +307,8 @@ function fmtTime(iso: string) {
background: var(--mc-bg-elevated);
}
/* Agent selector (same CSS as ChatConsole) */
/* Employee selector */
.agent-selector { position: relative; padding: 0 12px 8px; }
.agent-select-trigger {
width: 100%; display: flex; align-items: center; gap: 8px;
padding: 8px 12px; border: 1px solid var(--mc-border); border-radius: 12px;
font-size: 13px; color: var(--mc-text-primary); background: var(--mc-bg-sunken);
cursor: pointer; outline: none; transition: all 0.15s;
}
.agent-select-trigger:hover { border-color: var(--mc-primary); background: var(--mc-bg-elevated); }
.agent-select-trigger__icon { font-size: 18px; line-height: 1; }
.agent-select-trigger__name { flex: 1; text-align: left; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-select-trigger__arrow { flex-shrink: 0; color: var(--mc-text-tertiary); transition: transform 0.2s; }
.agent-select-trigger__arrow.open { transform: rotate(180deg); }
.agent-dropdown-backdrop { position: fixed; inset: 0; z-index: 99; }
.agent-dropdown {
position: absolute; top: calc(100% + 4px); left: 12px; right: 12px; min-width: 240px; z-index: 100;
background: var(--mc-bg-elevated); border: 1px solid var(--mc-border); border-radius: 14px;
padding: 6px; box-shadow: 0 8px 32px rgba(0,0,0,0.12); max-height: 320px; overflow-y: auto;
}
.agent-dropdown-item {
display: flex; align-items: center; gap: 10px; padding: 10px 12px;
border-radius: 10px; cursor: pointer; transition: background 0.12s;
}
.agent-dropdown-item:hover { background: var(--mc-bg-sunken); }
.agent-dropdown-item.active { background: var(--mc-primary-bg); }
.agent-dropdown-item__icon { font-size: 24px; line-height: 1; flex-shrink: 0; }
.agent-dropdown-item__info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.agent-dropdown-item__name { font-size: 13px; font-weight: 500; color: var(--mc-text-primary); }
.agent-dropdown-item__desc { font-size: 11px; color: var(--mc-text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-dropdown-item__check { color: var(--mc-primary); flex-shrink: 0; }
/* Segment control */
.memory-nav {
@ -504,12 +451,6 @@ function fmtTime(iso: string) {
.detail-empty p { font-size: 14px; max-width: 260px; line-height: 1.5; }
/* ========== Transitions ========== */
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.agent-dropdown-enter-active { transition: all 0.2s ease; }
.agent-dropdown-leave-active { transition: all 0.12s ease; }
.agent-dropdown-enter-from { opacity: 0; transform: translateY(-6px) scale(0.97); }
.agent-dropdown-leave-to { opacity: 0; transform: translateY(-4px) scale(0.98); }
.slide-down-enter-active, .slide-down-leave-active { transition: all 0.2s ease; }
.slide-down-enter-from, .slide-down-leave-to { opacity: 0; transform: translateY(-8px); }

View File

@ -279,7 +279,7 @@ const resumingId = ref<number | null>(null)
// Workspace agent list fed to the StepPropertyPanel's agent picker
// so authors stop typing free-form agent names that don't actually
// exist. Loaded once on mount and on workspace switch.
interface AgentOption { id: number; name: string; title?: string }
interface AgentOption { id: number; name: string; title?: string; icon?: string; description?: string }
const availableAgents = ref<AgentOption[]>([])
interface ChannelOption {
id: number | string
@ -607,7 +607,7 @@ async function reloadAgents() {
// via the X-Workspace-Id header, so no client-side filter is needed.
availableAgents.value = rows
.filter((a) => a && a.name)
.map((a) => ({ id: a.id, name: a.name, title: a.title }))
.map((a) => ({ id: a.id, name: a.name, title: a.title, icon: a.icon, description: a.description }))
} catch (e) {
console.error('listAgents failed', e)
}