GroupManager.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. <script setup lang="ts">
  2. import { computed, reactive, ref, watch } from 'vue'
  3. import type { GroupAgent, ModelOption } from '@shared/types'
  4. import AppIcon from '@/components/common/AppIcon.vue'
  5. import AgentAvatar from '@/components/common/AgentAvatar.vue'
  6. import type { UiRoom } from './types'
  7. import type { GroupProfileOption } from './types'
  8. type AgentSettingsPatch = Partial<Pick<GroupAgent,
  9. 'displayName' | 'description' | 'enabled' | 'replyWithoutMention' | 'isHost' | 'model' | 'provider' | 'reasoningEffort' | 'fastMode'>>
  10. type FastModeDraft = '' | 'true' | 'false'
  11. interface AgentDraft {
  12. displayName: string
  13. description: string
  14. enabled: boolean
  15. replyWithoutMention: boolean
  16. modelKey: string
  17. reasoningEffort: string
  18. fastMode: FastModeDraft
  19. }
  20. const REASONING_OPTIONS = [
  21. { value: '', label: '跟随 Profile 默认' },
  22. { value: 'none', label: '关闭' },
  23. { value: 'minimal', label: '最小' },
  24. { value: 'low', label: '低' },
  25. { value: 'medium', label: '中' },
  26. { value: 'high', label: '高' },
  27. { value: 'xhigh', label: '极高' },
  28. { value: 'max', label: '最大' },
  29. { value: 'ultra', label: 'Ultra' },
  30. ]
  31. const props = withDefaults(defineProps<{
  32. room: UiRoom
  33. agents: GroupAgent[]
  34. hostEnabled?: boolean
  35. hostFlowEnabled?: boolean
  36. roomInstructionsEnabled?: boolean
  37. availableProfiles?: GroupProfileOption[]
  38. modelOptionsByProfile?: Record<string, ModelOption[]>
  39. modelOptionsLoading?: Record<string, boolean>
  40. modelOptionsError?: Record<string, string>
  41. remoteServerAddresses?: Record<string, string>
  42. agentUpdateError?: Record<string, string>
  43. agentAvatars?: Record<string, string>
  44. managerError?: string
  45. busy?: boolean
  46. }>(), { hostEnabled: false, hostFlowEnabled: false, roomInstructionsEnabled: false, busy: false, agentAvatars: () => ({}) })
  47. const emit = defineEmits<{
  48. updateRoom: [patch: Partial<UiRoom>]
  49. addAgent: [profile: string]
  50. loadModels: [profile: string]
  51. clearAgentError: [id: string]
  52. updateAgent: [id: string, patch: AgentSettingsPatch]
  53. removeAgent: [id: string]
  54. interruptAgent: [id: string]
  55. archiveRoom: []
  56. }>()
  57. const form = reactive({
  58. name: props.room.name,
  59. instructions: props.room.instructions ?? '',
  60. replyRounds: props.room.replyRounds ?? 3,
  61. orchestrationMode: props.room.orchestrationMode ?? 'free' as 'free' | 'host',
  62. })
  63. const expandedAgentId = ref('')
  64. const agentDrafts = reactive<Record<string, AgentDraft>>({})
  65. const dirtyAgents = reactive(new Set<string>())
  66. const selectedAgent = computed(() => props.agents.find(agent => agent.id === expandedAgentId.value))
  67. const hostAgent = computed(() => props.agents.find(agent => agent.isHost))
  68. const hasActiveAgents = computed(() => props.agents.some(agent => ['queued', 'running', 'awaiting_input'].includes(agent.status)))
  69. function modelKey(provider?: string | null, model?: string | null): string {
  70. return provider && model ? JSON.stringify([provider, model]) : ''
  71. }
  72. function parseModelKey(value: string): { provider: string; model: string } | null {
  73. if (!value) return null
  74. try {
  75. const parsed = JSON.parse(value) as unknown
  76. if (Array.isArray(parsed) && parsed.length === 2 && parsed.every(item => typeof item === 'string' && item.trim())) {
  77. return { provider: parsed[0]!, model: parsed[1]! }
  78. }
  79. } catch { /* malformed select values fall back to the Profile default */ }
  80. return null
  81. }
  82. function draftFrom(agent: GroupAgent): AgentDraft {
  83. return {
  84. displayName: agent.displayName,
  85. description: agent.description,
  86. enabled: agent.enabled,
  87. replyWithoutMention: agent.replyWithoutMention,
  88. modelKey: modelKey(agent.provider, agent.model),
  89. reasoningEffort: agent.reasoningEffort ?? '',
  90. fastMode: agent.fastMode == null ? '' : agent.fastMode ? 'true' : 'false',
  91. }
  92. }
  93. watch(() => props.room, room => {
  94. form.name = room.name
  95. form.instructions = room.instructions ?? ''
  96. form.replyRounds = room.replyRounds ?? 3
  97. form.orchestrationMode = room.orchestrationMode ?? 'free'
  98. }, { deep: true })
  99. watch(() => props.agents, agents => {
  100. const ids = new Set(agents.map(agent => agent.id))
  101. if (expandedAgentId.value && !ids.has(expandedAgentId.value)) expandedAgentId.value = ''
  102. for (const id of Object.keys(agentDrafts)) if (!ids.has(id)) delete agentDrafts[id]
  103. for (const agent of agents) {
  104. if (!agentDrafts[agent.id] || !dirtyAgents.has(agent.id)) {
  105. agentDrafts[agent.id] = draftFrom(agent)
  106. } else if (!Object.keys(agentPatch(agent)).length) {
  107. dirtyAgents.delete(agent.id)
  108. agentDrafts[agent.id] = draftFrom(agent)
  109. }
  110. }
  111. }, { deep: true, immediate: true })
  112. function saveRoom() {
  113. const replyRounds = Number(form.replyRounds)
  114. emit('updateRoom', {
  115. name: form.name.trim() || props.room.name,
  116. ...(props.roomInstructionsEnabled ? { instructions: form.instructions.trim() } : {}),
  117. replyRounds: replyRounds === -1 ? -1 : Math.min(100, Math.max(1, replyRounds || 1)),
  118. ...(props.hostFlowEnabled ? { orchestrationMode: form.orchestrationMode } : {}),
  119. })
  120. }
  121. function selectHost(event: Event) {
  122. const id = (event.target as HTMLSelectElement).value
  123. if (!id || id === hostAgent.value?.id) return
  124. emit('updateAgent', id, { isHost: true })
  125. }
  126. function canRemoveAgent(agent: GroupAgent): boolean {
  127. if (props.agents.length <= 1) return false
  128. if (!props.hostEnabled || !agent.isHost) return true
  129. return props.agents.some(candidate => candidate.id !== agent.id && candidate.enabled)
  130. }
  131. function isRemoteAgent(agent: GroupAgent): boolean { return agent.nodeId !== 'local' }
  132. function removeAgentTitle(agent: GroupAgent): string {
  133. return canRemoveAgent(agent) ? '移除' : agent.isHost ? '需要另一位已启用成员才能移除主持人' : '团队必须保留至少一位成员'
  134. }
  135. function openAgentSettings(agent: GroupAgent) {
  136. expandedAgentId.value = agent.id
  137. if (!dirtyAgents.has(agent.id)) agentDrafts[agent.id] = draftFrom(agent)
  138. emit('loadModels', agent.profile)
  139. }
  140. function closeAgentSettings() { expandedAgentId.value = '' }
  141. function markDirty(agentId: string) {
  142. dirtyAgents.add(agentId)
  143. emit('clearAgentError', agentId)
  144. }
  145. function resetAgent(agent: GroupAgent) {
  146. agentDrafts[agent.id] = draftFrom(agent)
  147. dirtyAgents.delete(agent.id)
  148. emit('clearAgentError', agent.id)
  149. }
  150. function modelOptionsFor(agent: GroupAgent): ModelOption[] {
  151. const options = props.modelOptionsByProfile?.[agent.profile] ?? []
  152. if (!agent.model || !agent.provider || options.some(option => option.id === agent.model && option.provider === agent.provider)) return options
  153. return [{ id: agent.model, name: agent.model, provider: agent.provider }, ...options]
  154. }
  155. function fastModeValue(value: FastModeDraft): boolean | null {
  156. return value === '' ? null : value === 'true'
  157. }
  158. function agentPatch(agent: GroupAgent): AgentSettingsPatch {
  159. const draft = agentDrafts[agent.id]
  160. if (!draft) return {}
  161. const patch: AgentSettingsPatch = {}
  162. const displayName = draft.displayName.trim()
  163. const description = draft.description.trim()
  164. if (isRemoteAgent(agent) && displayName !== agent.displayName) patch.displayName = displayName
  165. if (description !== agent.description) patch.description = description
  166. if (draft.enabled !== agent.enabled) patch.enabled = draft.enabled
  167. if (draft.replyWithoutMention !== agent.replyWithoutMention) patch.replyWithoutMention = draft.replyWithoutMention
  168. if (draft.modelKey !== modelKey(agent.provider, agent.model)) {
  169. const selected = parseModelKey(draft.modelKey)
  170. patch.model = selected?.model ?? null
  171. patch.provider = selected?.provider ?? null
  172. }
  173. if (draft.reasoningEffort !== (agent.reasoningEffort ?? '')) patch.reasoningEffort = draft.reasoningEffort || null
  174. const nextFastMode = fastModeValue(draft.fastMode)
  175. if (nextFastMode !== (agent.fastMode ?? null)) patch.fastMode = nextFastMode
  176. return patch
  177. }
  178. function hasAgentChanges(agent: GroupAgent): boolean {
  179. return Object.keys(agentPatch(agent)).length > 0
  180. }
  181. function saveAgent(agent: GroupAgent) {
  182. const patch = agentPatch(agent)
  183. if (!Object.keys(patch).length) return
  184. emit('updateAgent', agent.id, patch)
  185. }
  186. function statusLabel(status: GroupAgent['status']): string {
  187. return { idle: '空闲', queued: '等待中', running: '正在回复', awaiting_input: '等待输入', unknown: '离线' }[status]
  188. }
  189. </script>
  190. <template>
  191. <div class="group-manager">
  192. <header>
  193. <span><small>团队管理</small><strong>{{ room.name }}</strong></span>
  194. </header>
  195. <section>
  196. <h3>团队设置</h3>
  197. <label><span>名称</span><input v-model="form.name" maxlength="80" @change="saveRoom" /></label>
  198. <label v-if="roomInstructionsEnabled"><span>说明<small>所有 Agent 都会在回复前查阅,可填写协作规则和形式准则。</small></span><textarea v-model="form.instructions" maxlength="4000" rows="5" aria-label="团队说明" placeholder="例如:先核对事实;结论使用中文;发布前等待确认。" @change="saveRoom" /></label>
  199. <label class="rounds"><span>最多回复轮数<small>-1 表示无限</small></span><input v-model.number="form.replyRounds" type="number" min="-1" max="100" aria-label="最多回复轮数" @change="saveRoom" /></label>
  200. <label v-if="hostFlowEnabled" class="flow-mode">
  201. <span>协作模式<small>主持人可按依赖逐步调度,也可一次 @ 多人并列执行。</small></span>
  202. <select v-model="form.orchestrationMode" aria-label="协作模式" :disabled="busy || hasActiveAgents" :title="hasActiveAgents ? '请等待当前回复完成或先中断 Agent' : ''" @change="saveRoom">
  203. <option value="free">自由讨论</option>
  204. <option value="host">主持流程</option>
  205. </select>
  206. </label>
  207. </section>
  208. <section>
  209. <div class="section-heading"><h3>成员 <em>{{ agents.length }}/8</em></h3></div>
  210. <label v-if="hostEnabled" class="host-selector">
  211. <span>主持人<small>用户没有明确 @ 时始终负责回应;“无需 @ 也回复”仍独立生效。</small></span>
  212. <select :value="hostAgent?.id || ''" aria-label="主持人" :disabled="busy" @change="selectHost">
  213. <option v-for="agent in agents" :key="agent.id" :value="agent.id" :disabled="!agent.enabled && !agent.isHost">{{ agent.displayName }}</option>
  214. </select>
  215. </label>
  216. <p v-if="managerError" class="agent-save-error" role="alert">{{ managerError }}</p>
  217. <div class="agent-list">
  218. <article v-for="agent in agents" :key="agent.id">
  219. <span class="agent-avatar"><AgentAvatar :name="agent.displayName" :avatar="agent.nodeId === 'local' ? agentAvatars[agent.profile] || '' : ''" :size="32" /><i :class="`status-${agent.status}`" /></span>
  220. <span class="agent-copy"><span class="agent-name-line"><strong>{{ agent.displayName }}</strong><em v-if="hostEnabled && agent.isHost" class="host-badge">主持人</em></span><small>{{ agent.profile }}<template v-if="agent.nodeId !== 'local'"> · {{ agent.nodeLabel || agent.nodeId.slice(0, 8) }}</template> · {{ statusLabel(agent.status) }}</small></span>
  221. <button class="agent-action" type="button" :title="`设置 ${agent.displayName}`" :aria-label="`设置${agent.displayName}`" @click="openAgentSettings(agent)"><AppIcon name="settings" :size="14" /></button>
  222. <button v-if="agent.status === 'running' || agent.status === 'queued'" class="agent-action" type="button" title="中断" @click="emit('interruptAgent', agent.id)"><AppIcon name="stop" :size="13" /></button>
  223. <button class="agent-action danger" type="button" :title="removeAgentTitle(agent)" :aria-label="`移除${agent.displayName}`" :disabled="!canRemoveAgent(agent) || busy" @click="emit('removeAgent', agent.id)"><AppIcon name="trash" :size="14" /></button>
  224. </article>
  225. </div>
  226. <div v-if="agents.length < 8 && availableProfiles?.length" class="add-agent">
  227. <select aria-label="选择要添加的 Agent" @change="($event.target as HTMLSelectElement).value && emit('addAgent', ($event.target as HTMLSelectElement).value); ($event.target as HTMLSelectElement).value = ''">
  228. <option value="">添加 Agent…</option>
  229. <option v-for="profile in availableProfiles" :key="profile.id" :value="profile.id">{{ profile.displayName }} · {{ profile.nodeLabel }}</option>
  230. </select>
  231. </div>
  232. </section>
  233. <section class="danger-zone">
  234. <div><strong>归档团队</strong><p>归档后不再出现在活跃列表,历史消息仍会保留。</p></div>
  235. <button class="quiet-button" type="button" :disabled="busy" @click="emit('archiveRoom')"><AppIcon name="archive" :size="14" />归档</button>
  236. </section>
  237. </div>
  238. <Teleport to="body">
  239. <div v-if="selectedAgent && agentDrafts[selectedAgent.id]" class="agent-settings-backdrop" @click.self="closeAgentSettings">
  240. <section class="agent-settings-dialog" role="dialog" aria-modal="true" :aria-label="`${selectedAgent.displayName} Agent 设置`" @keydown.esc="closeAgentSettings">
  241. <header>
  242. <span>
  243. <small>Agent 设置</small>
  244. <strong class="settings-agent-name">{{ selectedAgent.displayName }}<em v-if="hostEnabled && selectedAgent.isHost" class="host-badge">主持人</em></strong>
  245. </span>
  246. <button class="agent-settings-close" type="button" aria-label="关闭 Agent 设置" title="关闭 Agent 设置" @click="closeAgentSettings"><AppIcon name="close" :size="17" /></button>
  247. </header>
  248. <fieldset class="agent-editor">
  249. <legend class="sr-only">{{ selectedAgent.displayName }} Agent 设置</legend>
  250. <div class="editor-grid">
  251. <label v-if="isRemoteAgent(selectedAgent)"><span>群内名称<small>仅影响此群,会同步到其他设备;不修改远端 Agent 名称。</small></span><input v-model="agentDrafts[selectedAgent.id].displayName" maxlength="100" aria-label="群内名称" @input="markDirty(selectedAgent.id)" /></label>
  252. <label><span>职责说明</span><textarea v-model="agentDrafts[selectedAgent.id].description" rows="3" maxlength="500" aria-label="职责说明" @input="markDirty(selectedAgent.id)" /></label>
  253. <label>
  254. <span>模型</span>
  255. <select v-model="agentDrafts[selectedAgent.id].modelKey" aria-label="模型" @focus="emit('loadModels', selectedAgent.profile)" @change="markDirty(selectedAgent.id)">
  256. <option value="">跟随 Profile 默认</option>
  257. <option v-for="option in modelOptionsFor(selectedAgent)" :key="modelKey(option.provider, option.id)" :value="modelKey(option.provider, option.id)">{{ option.name }} · {{ option.provider }}</option>
  258. </select>
  259. <small v-if="modelOptionsLoading?.[selectedAgent.profile]" class="editor-note">正在加载模型选项…</small>
  260. <small v-else-if="modelOptionsError?.[selectedAgent.profile]" class="editor-note error">{{ modelOptionsError[selectedAgent.profile] }}</small>
  261. </label>
  262. <label>
  263. <span>推理强度</span>
  264. <select v-model="agentDrafts[selectedAgent.id].reasoningEffort" aria-label="推理强度" @change="markDirty(selectedAgent.id)">
  265. <option v-for="option in REASONING_OPTIONS" :key="option.value || 'default'" :value="option.value">{{ option.label }}</option>
  266. </select>
  267. </label>
  268. <label>
  269. <span>快速模式</span>
  270. <select v-model="agentDrafts[selectedAgent.id].fastMode" aria-label="快速模式" @change="markDirty(selectedAgent.id)">
  271. <option value="">跟随 Profile 默认</option>
  272. <option value="true">开启</option>
  273. <option value="false">关闭</option>
  274. </select>
  275. </label>
  276. </div>
  277. <p v-if="isRemoteAgent(selectedAgent) && remoteServerAddresses?.[selectedAgent.nodeId]" class="remote-agent-address">远程地址 · {{ remoteServerAddresses[selectedAgent.nodeId] }}</p>
  278. <div class="editor-toggles">
  279. <label><input v-model="agentDrafts[selectedAgent.id].enabled" type="checkbox" aria-label="启用" @change="markDirty(selectedAgent.id)" />启用</label>
  280. <label><input v-model="agentDrafts[selectedAgent.id].replyWithoutMention" type="checkbox" :aria-label="hostEnabled ? '无需 @ 也回复' : '自动回复'" @change="markDirty(selectedAgent.id)" />{{ hostEnabled ? '无需 @ 也回复' : '自动回复' }}</label>
  281. </div>
  282. <p v-if="hostEnabled" class="host-explanation">{{ form.orchestrationMode === 'host' ? '主持流程下此开关暂不触发自动并发;切回自由讨论后继续按原设置生效。' : (selectedAgent.isHost ? '主持人始终处理用户无 @ 消息;此开关仅决定是否自动参与 Agent 发出的无 @ 消息。' : '开启后,该成员会自动参与未明确 @ 的消息;用户无 @ 消息仍由主持人兜底。') }}</p>
  283. <p v-if="agentUpdateError?.[selectedAgent.id]" class="agent-save-error" role="alert">{{ agentUpdateError[selectedAgent.id] }}</p>
  284. <div class="editor-actions">
  285. <button class="quiet-button" type="button" :disabled="busy || !hasAgentChanges(selectedAgent)" @click="resetAgent(selectedAgent)">取消更改</button>
  286. <button class="save-agent" type="button" :disabled="busy || !hasAgentChanges(selectedAgent)" @click="saveAgent(selectedAgent)">保存 Agent 设置</button>
  287. </div>
  288. </fieldset>
  289. </section>
  290. </div>
  291. </Teleport>
  292. </template>
  293. <style scoped>
  294. .group-manager { height: 100%; overflow-y: auto; padding: 0 17px 24px; }
  295. header { display: flex; position: sticky; z-index: 3; top: 0; min-height: 62px; align-items: center; justify-content: space-between; padding-right: 40px; background: var(--surface); }
  296. header > span { display: flex; flex-direction: column; } header small { color: var(--text-muted); font-size: 9px; } header strong { margin-top: 2px; font-size: 13px; font-weight: 620; }
  297. section { padding: 17px 0; border-top: 1px solid var(--line); } section:first-of-type { border-top: 0; }
  298. h3 { margin: 0 0 13px; color: var(--text-secondary); font-size: 10px; font-weight: 650; letter-spacing: .06em; text-transform: uppercase; } h3 em { color: var(--text-muted); font-style: normal; font-weight: 500; }
  299. label { display: flex; flex-direction: column; gap: 5px; margin: 0 0 11px; color: var(--text-muted); font-size: 10px; }
  300. input, textarea, select { width: 100%; padding: 8px 9px; border: 1px solid var(--line); border-radius: 9px; outline: 0; resize: vertical; background: var(--surface-soft); color: var(--text-primary); font-size: 11px; } input:focus, textarea:focus, select:focus { border-color: var(--line-strong); box-shadow: 0 0 0 3px var(--focus-ring); }
  301. .rounds { flex-direction: row; align-items: center; justify-content: space-between; }.rounds > span { display: flex; flex-direction: column; gap: 2px; }.rounds small { color: var(--text-muted); font-size: 9px; }.rounds input { width: 62px; text-align: center; }
  302. .flow-mode { display: grid; grid-template-columns: minmax(0, 1fr) 128px; align-items: center; gap: 10px; }.flow-mode > span { display: flex; flex-direction: column; gap: 2px; color: var(--text-secondary); font-weight: 650; }.flow-mode small { color: var(--text-muted); font-size: 9px; font-weight: 400; line-height: 1.4; }.flow-mode select { margin: 0; padding: 7px 8px; background: var(--surface); }
  303. .host-selector { display: grid; grid-template-columns: minmax(0, 1fr) 128px; align-items: center; gap: 10px; margin-bottom: 10px; padding: 9px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-soft); }.host-selector > span { display: flex; min-width: 0; flex-direction: column; gap: 2px; color: var(--text-secondary); font-weight: 650; }.host-selector small { color: var(--text-muted); font-size: 9px; font-weight: 400; line-height: 1.4; }.host-selector select { padding: 7px 8px; background: var(--surface); }
  304. .agent-list { display: flex; flex-direction: column; gap: 7px; }.agent-list article { display: grid; grid-template-columns: 32px minmax(0,1fr) repeat(3, 28px); align-items: center; gap: 7px; padding: 8px; border: 1px solid transparent; border-radius: 10px; background: var(--surface-soft); }
  305. .agent-avatar { position: relative; display: grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; background: transparent; color: var(--text-secondary); font-size: 10px; font-weight: 700; }.agent-avatar i { position: absolute; right: -2px; bottom: -2px; width: 8px; height: 8px; border: 2px solid var(--surface-soft); border-radius: 50%; background: var(--text-muted); }.agent-avatar .status-running, .agent-avatar .status-queued { background: var(--warning); }.agent-avatar .status-idle { background: var(--success); }.agent-avatar .status-awaiting_input { background: var(--warning); }
  306. .agent-copy { display: flex; min-width: 0; flex-direction: column; }.agent-name-line, .settings-agent-name { display: flex; min-width: 0; align-items: center; gap: 5px; }.agent-copy strong, .agent-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.agent-copy strong { min-width: 0; font-size: 11px; }.agent-copy small { margin-top: 2px; color: var(--text-muted); font-size: 9px; }.host-badge { flex: 0 0 auto; padding: 2px 5px; border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line)); border-radius: 999px; background: color-mix(in srgb, var(--accent) 9%, transparent); color: var(--accent); font-size: 8px; font-style: normal; font-weight: 650; line-height: 1.2; }
  307. .agent-action { display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: 7px; background: transparent; color: var(--text-muted); cursor: pointer; }.agent-action:hover { background: var(--surface-hover); color: var(--text-primary); }.agent-action.danger:hover { color: var(--danger); }.agent-action:disabled { cursor: not-allowed; opacity: .25; }
  308. .agent-settings-backdrop { position: fixed; z-index: 50; inset: 0; display: grid; place-items: center; padding: 24px; background: color-mix(in srgb, var(--text-primary) 24%, transparent); }.agent-settings-dialog { width: min(100%, 520px); max-height: min(720px, calc(100vh - 48px)); overflow: auto; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); box-shadow: 0 24px 72px color-mix(in srgb, var(--text-primary) 24%, transparent); }.agent-settings-dialog header { min-height: 68px; padding: 0 20px; border-bottom: 1px solid var(--line); border-radius: 16px 16px 0 0; }.agent-settings-close { display: grid; place-items: center; width: 32px; height: 32px; padding: 0; border: 0; border-radius: 8px; background: transparent; color: var(--text-muted); cursor: pointer; }.agent-settings-close:hover { background: var(--surface-hover); color: var(--text-primary); }.agent-editor { display: block; min-width: 0; margin: 0; padding: 20px; border: 0; }.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }.editor-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 12px; }.editor-grid > label:nth-child(1), .editor-grid > label:nth-child(2) { grid-column: 1 / -1; }.editor-grid label:last-child { margin-bottom: 7px; }.editor-note { margin: -1px 2px 0; color: var(--text-muted); font-size: 9px; }.editor-note.error { color: var(--danger); }
  309. .editor-toggles { display: flex; align-items: center; gap: 16px; padding: 3px 0 12px; }.editor-toggles label { display: flex; flex-direction: row; align-items: center; gap: 5px; margin: 0; color: var(--text-secondary); }.editor-toggles input { width: auto; margin: 0; accent-color: var(--accent); }
  310. .remote-agent-address { margin: 0 0 10px; overflow-wrap: anywhere; color: var(--text-muted); font-size: 9px; line-height: 1.5; }
  311. .host-explanation { margin: -4px 0 12px; color: var(--text-muted); font-size: 9px; line-height: 1.5; }
  312. .agent-save-error { margin: 0 0 10px; color: var(--danger); font-size: 9px; line-height: 1.45; }
  313. .editor-actions { display: flex; justify-content: flex-end; gap: 7px; }.editor-actions button { min-height: 30px; padding: 0 10px; border-radius: 8px; cursor: pointer; font-size: 10px; }.editor-actions button:disabled { cursor: not-allowed; opacity: .35; }.save-agent { border: 1px solid var(--accent); background: var(--accent); color: var(--text-on-solid); }
  314. .add-agent { margin-top: 8px; }.add-agent select { cursor: pointer; }
  315. .danger-zone { display: flex; align-items: center; justify-content: space-between; gap: 12px; }.danger-zone strong { font-size: 11px; }.danger-zone p { margin: 3px 0 0; color: var(--text-muted); font-size: 9px; line-height: 1.5; }.danger-zone .quiet-button { display: flex; flex: 0 0 auto; gap: 6px; color: var(--danger); font-size: 10px; }
  316. @media (max-width: 600px) { .agent-settings-backdrop { place-items: end center; padding: 0; }.agent-settings-dialog { width: 100%; max-height: min(760px, calc(100vh - 16px)); border-radius: 18px 18px 0 0; }.agent-settings-dialog header { border-radius: 18px 18px 0 0; }.editor-grid { grid-template-columns: 1fr; }.editor-grid > label { grid-column: 1 / -1; } }
  317. </style>