feat(agents): open AGENTS.md editor in a modal, as a settings row, with section reordering

Move the AGENTS.md section editing into a click-to-open modal so the Basic
tab no longer expands the full document inline. Present the entry as a
settings-style row (title + description on the left, a "manage" button on the
right) instead of stacked label/hint/button.

Add optional move-up/move-down controls to MemorySection (off by default, so
MemoryBrowser is unaffected) and wire reordering in AgentGuideEditor: adjacent
sections swap and the whole file is re-saved, with a synthetic preamble pinned
to the top.
This commit is contained in:
倪程伟 2026-06-08 13:53:26 +08:00 committed by matevip
parent 4f11a444c1
commit da5aaba0f7
5 changed files with 139 additions and 35 deletions

View File

@ -1238,6 +1238,7 @@ export default {
},
guide: {
summary: 'Behavior Guide (AGENTS.md)',
open: 'Manage Behavior Guide',
desc: "This is the employee's behavior handbook (AGENTS.md): when to write memories, what goes in each file, and which memory tools are available. The Role/Goal/Backstory above are the employee's factory identity; evolving memories like PROFILE/MEMORY live in the Memory menu.",
empty: 'This employee has no behavior guide (AGENTS.md) yet.',
create: 'Create Behavior Guide',
@ -3780,6 +3781,8 @@ export default {
editPlaceholder: 'Edit memory content...',
confirmed: 'Confirmed',
saved: 'Saved',
moveUp: 'Move up',
moveDown: 'Move down',
},
facts: {
searchPlaceholder: 'Search facts…',

View File

@ -1130,6 +1130,7 @@ export default {
},
guide: {
summary: '行为指南AGENTS.md',
open: '管理行为指南',
desc: '这里是该员工的行为手册AGENTS.md规定何时写记忆、各文件放什么、可用的记忆工具。上方的岗位目标背景是员工的出厂身份PROFILEMEMORY 等可演化的记忆在「记忆」菜单管理。',
empty: '该员工还没有行为指南AGENTS.md。',
create: '创建行为指南',
@ -3872,6 +3873,8 @@ export default {
editPlaceholder: '修改记忆内容...',
confirmed: '已确认',
saved: '已保存',
moveUp: '上移',
moveDown: '下移',
},
facts: {
searchPlaceholder: '搜索事实…',

View File

@ -347,8 +347,10 @@
<textarea v-model="profileForm.extra" class="form-textarea" rows="4" :placeholder="t('agents.placeholders.extraInstructions')"></textarea>
<p class="form-hint">{{ t('agents.fields.extraInstructionsHint') }}</p>
<div v-if="editingAgent" class="advanced-guide">
<label class="form-label">{{ t('agents.guide.summary') }}</label>
<p class="form-hint">{{ t('agents.guide.desc') }}</p>
<div class="advanced-guide__info">
<span class="advanced-guide__title">{{ t('agents.guide.summary') }}</span>
<p class="advanced-guide__desc">{{ t('agents.guide.desc') }}</p>
</div>
<AgentGuideEditor :agent-id="editingAgent.id" />
</div>
</details>
@ -1965,7 +1967,14 @@ html.dark .seg-count.warn {
.advanced-prompt[open] > .advanced-prompt__summary { padding: 0 0 8px; border-bottom: 1px solid var(--mc-border-light); margin-bottom: 8px; }
.advanced-prompt[open] > .advanced-prompt__summary::before { transform: rotate(90deg); }
.advanced-prompt .form-textarea { width: 100%; box-sizing: border-box; }
.advanced-guide { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--mc-border-light); }
.advanced-guide {
margin-top: 16px; display: flex; align-items: center; gap: 14px;
padding: 12px 14px; border-radius: 10px; background: var(--mc-bg-sunken);
border: 1px solid var(--mc-border-light);
}
.advanced-guide__info { flex: 1; min-width: 0; }
.advanced-guide__title { display: block; font-size: 13px; font-weight: 600; color: var(--mc-text-primary); }
.advanced-guide__desc { margin: 4px 0 0; font-size: 12px; line-height: 1.5; color: var(--mc-text-tertiary); }
.modal-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 16px 24px; border-top: 1px solid var(--mc-border-light); }
@media (max-width: 900px) {

View File

@ -1,41 +1,60 @@
<template>
<div class="guide-editor">
<div v-if="loading" class="guide-state"></div>
<button type="button" class="guide-open-btn" @click="open">
{{ t('agents.guide.open') }}
</button>
<!-- Empty state: AGENTS.md not present or blank -->
<div v-else-if="sections.length === 0" class="guide-empty">
<p class="guide-empty__text">{{ t('agents.guide.empty') }}</p>
<button class="guide-create-btn" :disabled="saving" @click="createGuide">
{{ t('agents.guide.create') }}
</button>
</div>
<Teleport to="body">
<div v-if="visible" class="guide-overlay" @click.self="visible = false">
<div class="guide-modal">
<div class="guide-modal__header">
<h3 class="guide-modal__title">{{ t('agents.guide.summary') }}</h3>
<button type="button" class="guide-modal__close" @click="visible = false">×</button>
</div>
<!-- Section cards -->
<div v-else class="guide-sections">
<MemorySection
v-for="(sec, idx) in sections"
:key="idx"
:section="sec"
:save-handler="(body: string) => saveSection(idx, body)"
/>
<div class="guide-modal__body">
<div v-if="loading" class="guide-state"></div>
<div class="guide-add">
<input
v-model="newHeading"
class="guide-add__input"
:placeholder="t('agents.guide.addSectionPlaceholder')"
@keyup.enter="addSection"
/>
<button class="guide-add__btn" :disabled="!newHeading.trim() || saving" @click="addSection">
+ {{ t('agents.guide.addSection') }}
</button>
<!-- Empty state: AGENTS.md not present or blank -->
<div v-else-if="sections.length === 0" class="guide-empty">
<p class="guide-empty__text">{{ t('agents.guide.empty') }}</p>
<button type="button" class="guide-create-btn" :disabled="saving" @click="createGuide">
{{ t('agents.guide.create') }}
</button>
</div>
<!-- Section cards -->
<div v-else class="guide-sections">
<MemorySection
v-for="(sec, idx) in sections"
:key="idx"
:section="sec"
:save-handler="(body: string) => saveSection(idx, body)"
:can-move-up="canMoveUp(idx)"
:can-move-down="canMoveDown(idx)"
@move-up="move(idx, -1)"
@move-down="move(idx, 1)"
/>
<div class="guide-add">
<input
v-model="newHeading"
class="guide-add__input"
:placeholder="t('agents.guide.addSectionPlaceholder')"
@keyup.enter="addSection"
/>
<button type="button" class="guide-add__btn" :disabled="!newHeading.trim() || saving" @click="addSection">
+ {{ t('agents.guide.addSection') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { agentContextApi } from '@/api/index'
import { mcToast } from '@/composables/useMcToast'
@ -46,12 +65,16 @@ const props = defineProps<{ agentId: string | number }>()
const { t } = useI18n()
const FILE = 'AGENTS.md'
const loading = ref(true)
const visible = ref(false)
const loading = ref(false)
const saving = ref(false)
const sections = ref<MemorySectionData[]>([])
const newHeading = ref('')
onMounted(load)
function open() {
visible.value = true
load()
}
async function load() {
loading.value = true
@ -109,6 +132,34 @@ async function createGuide() {
}
}
// A synthetic preamble (content before the first `## `) is pinned to the top
// and never reorders, so the first movable section is index 1 when present.
function firstMovable(): number {
return sections.value[0]?.synthetic ? 1 : 0
}
function canMoveUp(idx: number): boolean {
return !sections.value[idx]?.synthetic && idx > firstMovable()
}
function canMoveDown(idx: number): boolean {
return !sections.value[idx]?.synthetic && idx < sections.value.length - 1
}
async function move(idx: number, dir: -1 | 1) {
const target = idx + dir
if (target < firstMovable() || target >= sections.value.length) return
saving.value = true
try {
const next = [...sections.value]
;[next[idx], next[target]] = [next[target], next[idx]]
await agentContextApi.saveFile(props.agentId, FILE, serialize(next))
await load()
} catch (e: any) {
mcToast.error(e?.message || 'Save failed')
} finally {
saving.value = false
}
}
async function addSection() {
const heading = newHeading.value.trim()
if (!heading) return
@ -127,7 +178,36 @@ async function addSection() {
</script>
<style scoped>
.guide-editor { margin-top: 8px; }
.guide-open-btn {
flex-shrink: 0; white-space: nowrap;
padding: 7px 14px; border-radius: 8px; font-size: 12px; font-weight: 500;
background: var(--mc-bg-elevated); color: var(--mc-text-secondary); border: 1px solid var(--mc-border);
cursor: pointer; transition: all 0.12s;
}
.guide-open-btn:hover { border-color: var(--mc-primary); color: var(--mc-primary); }
/* Nested modal — sits above the edit-agent modal (z-index 1000). */
.guide-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex;
align-items: center; justify-content: center; z-index: 1100; padding: 20px;
}
.guide-modal {
background: var(--mc-bg-elevated); border: 1px solid var(--mc-border); border-radius: 16px;
width: 100%; max-width: 640px; max-height: 85vh; display: flex; flex-direction: column;
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
}
.guide-modal__header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 20px; border-bottom: 1px solid var(--mc-border-light);
}
.guide-modal__title { margin: 0; font-size: 15px; font-weight: 600; color: var(--mc-text-primary); }
.guide-modal__close {
border: none; background: transparent; font-size: 22px; line-height: 1;
color: var(--mc-text-tertiary); cursor: pointer; padding: 0 4px;
}
.guide-modal__close:hover { color: var(--mc-text-primary); }
.guide-modal__body { padding: 16px 20px; overflow-y: auto; }
.guide-state { font-size: 13px; color: var(--mc-text-tertiary); padding: 8px 0; }
.guide-empty {
display: flex; flex-direction: column; align-items: flex-start; gap: 10px;

View File

@ -3,6 +3,8 @@
<div class="section-header">
<h4 class="section-title">{{ section.heading }}</h4>
<div class="section-actions">
<button v-if="!editing && canMoveUp" class="section-btn section-btn--move" :title="t('memory.hil.moveUp')" @click="emit('moveUp')"></button>
<button v-if="!editing && canMoveDown" class="section-btn section-btn--move" :title="t('memory.hil.moveDown')" @click="emit('moveDown')"></button>
<button v-if="!editing" class="section-btn" :title="t('memory.hil.edit')" @click="startEdit">
<MemoryIcon name="edit" :size="12" />
</button>
@ -41,8 +43,14 @@ const props = defineProps<{
section: MemorySectionData
/** Persists the edited body — resolves on success, rejects on failure. */
saveHandler: (body: string) => Promise<void>
/** Show a ↑ button; emits `moveUp`. Off by default (MemoryBrowser doesn't reorder). */
canMoveUp?: boolean
/** Show a ↓ button; emits `moveDown`. */
canMoveDown?: boolean
}>()
const emit = defineEmits<{ moveUp: []; moveDown: [] }>()
const { t } = useI18n()
const editing = ref(false)
const draft = ref('')
@ -110,6 +118,7 @@ function renderMarkdown(body: string): string {
cursor: pointer; transition: all 0.12s;
}
.section-btn:hover { background: var(--mc-bg-elevated); color: var(--mc-primary); }
.section-btn--move { font-size: 14px; line-height: 1; }
.section-body {
margin-top: 8px; font-size: 13px; color: var(--mc-text-secondary); line-height: 1.6;