diff --git a/core/http/react-ui/e2e/agent-memory.spec.js b/core/http/react-ui/e2e/agent-memory.spec.js index 40be405264..261d0cf076 100644 --- a/core/http/react-ui/e2e/agent-memory.spec.js +++ b/core/http/react-ui/e2e/agent-memory.spec.js @@ -44,7 +44,6 @@ test.describe('Memory section of the agent form', () => { test('a new agent starts with Mine and its own, the permission to save, and sends them', async ({ page }) => { const s = await serve(page) await page.goto('/app/agents/new') - await fold(page, 'Notes').locator('.ag-fold__button').click() const choice = page.getByTestId('agent-memory-choice') await expect(choice.getByTestId('agent-memory-mine_and_own').locator('input')).toBeChecked() await expect(choice.getByTestId('agent-memory-mine_and_own')).toContainText('Recommended') @@ -63,7 +62,6 @@ test.describe('Memory section of the agent form', () => { test('an agent saved before shows As before, and picking a choice sends it with the grant', async ({ page }) => { const s = await serve(page) await page.goto('/app/agents/a2/edit') - await fold(page, 'Notes').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-memory-before')).toContainText('As before') await expect(fold(page, 'Notes')).toContainText('As before') await expect(page.getByTestId('agent-memory-choice').locator('input[type=radio]:checked')).toHaveCount(0) @@ -74,7 +72,6 @@ test.describe('Memory section of the agent form', () => { expect('memory' in s.calls.find(c => c.method === 'PUT').body).toBe(false) await page.goto('/app/agents/a2/edit') - await fold(page, 'Notes').locator('.ag-fold__button').click() await page.getByTestId('agent-memory-mine').click() await expect(page.getByTestId('agent-memory-mine-warn')).toContainText('You confirm each one') await expect(page.getByTestId('agent-memory-before')).toHaveCount(0) @@ -89,7 +86,6 @@ test.describe('Memory section of the agent form', () => { test('None leaves the permissions alone and the save switch is hidden', async ({ page }) => { const s = await serve(page, { agents: [{ ...AGENTS[1], memory: 'own', builtins: ['skills.write'] }] }) await page.goto('/app/agents/a2/edit') - await fold(page, 'Notes').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-memory-own').locator('input')).toBeChecked() await page.getByTestId('agent-memory-none').click() await expect(page.getByTestId('agent-memory-saves')).toHaveCount(0) @@ -103,7 +99,6 @@ test.describe('Memory section of the agent form', () => { test('says that memory is off in the settings of the person', async ({ page }) => { await serve(page, { enabled: false }) await page.goto('/app/agents/a2/edit') - await fold(page, 'Notes').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-memory-user-off')).toContainText('Memory is off in your settings') await shot(page, 'agent-memory-user-off', { fullPage: true }) }) @@ -112,7 +107,6 @@ test.describe('Memory section of the agent form', () => { test.describe('The save rule of the agent', () => { const open = async (page, path) => { await page.goto(path) - await fold(page, 'Notes').locator('.ag-fold__button').click() } test('offers three cards, never auto, says what is in force, and sends nothing until it is changed', async ({ page }) => { diff --git a/core/http/react-ui/e2e/agents-edit.spec.js b/core/http/react-ui/e2e/agents-edit.spec.js index 576772da20..6d52a32ec9 100644 --- a/core/http/react-ui/e2e/agents-edit.spec.js +++ b/core/http/react-ui/e2e/agents-edit.spec.js @@ -17,18 +17,14 @@ test.describe('Agent form', () => { await page.getByLabel('Name', { exact: false }).first().fill('Reviewer') await page.getByLabel('Description').fill('Checks a change') - await fold(page, 'PromptsGoals').locator('.ag-fold__button').click() await page.getByLabel('Instructions').fill('Answer in French.') - await fold(page, 'MemorySettings').locator('.ag-fold__button').click() await page.getByRole('combobox', { name: 'Collections +' }).fill('handbook') await page.getByRole('combobox', { name: 'Collections +' }).press('Enter') await expect(page.getByTestId('agent-collections').getByRole('listitem')).toHaveText(['handbook']) - await fold(page, 'Skills').locator('.ag-fold__button').click() await page.getByTestId('agent-skills-panel').getByRole('switch', { name: /citations/ }).click() - await fold(page, 'Tools').locator('.ag-fold__button').click() await page.getByTestId('agent-tools-panel').getByRole('switch', { name: /Docs/ }).click() await page.getByRole('button', { name: 'Create Agent' }).first().click() @@ -44,7 +40,6 @@ test.describe('Agent form', () => { test('warns when the first option turns off the messaging defaults, and adds the missing ones in one click', async ({ page }) => { const s = await installAgents(page) await page.goto('/app/agents/a2/edit') - await fold(page, 'Tools').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-grants-notice')).toHaveCount(0) await page.getByTestId('agent-tools-panel').getByRole('switch', { name: 'Remember things' }).click() const notice = page.getByTestId('agent-grants-notice') @@ -60,7 +55,6 @@ test.describe('Agent form', () => { test('shows no warning for an agent that already had permissions', async ({ page }) => { await installAgents(page, { agents: [{ ...AGENTS[1], builtins: ['memory.write'] }] }) await page.goto('/app/agents/a2/edit') - await fold(page, 'Tools').locator('.ag-fold__button').click() await page.getByTestId('agent-tools-panel').getByRole('switch', { name: 'Also propose from messaging apps' }).click() await expect(page.getByTestId('agent-grants-notice')).toHaveCount(0) }) @@ -68,7 +62,6 @@ test.describe('Agent form', () => { test('each capability is one switch with its options under it, greyed with a reason while the switch is off', async ({ page }) => { const s = await installAgents(page, { agents: [{ ...AGENTS[1], builtins: ['triggers.propose', 'act.files'], builtins_off: ['triggers'] }] }) await page.goto('/app/agents/a2/edit') - await fold(page, 'Tools').locator('.ag-fold__button').click() const panel = page.getByTestId('agent-tools-panel') // The three trigger switches have names that cannot be mixed up. const triggers = panel.getByRole('switch', { name: 'Create and manage triggers' }) @@ -98,7 +91,6 @@ test.describe('Agent form', () => { test('saved agents is one switch: turning it off takes the grant away too, turning it on adds only the group', async ({ page }) => { const s = await installAgents(page, { agents: [{ ...AGENTS[1], builtins: ['memory.write', 'saved_agents'] }, { ...AGENTS[0], id: 'a9', name: 'plain' }] }) await page.goto('/app/agents/a2/edit') - await fold(page, 'Tools').locator('.ag-fold__button').click() const sw = page.getByTestId('agent-tools-panel').getByRole('switch', { name: 'Saved agents' }) await expect(sw).toBeChecked() await expect(page.getByTestId('agent-tools-panel').getByText('Call your saved agents')).toHaveCount(0) @@ -106,7 +98,6 @@ test.describe('Agent form', () => { await page.getByRole('button', { name: 'Save changes' }).first().click() await expect.poll(() => s.calls.find(c => c.method === 'PUT')?.body).toMatchObject({ builtins: ['memory.write'], builtins_on: [] }) await page.goto('/app/agents/a9/edit') - await fold(page, 'Tools').locator('.ag-fold__button').click() await page.getByTestId('agent-tools-panel').getByRole('switch', { name: 'Saved agents' }).click() await page.getByRole('button', { name: 'Save changes' }).first().click() await expect.poll(() => s.calls.filter(c => c.method === 'PUT').at(-1)?.body).toMatchObject({ builtins: [], builtins_on: ['saved_agents'] }) @@ -120,7 +111,6 @@ test.describe('Agent form', () => { ] await installAgents(page, { bindings, agents: [{ ...AGENTS[1], builtins: ['triggers.propose', 'memory.write'] }] }) await page.goto('/app/agents/a2/edit') - await fold(page, 'Tools').locator('.ag-fold__button').click() const panel = page.getByTestId('agent-tools-panel') const notes = panel.getByTestId('agent-tools-conn-note') await expect(notes).toHaveCount(3) @@ -137,7 +127,6 @@ test.describe('Agent form', () => { const s = await installAgents(page) await page.goto('/app/agents/new') await page.getByLabel('Name', { exact: false }).first().fill('Builder') - await fold(page, 'Tools').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-network')).toHaveValue('off') await page.getByTestId('agent-network').selectOption('registries') await page.getByRole('button', { name: 'Create Agent' }).first().click() @@ -148,7 +137,7 @@ test.describe('Agent form', () => { test('needs a name, and says so on the fold', async ({ page }) => { const s = await installAgents(page) await page.goto('/app/agents/new') - await expect(fold(page, 'BasicInfo')).toContainText('Needs a name') + await expect(page.locator('[data-group="identity"]')).toContainText('Needs a name') await page.getByRole('button', { name: 'Create Agent' }).first().click() expect(s.calls.some(c => c.method === 'POST')).toBe(false) }) @@ -166,7 +155,7 @@ test.describe('Agent form', () => { await page.goto('/app/agents/a1/edit') await expect(page.getByLabel('Name', { exact: false }).first()).toHaveValue('research-assistant') await page.getByLabel('Description').fill('Searches and cites.') - await expect(fold(page, 'BasicInfo')).toContainText('1 changed') + await expect(page.locator('[data-group="identity"]')).toContainText('1 changed') await page.getByTestId('agent-preview-open').click() const sheet = page.getByTestId('agent-preview') @@ -185,11 +174,9 @@ test.describe('Agent form', () => { test('the tools list is the list of the chat: connectors by name, one that was deleted stays, and the servers are free text', async ({ page }) => { const s = await installAgents(page) await page.goto('/app/agents/a1/edit') - await fold(page, 'Tools').locator('.ag-fold__button').click() const picker = page.getByTestId('agent-tools-panel') await expect(picker.getByRole('switch', { name: /Docs/ })).toBeChecked() await expect(picker.getByRole('switch', { name: /Parked/ })).not.toBeChecked() - await fold(page, 'MCP').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-mcp').getByRole('listitem')).toHaveText(['crm']) await expect(page.locator('datalist option[value="mail"]')).toHaveCount(1) await expect(picker.getByTestId('agent-tools-item').filter({ hasText: 'Parked' })).toContainText('off') @@ -203,7 +190,6 @@ test.describe('Agent form', () => { test('every switch of the tools list says what it does, and the switches are saved with the agent', async ({ page }) => { const s = await installAgents(page) await page.goto('/app/agents/a1/edit') - await fold(page, 'Tools').locator('.ag-fold__button').click() const panel = page.getByTestId('agent-tools-panel') // The same words as the Tools menu of a chat: a title, a description, and // the add-on's declared title instead of its id. Eight rows are the built-in @@ -290,7 +276,6 @@ test.describe('Agent form', () => { test('the Export fold downloads the definition of a saved agent', async ({ page }) => { await installAgents(page) await page.goto('/app/agents/a1/edit') - await fold(page, 'export').locator('.ag-fold__button').click() const download = page.waitForEvent('download') await page.getByRole('button', { name: 'Export agent' }).click() expect((await download).suggestedFilename()).toBe('research-assistant.json') @@ -307,7 +292,6 @@ test.describe('Agent folder', () => { const s = await installAgents(page, { files: HOME_FOLDERS }) await page.goto('/app/agents/a2/edit') await expect(fold(page, 'Files')).toContainText('All my files') - await fold(page, 'Files').locator('.ag-fold__button').click() await page.getByTestId('agent-folder-scope-one').click() // Nothing chosen yet: the fold needs the person, and Save says so. await expect(fold(page, 'Files')).toContainText('Choose a folder, or pick All my files.') @@ -339,7 +323,6 @@ test.describe('Agent folder', () => { await expect(chip.locator('[data-icon="lock"]')).toHaveCount(1) await page.goto('/app/agents/a2/edit') - await fold(page, 'Files').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-folder-mode-0-read')).toHaveAttribute('aria-pressed', 'true') await page.getByTestId('agent-folder-mode-0-write').click() await expect(fold(page, 'Files')).toContainText('1 changed') @@ -360,7 +343,6 @@ test.describe('Agent folder', () => { expect(s.calls.find(c => c.method === 'PUT').body).not.toHaveProperty('folders') await page.goto('/app/agents/a2/edit') - await fold(page, 'Files').locator('.ag-fold__button').click() await page.getByTestId('agent-folder-scope-all').click() await page.getByRole('button', { name: 'Save changes' }).first().click() await expect(page).toHaveURL(/\/app\/agents$/) @@ -381,7 +363,6 @@ test.describe('Agent folder', () => { // The fold says so; the save of another field keeps working and keeps it. await expect(fold(page, 'Files')).toContainText('Folder missing') - await fold(page, 'Files').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-folder-missing')).toContainText('This folder is gone. Choose another folder to use this agent.') await page.getByLabel('Description').fill('Still saves.') await page.getByRole('button', { name: 'Save changes' }).first().click() @@ -390,7 +371,6 @@ test.describe('Agent folder', () => { // Choosing another folder sends it with the mode the agent had. await page.goto('/app/agents/a2/edit') - await fold(page, 'Files').locator('.ag-fold__button').click() await page.getByTestId('agent-folder-missing').getByRole('button', { name: 'Choose another folder' }).click() await page.getByTestId('agent-folder-dialog').getByRole('button', { name: 'Projects' }).click() await page.getByTestId('agent-folder-use').click() @@ -403,7 +383,6 @@ test.describe('Agent folder', () => { const s = await installAgents(page, { files: HOME_FOLDERS }) await page.goto('/app/agents/new') await page.getByLabel('Name', { exact: false }).first().fill('Reader') - await fold(page, 'Files').locator('.ag-fold__button').click() await page.getByTestId('agent-folder-scope-one').click() await page.getByTestId('agent-folder-choose').click() const dialog = page.getByTestId('agent-folder-dialog') @@ -430,7 +409,6 @@ test.describe('Agent folder', () => { const s = await installAgents(page, { files }) await page.goto('/app/agents/new') await page.getByLabel('Name', { exact: false }).first().fill('Many') - await fold(page, 'Files').locator('.ag-fold__button').click() await page.getByTestId('agent-folder-scope-one').click() const pick = async (name, choose) => { await choose.click() @@ -473,7 +451,6 @@ test.describe('Agent folder', () => { test('a row added and cancelled goes away, and a folder inside another shows the outer mode', async ({ page }) => { await installAgents(page, { files: HOME_FOLDERS, agents: withFolder(ACME_READ) }) await page.goto('/app/agents/a2/edit') - await fold(page, 'Files').locator('.ag-fold__button').click() await page.getByTestId('agent-folder-add').click() await page.getByTestId('agent-folder-dialog').getByRole('button', { name: 'Cancel' }).first().click() await expect(page.getByTestId('agent-folder-row')).toHaveCount(1) @@ -492,7 +469,6 @@ test.describe('Agent folder', () => { const s = await installAgents(page, { files: HOME_FOLDERS, agents: withFolder(ACME_READ).map(a => (a.id === 'a2' ? { ...a, folders: rows } : a)) }) await page.goto('/app/agents/a2/edit') await expect(fold(page, 'Files')).toContainText('2 folders') - await fold(page, 'Files').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-folder-row')).toHaveCount(2) await page.getByTestId('agent-folder-add').click() await page.getByTestId('agent-folder-dialog').getByRole('button', { name: 'Cancel' }).first().click() @@ -510,7 +486,6 @@ test.describe('Agent folder', () => { return route.fallback() }) await page.goto('/app/agents/new') - await fold(page, 'Files').locator('.ag-fold__button').click() await page.getByTestId('agent-folder-scope-one').click() await page.getByTestId('agent-folder-choose').click() const dialog = page.getByTestId('agent-folder-dialog') @@ -524,7 +499,6 @@ test.describe('Agent folder', () => { test('the Export fold says that the file names the folder', async ({ page }) => { const s = await installAgents(page, { files: HOME_FOLDERS, agents: withFolder(ACME_READ) }) await page.goto('/app/agents/a2/edit') - await fold(page, 'export').locator('.ag-fold__button').click() await expect(page.getByTestId('agent-export-folder')).toContainText('The file names the folder /Projects/Acme.') const download = page.waitForEvent('download') await page.getByRole('button', { name: 'Export agent' }).click() @@ -536,7 +510,6 @@ test.describe('Agent folder', () => { test('no copy of the folder fold names an internal path', async ({ page }) => { await installAgents(page, { files: HOME_FOLDERS, agents: withFolder(ACME_READ) }) await page.goto('/app/agents/a2/edit') - await fold(page, 'Files').locator('.ag-fold__button').click() await page.getByTestId('agent-folder').getByRole('button', { name: 'Change', exact: true }).click() await expect(page.getByTestId('folder-picker')).toBeVisible() const text = await page.locator('body').innerText() @@ -554,7 +527,6 @@ const aliasModels = [ { id: 'chained', capabilities: [] }, ] const openModel = async page => { - await fold(page, 'ModelSettings').locator('.ag-fold__button').click() return page.getByTestId('home-model-chip') } diff --git a/core/http/react-ui/e2e/chats-connection.spec.js b/core/http/react-ui/e2e/chats-connection.spec.js index e07b90215e..bc8e5b91ea 100644 --- a/core/http/react-ui/e2e/chats-connection.spec.js +++ b/core/http/react-ui/e2e/chats-connection.spec.js @@ -140,7 +140,6 @@ test.describe('The connections an agent names', () => { })) await page.goto('/app/agents/new') await page.getByLabel('Name', { exact: false }).first().fill('Reviewer') - await page.locator('[data-section="Tools"] .ag-fold__button').click() const panel = page.getByTestId('agent-tools-panel') const group = panel.getByRole('group', { name: 'Connections for chats of this agent' }) await expect(group).toBeVisible() diff --git a/core/http/react-ui/e2e/chats-crud-real.spec.js b/core/http/react-ui/e2e/chats-crud-real.spec.js index 8ea784532d..da6ec2a3a6 100644 --- a/core/http/react-ui/e2e/chats-crud-real.spec.js +++ b/core/http/react-ui/e2e/chats-crud-real.spec.js @@ -55,7 +55,6 @@ test.describe('Skills, agents, triggers and connectors against the real server', await page.goto('/app/agents/new') await page.getByLabel('Name', { exact: false }).first().fill(name) await page.getByLabel('Description').fill('Reviews text') - await page.locator('[data-section="PromptsGoals"] .ag-fold__button').click() await page.getByLabel('Instructions').fill('Be brief.') await page.getByRole('button', { name: 'Create Agent' }).first().click() await expect(page).toHaveURL(/\/app\/agents$/) diff --git a/core/http/react-ui/e2e/memory-off.spec.js b/core/http/react-ui/e2e/memory-off.spec.js index f7984e6751..b86354caf6 100644 --- a/core/http/react-ui/e2e/memory-off.spec.js +++ b/core/http/react-ui/e2e/memory-off.spec.js @@ -263,7 +263,6 @@ test.describe('Agent form: the memory switch of the person', () => { await installAgents(page) await memory(page, false) await page.goto('/app/agents/new') - await page.locator('[data-section="Tools"] .ag-fold__button').click() const line = page.getByTestId('agent-memory-off') await expect(line).toContainText('Memory is off in your settings') await expect(line.getByRole('link', { name: 'Settings, Memory' })).toHaveAttribute('href', /\/app\/account\?tab=memory/) @@ -274,7 +273,6 @@ test.describe('Agent form: the memory switch of the person', () => { await installAgents(page) await memory(page, true) await page.goto('/app/agents/new') - await page.locator('[data-section="Tools"] .ag-fold__button').click() await expect(page.getByTestId('agent-tools-panel')).toBeVisible() await expect(page.getByTestId('agent-memory-off')).toHaveCount(0) }) diff --git a/core/http/react-ui/e2e/no-horizontal-overflow.spec.js b/core/http/react-ui/e2e/no-horizontal-overflow.spec.js index 8378444f82..47ffbd4100 100644 --- a/core/http/react-ui/e2e/no-horizontal-overflow.spec.js +++ b/core/http/react-ui/e2e/no-horizontal-overflow.spec.js @@ -97,7 +97,7 @@ async function chatSetup(page) { } const PAGES = [ - { name: 'agent editor', url: '/app/agents/a1/edit', ready: '[data-testid=agent-edit]', setup: agentsSetup, open: '.ag-fold__button' }, + { name: 'agent editor', url: '/app/agents/a1/edit', ready: '[data-testid=agent-edit]', setup: agentsSetup }, { name: 'new agent', url: '/app/agents/new', ready: '[data-testid=agent-edit]', setup: agentsSetup }, { name: 'agents list', url: '/app/agents', ready: '.ag-home, .page', setup: agentsSetup }, { name: 'agent page', url: '/app/agents/a1', ready: '.page', setup: agentsSetup }, @@ -211,7 +211,6 @@ for (const width of [1280, 1100, 390]) { await page.route(/\/api\/chats\/connectors\/kinds$/, r => r.fulfill({ json: { kinds: [{ kind: 'slack', addon: 'slack', title: 'Slack' }, { kind: 'telegram', addon: 'telegram', title: 'Telegram' }], capabilities: [] } })) await page.goto(`/app/agents/${aid}/edit`) await page.locator('[data-testid=agent-edit]').waitFor() - for (const b of await page.locator('.ag-fold__button').all()) await b.click() await page.waitForTimeout(300) const r = await overflow(page) expect(r.bad, `elements past the right edge (document ${r.scrollWidth} in ${r.clientWidth})`).toEqual([]) diff --git a/core/http/react-ui/e2e/page-render-smoke.spec.js b/core/http/react-ui/e2e/page-render-smoke.spec.js index 95447ab48e..fba1830431 100644 --- a/core/http/react-ui/e2e/page-render-smoke.spec.js +++ b/core/http/react-ui/e2e/page-render-smoke.spec.js @@ -73,7 +73,6 @@ test.describe('Add-on page render smoke', () => { test.describe('Folder picker render smoke', () => { test('renders the folder picker of the agent editor', async ({ page }) => { await page.goto('/app/agents/new') - await page.locator('[data-section="Files"] .ag-fold__button').click() await page.getByTestId('agent-folder-scope-one').click() await page.getByTestId('agent-folder-choose').click() const picker = page.getByTestId('folder-picker') diff --git a/core/http/react-ui/public/locales/de/agents.json b/core/http/react-ui/public/locales/de/agents.json index 2bfed2fe99..067122b0e4 100644 --- a/core/http/react-ui/public/locales/de/agents.json +++ b/core/http/react-ui/public/locales/de/agents.json @@ -151,7 +151,31 @@ "files_upload": "Dateien aus Messaging-Apps empfangen" }, "memoryOffUser": "Das Gedächtnis ist in deinen Einstellungen aus. Dieser Agent liest und speichert deshalb keine Notizen, egal was du hier einstellst.", - "memoryOffFix": "Einstellungen, Gedächtnis" + "memoryOffFix": "Einstellungen, Gedächtnis", + "nav": { + "label": "Abschnitte", + "identity": "Identität", + "capabilities": "Fähigkeiten", + "memory": "Gedächtnis und Wissen", + "advanced": "Erweitert" + }, + "lead": { + "identity": "Wer der Agent ist und wie er sich verhält.", + "capabilities": "Was der Agent nutzen und erreichen darf.", + "memory": "Woran sich der Agent erinnert und was er nachschlagen kann.", + "advanced": "Zusätzliche Server und Export." + }, + "header": { + "unnamed": "Unbenannter Agent", + "modeEdit": "Agent bearbeiten", + "modeCreate": "Neuer Agent", + "summary": "Übersicht dieses Agenten" + }, + "status": { + "clean": "Keine ungespeicherten Änderungen", + "new": "Noch nicht erstellt", + "unsaved": "Ungespeicherte Änderungen ({{count}})" + } }, "run": { "workingLine": "Working" diff --git a/core/http/react-ui/public/locales/en/agents.json b/core/http/react-ui/public/locales/en/agents.json index 59c67c16d5..6fa19b1bc7 100644 --- a/core/http/react-ui/public/locales/en/agents.json +++ b/core/http/react-ui/public/locales/en/agents.json @@ -254,7 +254,31 @@ "files_upload": "Receive files from messaging apps" }, "memoryOffUser": "Memory is off in your settings, so this agent reads and saves no notes, whatever you set here.", - "memoryOffFix": "Settings, Memory" + "memoryOffFix": "Settings, Memory", + "nav": { + "label": "Sections", + "identity": "Identity", + "capabilities": "Capabilities", + "memory": "Memory and knowledge", + "advanced": "Advanced" + }, + "lead": { + "identity": "Who the agent is and how it behaves.", + "capabilities": "What the agent may use and reach.", + "memory": "What the agent remembers and what it can look up.", + "advanced": "Extra servers and export." + }, + "header": { + "unnamed": "Unnamed agent", + "modeEdit": "Edit agent", + "modeCreate": "New agent", + "summary": "Summary of this agent" + }, + "status": { + "clean": "No unsaved changes", + "new": "Not created yet", + "unsaved": "Unsaved changes ({{count}})" + } }, "status": { "title": "Status", diff --git a/core/http/react-ui/public/locales/es/agents.json b/core/http/react-ui/public/locales/es/agents.json index 6e8ad75f70..ab2d8ec9be 100644 --- a/core/http/react-ui/public/locales/es/agents.json +++ b/core/http/react-ui/public/locales/es/agents.json @@ -151,7 +151,31 @@ "files_upload": "Recibir archivos de aplicaciones de mensajería" }, "memoryOffUser": "La memoria está desactivada en tus ajustes, así que este agente no lee ni guarda notas, hagas lo que hagas aquí.", - "memoryOffFix": "Ajustes, Memoria" + "memoryOffFix": "Ajustes, Memoria", + "nav": { + "label": "Secciones", + "identity": "Identidad", + "capabilities": "Capacidades", + "memory": "Memoria y conocimiento", + "advanced": "Avanzado" + }, + "lead": { + "identity": "Quién es el agente y cómo se comporta.", + "capabilities": "Lo que el agente puede usar y alcanzar.", + "memory": "Lo que el agente recuerda y lo que puede consultar.", + "advanced": "Servidores adicionales y exportación." + }, + "header": { + "unnamed": "Agente sin nombre", + "modeEdit": "Editar agente", + "modeCreate": "Agente nuevo", + "summary": "Resumen de este agente" + }, + "status": { + "clean": "Sin cambios sin guardar", + "new": "Aún no creado", + "unsaved": "Cambios sin guardar ({{count}})" + } }, "run": { "workingLine": "Working" diff --git a/core/http/react-ui/public/locales/fr/agents.json b/core/http/react-ui/public/locales/fr/agents.json index e8964c98bd..ae08a81df0 100644 --- a/core/http/react-ui/public/locales/fr/agents.json +++ b/core/http/react-ui/public/locales/fr/agents.json @@ -151,7 +151,31 @@ "files_upload": "Recevoir des fichiers des applications de messagerie" }, "memoryOffUser": "La mémoire est désactivée dans vos réglages : cet agent ne lit ni n'enregistre de notes, quoi que vous choisissiez ici.", - "memoryOffFix": "Réglages, Mémoire" + "memoryOffFix": "Réglages, Mémoire", + "nav": { + "label": "Sections", + "identity": "Identité", + "capabilities": "Capacités", + "memory": "Mémoire et connaissances", + "advanced": "Avancé" + }, + "lead": { + "identity": "Qui est l'agent et comment il se comporte.", + "capabilities": "Ce que l'agent peut utiliser et atteindre.", + "memory": "Ce dont l'agent se souvient et ce qu'il peut consulter.", + "advanced": "Serveurs supplémentaires et export." + }, + "header": { + "unnamed": "Agent sans nom", + "modeEdit": "Modifier l'agent", + "modeCreate": "Nouvel agent", + "summary": "Résumé de cet agent" + }, + "status": { + "clean": "Aucune modification non enregistrée", + "new": "Pas encore créé", + "unsaved": "Modifications non enregistrées ({{count}})" + } }, "run": { "workingLine": "Working" diff --git a/core/http/react-ui/public/locales/id/agents.json b/core/http/react-ui/public/locales/id/agents.json index bdd1a038b2..e1a85b8f98 100644 --- a/core/http/react-ui/public/locales/id/agents.json +++ b/core/http/react-ui/public/locales/id/agents.json @@ -151,7 +151,31 @@ "files_upload": "Menerima file dari aplikasi pesan" }, "memoryOffUser": "Memori nonaktif di pengaturan Anda, jadi agen ini tidak membaca atau menyimpan catatan, apa pun yang Anda atur di sini.", - "memoryOffFix": "Pengaturan, Memori" + "memoryOffFix": "Pengaturan, Memori", + "nav": { + "label": "Bagian", + "identity": "Identitas", + "capabilities": "Kemampuan", + "memory": "Memori dan pengetahuan", + "advanced": "Lanjutan" + }, + "lead": { + "identity": "Siapa agen ini dan bagaimana ia bersikap.", + "capabilities": "Apa yang boleh digunakan dan dijangkau agen.", + "memory": "Apa yang diingat agen dan apa yang bisa dicarinya.", + "advanced": "Server tambahan dan ekspor." + }, + "header": { + "unnamed": "Agen tanpa nama", + "modeEdit": "Ubah agen", + "modeCreate": "Agen baru", + "summary": "Ringkasan agen ini" + }, + "status": { + "clean": "Tidak ada perubahan yang belum disimpan", + "new": "Belum dibuat", + "unsaved": "Perubahan belum disimpan ({{count}})" + } }, "run": { "workingLine": "Working" diff --git a/core/http/react-ui/public/locales/it/agents.json b/core/http/react-ui/public/locales/it/agents.json index a62171ffa4..c9510a0d87 100644 --- a/core/http/react-ui/public/locales/it/agents.json +++ b/core/http/react-ui/public/locales/it/agents.json @@ -151,7 +151,31 @@ "files_upload": "Ricevere file dalle app di messaggistica" }, "memoryOffUser": "La memoria è disattivata nelle tue impostazioni, quindi questo agente non legge né salva note, qualunque cosa imposti qui.", - "memoryOffFix": "Impostazioni, Memoria" + "memoryOffFix": "Impostazioni, Memoria", + "nav": { + "label": "Sezioni", + "identity": "Identità", + "capabilities": "Capacità", + "memory": "Memoria e conoscenza", + "advanced": "Avanzate" + }, + "lead": { + "identity": "Chi è l'agente e come si comporta.", + "capabilities": "Cosa può usare e raggiungere l'agente.", + "memory": "Cosa ricorda l'agente e cosa può consultare.", + "advanced": "Server aggiuntivi ed esportazione." + }, + "header": { + "unnamed": "Agente senza nome", + "modeEdit": "Modifica agente", + "modeCreate": "Nuovo agente", + "summary": "Riepilogo di questo agente" + }, + "status": { + "clean": "Nessuna modifica non salvata", + "new": "Non ancora creato", + "unsaved": "Modifiche non salvate ({{count}})" + } }, "run": { "workingLine": "Working" diff --git a/core/http/react-ui/public/locales/ko/agents.json b/core/http/react-ui/public/locales/ko/agents.json index 2a845cf5b8..d2ad5d7f94 100644 --- a/core/http/react-ui/public/locales/ko/agents.json +++ b/core/http/react-ui/public/locales/ko/agents.json @@ -151,7 +151,31 @@ "files_upload": "메신저 앱에서 파일 받기" }, "memoryOffUser": "설정에서 메모리가 꺼져 있어 여기서 어떻게 정하든 이 에이전트는 메모를 읽거나 저장하지 않습니다.", - "memoryOffFix": "설정, 메모리" + "memoryOffFix": "설정, 메모리", + "nav": { + "label": "섹션", + "identity": "정체성", + "capabilities": "기능", + "memory": "메모리와 지식", + "advanced": "고급" + }, + "lead": { + "identity": "에이전트가 누구이고 어떻게 행동하는지 정합니다.", + "capabilities": "에이전트가 사용하고 접근할 수 있는 것을 정합니다.", + "memory": "에이전트가 기억하는 것과 찾아볼 수 있는 것을 정합니다.", + "advanced": "추가 서버와 내보내기." + }, + "header": { + "unnamed": "이름 없는 에이전트", + "modeEdit": "에이전트 편집", + "modeCreate": "새 에이전트", + "summary": "이 에이전트 요약" + }, + "status": { + "clean": "저장하지 않은 변경 사항 없음", + "new": "아직 만들지 않음", + "unsaved": "저장하지 않은 변경 사항 ({{count}})" + } }, "run": { "workingLine": "Working" diff --git a/core/http/react-ui/public/locales/pt-BR/agents.json b/core/http/react-ui/public/locales/pt-BR/agents.json index 7d0b782258..c6e3e2915d 100644 --- a/core/http/react-ui/public/locales/pt-BR/agents.json +++ b/core/http/react-ui/public/locales/pt-BR/agents.json @@ -151,7 +151,31 @@ "files_upload": "Receber arquivos de aplicativos de mensagens" }, "memoryOffUser": "A memória está desativada nas suas configurações, então este agente não lê nem salva notas, não importa o que você defina aqui.", - "memoryOffFix": "Configurações, Memória" + "memoryOffFix": "Configurações, Memória", + "nav": { + "label": "Seções", + "identity": "Identidade", + "capabilities": "Capacidades", + "memory": "Memória e conhecimento", + "advanced": "Avançado" + }, + "lead": { + "identity": "Quem é o agente e como ele se comporta.", + "capabilities": "O que o agente pode usar e alcançar.", + "memory": "O que o agente lembra e o que ele pode consultar.", + "advanced": "Servidores extras e exportação." + }, + "header": { + "unnamed": "Agente sem nome", + "modeEdit": "Editar agente", + "modeCreate": "Novo agente", + "summary": "Resumo deste agente" + }, + "status": { + "clean": "Nenhuma alteração não salva", + "new": "Ainda não criado", + "unsaved": "Alterações não salvas ({{count}})" + } }, "run": { "workingLine": "Working" diff --git a/core/http/react-ui/public/locales/zh-CN/agents.json b/core/http/react-ui/public/locales/zh-CN/agents.json index b61f0273ef..1e8aa280fe 100644 --- a/core/http/react-ui/public/locales/zh-CN/agents.json +++ b/core/http/react-ui/public/locales/zh-CN/agents.json @@ -151,7 +151,31 @@ "files_upload": "从消息应用接收文件" }, "memoryOffUser": "你的设置中记忆已关闭,因此无论这里如何设置,此智能体都不会读取或保存笔记。", - "memoryOffFix": "设置,记忆" + "memoryOffFix": "设置,记忆", + "nav": { + "label": "分区", + "identity": "身份", + "capabilities": "能力", + "memory": "记忆与知识", + "advanced": "高级" + }, + "lead": { + "identity": "智能体是谁,以及它如何行事。", + "capabilities": "智能体可以使用和访问的内容。", + "memory": "智能体记住什么,以及可以查找什么。", + "advanced": "额外的服务器和导出。" + }, + "header": { + "unnamed": "未命名的智能体", + "modeEdit": "编辑智能体", + "modeCreate": "新智能体", + "summary": "此智能体的摘要" + }, + "status": { + "clean": "没有未保存的更改", + "new": "尚未创建", + "unsaved": "未保存的更改({{count}})" + } }, "run": { "workingLine": "Working" diff --git a/core/http/react-ui/src/pages/AgentCreate.jsx b/core/http/react-ui/src/pages/AgentCreate.jsx index a76f5cc1b4..a11c41287d 100644 --- a/core/http/react-ui/src/pages/AgentCreate.jsx +++ b/core/http/react-ui/src/pages/AgentCreate.jsx @@ -23,7 +23,7 @@ import UnsavedChangesGuard from '../components/UnsavedChangesGuard' import { CAP_CHAT } from '../utils/capabilities' import Icon from '../components/Icon' // eslint-disable-next-line no-unused-vars -import { Button, Callout, LoadError, Page, PageHeader, Segmented, SkeletonRows } from '../components/ui' +import { Button, Callout, LoadError, Page, Segmented, SkeletonRows } from '../components/ui' // eslint-disable-next-line no-unused-vars import AgentFolderDialog from '../components/agents/AgentFolderDialog' // eslint-disable-next-line no-unused-vars @@ -34,18 +34,23 @@ import { switchCounts, switchRows } from '../utils/chatSwitchRows' // eslint-disable-next-line no-unused-vars import { SkillsPanel } from '../components/chat/SkillsMenu' -// The sections of the form. Each folds; Basic info is open at first. -const SECTIONS = [ - { id: 'BasicInfo', icon: 'info' }, - { id: 'ModelSettings', icon: 'brain' }, - { id: 'PromptsGoals', icon: 'target' }, - { id: 'Files', icon: 'folder' }, - { id: 'Notes', icon: 'memory' }, - { id: 'MemorySettings', icon: 'database' }, - { id: 'Skills', icon: 'sparkles' }, - { id: 'Tools', icon: 'plug' }, - { id: 'MCP', icon: 'server' }, +// The cards of the page, in order. `bare` parts have no heading of their own: +// the card heading says what they are. `export` exists only for a saved agent. +const GROUPS = [ + { id: 'identity', icon: 'info', parts: ['BasicInfo', 'ModelSettings', 'PromptsGoals'], bare: true }, + { id: 'capabilities', icon: 'plug', parts: ['Tools', 'Skills', 'Files'] }, + { id: 'memory', icon: 'memory', parts: ['Notes', 'MemorySettings'] }, + { id: 'advanced', icon: 'server', parts: ['MCP', 'export'] }, ] +const secDomId = (gid) => `aed-sec-${gid}` + +// Two letters for the avatar tile of the header: the start of the name. +function monogram(name) { + const words = (name || '').match(/[\p{L}\p{N}]+/gu) || [] + if (!words.length) return '' + const letters = words.length > 1 ? words[0][0] + words[1][0] : words[0].slice(0, 2) + return letters.toUpperCase() +} // The fields each fold summarises on its closed line. const SUMMARY_FIELDS = { @@ -71,8 +76,8 @@ export default function AgentCreate() { const [loadError, setLoadError] = useState(null) const [attempt, setAttempt] = useState(0) const [saving, setSaving] = useState(false) - // Sections fold. Basics is open at first; the rest open on demand. - const [openSections, setOpenSections] = useState(() => new Set(['BasicInfo'])) + // The section the reader is in, for the section nav. + const [activeSec, setActiveSec] = useState(GROUPS[0].id) const [savedConfig, setSavedConfig] = useState(null) const [previewOpen, setPreviewOpen] = useState(false) const [previewTab, setPreviewTab] = useState('config') @@ -120,7 +125,6 @@ export default function AgentCreate() { } initialFormRef.current = initialForm setForm(initialForm) - if (imported) setOpenSections(new Set(SECTIONS.map(s => s.id))) } catch (err) { // An agent that could not be read is said in place of the form: an // empty form would save over it. @@ -154,16 +158,24 @@ export default function AgentCreate() { // by default on its messaging connections. Say so, and offer the fix. const missingGrants = isEdit ? missingConnectorGrants(savedConfig?.builtins, form.builtins) : [] + // Bring a field into view below the sticky header, then focus it. + const focusField = (fieldId) => { + const el = document.getElementById(fieldId) + if (!el) return + el.scrollIntoView({ block: 'center' }) + el.focus({ preventScroll: true }) + } + const handleSubmit = async (e) => { e?.preventDefault?.() if (!payload.name) { addToast(t('create.needsName'), 'warning') - setOpenSections(prev => new Set([...prev, 'BasicInfo'])) + focusField('ag-name') return } if (folderUnchosen) { addToast(t('folder.needsChoice'), 'warning') - setOpenSections(prev => new Set([...prev, 'Files'])) + document.querySelector('[data-section="Files"]')?.scrollIntoView({ block: 'start' }) return } setSaving(true) @@ -184,6 +196,31 @@ export default function AgentCreate() { } } + // Mark the section in view in the nav: the last one whose top has passed + // the line under the sticky header. + useEffect(() => { + if (loading || loadError) return undefined + let frame = 0 + const update = () => { + frame = 0 + let current = GROUPS[0].id + for (const g of GROUPS) { + const el = document.getElementById(secDomId(g.id)) + if (el && el.getBoundingClientRect().top <= 180) current = g.id + } + setActiveSec(current) + } + const onScroll = () => { if (!frame) frame = requestAnimationFrame(update) } + window.addEventListener('scroll', onScroll, { passive: true }) + window.addEventListener('resize', onScroll) + update() + return () => { + window.removeEventListener('scroll', onScroll) + window.removeEventListener('resize', onScroll) + if (frame) cancelAnimationFrame(frame) + } + }, [loading, loadError]) + const mcp = splitAgentMCP(form.mcp_servers) if (loading || loadError) { return ( @@ -265,16 +302,8 @@ export default function AgentCreate() { const removeRow = (i) => updateField('folders', form.folders.filter((_, j) => j !== i)) const addRow = () => { updateField('folders', [...form.folders, { mode: 'read' }]); setChoosingFolder('new') } - const toggleSection = (sid) => setOpenSections(prev => { - const next = new Set(prev) - if (next.has(sid)) next.delete(sid) - else next.add(sid) - return next - }) - const applyTemplate = (tpl) => { setForm(prev => ({ ...prev, name: tpl.name, description: tpl.description, instructions: tpl.instructions })) - setOpenSections(prev => new Set([...prev, 'BasicInfo', 'PromptsGoals'])) } const renderSection = (sid) => { @@ -464,112 +493,147 @@ export default function AgentCreate() { onChange={(v) => updateField('mcp_servers', joinAgentMCP(v, mcp.connectors))} testId="agent-mcp" /> ) + case 'export': + return ( + <> +

{t('create.exportHelp')}

+ {savedConfig?.folder?.path &&

{folderRows(savedConfig).length > 1 ? t('folder.exportNoteMany', { n: folderRows(savedConfig).length }) : t('folder.exportNote', { path: shownPath(savedConfig.folder.path) })}

} + + + ) default: return null } } - const title = isEdit ? t('create.titleEdit', { name: savedConfig?.name || '' }) : imported ? t('create.titleImport') : t('create.titleCreate') const saveLabel = isEdit ? t('create.save') : imported ? t('create.titleImport') : t('create.titleCreate') + const headName = (form.name || '').trim() + const visibleGroups = GROUPS.map(g => ({ ...g, parts: g.parts.filter(pid => pid !== 'export' || (isEdit && savedConfig)) })) + + // A card needs the person when one of its parts does. + const groupNeeds = (g) => g.parts.some(pid => pid !== 'export' && foldInfo({ id: pid }).state === 'needs') + + const goTo = (gid) => { + const el = document.getElementById(secDomId(gid)) + if (!el) return + const calm = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches + el.scrollIntoView({ behavior: calm ? 'auto' : 'smooth', block: 'start' }) + setActiveSec(gid) + } + + const status = dirty + ? t('create.status.unsaved', { count: Math.max(diff.length, 1) }) + : t(isEdit ? 'create.status.clean' : 'create.status.new') + const toolsInfo = foldInfo({ id: 'Tools' }) + const memoryInfo = foldInfo({ id: 'Notes' }) return ( - + -
-
- - - - - + + +
+
+ +
+

{headName || t('create.header.unnamed')}

+

{isEdit ? t('create.header.modeEdit') : imported ? t('create.titleImport') : t('create.header.modeCreate')}

+ +
+
+
+ {status} + + + +
+
    +
  • {t('create.sections.ModelSettings')}{form.model || t('create.anyModel')}
  • +
  • {t('create.sections.Tools')}{toolsInfo.summary}
  • +
  • {t('create.sections.Notes')}{memoryInfo.summary}
  • +
+
+ +
+ + +
+ {imported && ( +
+

{imported.legacy ? t('create.importLegacy') : t('create.importNew')}

+ {imported.dropped.length > 0 &&

{t('create.importDropped', { keys: imported.dropped.join(', ') })}

}
)} - /> -
- {imported && ( -
-

{imported.legacy ? t('create.importLegacy') : t('create.importNew')}

- {imported.dropped.length > 0 &&

{t('create.importDropped', { keys: imported.dropped.join(', ') })}

} -
- )} - - {!isEdit && !imported && ( -
-

{t('create.startFrom')}

-
- {AGENT_TEMPLATES.map(tpl => ( - - ))} -
-
- )} - -
- {SECTIONS.map(s => { - const open = openSections.has(s.id) - const info = foldInfo(s) - return ( -
-

- -

- {open && ( -
- {renderSection(s.id)} -
- )} -
- ) - })} - {isEdit && savedConfig && ( -
-

- -

- {openSections.has('export') && ( -
-

{t('create.exportHelp')}

- {savedConfig.folder?.path &&

{folderRows(savedConfig).length > 1 ? t('folder.exportNoteMany', { n: folderRows(savedConfig).length }) : t('folder.exportNote', { path: shownPath(savedConfig.folder.path) })}

} - + {!isEdit && !imported && ( +
+

{t('create.startFrom')}

+
+ {AGENT_TEMPLATES.map(tpl => ( + + ))}
- )} -
- )} -
+
+ )} -
- - + {visibleGroups.map(g => { + // What the card says about itself: the first part that needs the + // person, and how many of its fields differ from the saved agent. + const infos = g.parts.filter(pid => pid !== 'export').map(pid => foldInfo({ id: pid })) + const needs = infos.find(i => i.state === 'needs') + const changed = infos.reduce((n, i) => n + i.changed, 0) + return ( +
+
+

{t(`create.nav.${g.id}`)}

+

{t(`create.lead.${g.id}`)}

+ {needs && {needs.summary}} + {changed > 0 && {t('create.changed', { count: changed })}} +
+ {g.parts.map(pid => { + const info = pid === 'export' ? null : foldInfo({ id: pid }) + return ( +
+ {!g.bare && ( +
+ {info && } +

{t(`create.sections.${pid}`)}

+ {info && {info.summary}} + {info && info.changed > 0 && {t('create.changed', { count: info.changed })}} +
+ )} +
+ {renderSection(pid)} +
+
+ ) + })} +
+ ) + })} +
diff --git a/core/http/react-ui/src/pages/agents.css b/core/http/react-ui/src/pages/agents.css index 1f4f9a4f94..1a1a7d040e 100644 --- a/core/http/react-ui/src/pages/agents.css +++ b/core/http/react-ui/src/pages/agents.css @@ -367,3 +367,104 @@ a.ag-run-row:hover { background: color-mix(in srgb, var(--dk-hover) 45%, transpa .ag-grants__text { display: flex; flex-direction: column; min-width: 0; } .ag-grants__title { font-weight: 500; } .ag-grants__tag { margin-left: var(--sp-2); font-size: var(--fs-small); color: var(--dk-muted); } + +/* ---- Agent editor: one page, sticky summary header, section nav ------------ + Everything is prefixed aed-. Grid children get min-width: 0 so a long name + or model id wraps instead of widening the page. */ +.aed { gap: var(--sp-3); min-width: 0; } +.aed-bar { + position: sticky; + top: 0; + z-index: 20; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: var(--sp-2) var(--sp-4); + padding: var(--sp-3) var(--sp-4); + border: 1px solid var(--dk-edge); + border-radius: var(--r-lg); + background: var(--dk-card); + box-shadow: 0 1px 0 var(--dk-edge), 0 8px 16px -12px rgba(0, 0, 0, .35); +} +.aed-bar__who { display: flex; align-items: center; gap: var(--sp-3); min-width: 0; } +.aed-avatar { + flex: none; display: grid; place-items: center; width: 40px; height: 40px; + border-radius: var(--r-md); background: var(--dk-accent-wash); color: var(--dk-accent-text); + font-weight: 600; font-size: var(--fs-small); letter-spacing: .02em; +} +.aed-avatar .lai-icon { width: 18px; height: 18px; } +.aed-bar__text { min-width: 0; } +.aed-bar__name { + margin: 0; font: 600 var(--fs-section)/var(--lh-section) var(--font-sans); color: var(--dk-text); + overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; +} +.aed-bar__name[data-empty] { color: var(--dk-muted); font-weight: 500; } +.aed-bar__sub { margin: 0; font-size: var(--fs-small); color: var(--dk-muted); } +.aed-bar__sub--status { display: none; } +.aed-bar__sub--status[data-dirty] { color: var(--dk-warn-text); font-weight: 500; } +.aed-bar__acts { display: flex; align-items: center; justify-content: flex-end; gap: var(--sp-2); flex-wrap: wrap; min-width: 0; } +.aed-status { font-size: var(--fs-small); color: var(--dk-muted); white-space: nowrap; } +.aed-status[data-dirty] { color: var(--dk-warn-text); font-weight: 500; } +.aed-chips { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: 0; padding: 0; list-style: none; min-width: 0; } +.aed-chip { + display: inline-flex; align-items: baseline; gap: 6px; min-width: 0; max-width: 100%; + padding: 3px 10px; border-radius: var(--r-xl); background: var(--dk-hover); + font-size: var(--fs-meta); +} +.aed-chip__k { color: var(--dk-muted); white-space: nowrap; } +.aed-chip__v { min-width: 0; color: var(--dk-text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +.aed-layout { display: grid; grid-template-columns: 180px minmax(0, 1fr); gap: var(--sp-6); align-items: start; min-width: 0; } +.aed-nav { position: sticky; top: 148px; display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.aed-nav__item { + display: flex; align-items: center; gap: var(--sp-2); min-height: 36px; padding: 0 var(--sp-3); + border-left: 2px solid transparent; border-radius: 0 var(--r-md) var(--r-md) 0; + color: var(--dk-muted); font-size: var(--fs-small); font-weight: 500; text-decoration: none; +} +.aed-nav__item:hover { background: var(--dk-hover); color: var(--dk-text); } +.aed-nav__item[aria-current="true"] { border-left-color: var(--dk-accent-text); background: var(--dk-accent-wash); color: var(--dk-accent-text); } +.aed-nav__dot { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--dk-ok-text); } +.aed-nav__item[data-needs] .aed-nav__dot { background: var(--dk-warn-text); } + +.aed-main { display: flex; flex-direction: column; gap: var(--sp-5); min-width: 0; } +.aed-card { + display: flex; flex-direction: column; gap: var(--sp-5); min-width: 0; + padding: var(--sp-5); border: 1px solid var(--dk-edge); border-radius: var(--r-lg); background: var(--dk-card); + scroll-margin-top: 148px; +} +.aed-card__head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px var(--sp-3); } +.aed-card__title { margin: 0; font: 600 var(--fs-section)/var(--lh-section) var(--font-sans); color: var(--dk-text); } +.aed-card__lead { margin: 0; font-size: var(--fs-small); color: var(--dk-muted); } +.aed-part { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; } +.aed-part:not([data-bare]) { padding-top: var(--sp-5); border-top: 1px solid var(--dk-edge); scroll-margin-top: 148px; } +.aed-part__head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); min-width: 0; } +.aed-part__title { margin: 0; font: 600 var(--fs-body)/var(--lh-body) var(--font-sans); color: var(--dk-text); } +.aed-part__sum { min-width: 0; font-size: var(--fs-small); color: var(--dk-muted); overflow-wrap: anywhere; } +.aed-part__body { min-width: 0; } +.aed-part__body .form-group { margin-bottom: 0; } +.aed-part__body[data-pair] { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--sp-4); } +.aed-part__body .input, .aed-part__body .textarea { width: 100%; max-width: 100%; box-sizing: border-box; } +.aed-card .ag-switches { margin-bottom: var(--sp-4); } + +@media (max-width: 960px) { + .aed-layout { grid-template-columns: minmax(0, 1fr); gap: var(--sp-3); } + .aed-nav { position: static; flex-direction: row; flex-wrap: wrap; gap: var(--sp-2); } + .aed-nav__item { flex: none; border: 1px solid var(--dk-edge); border-radius: var(--r-xl); padding: 0 var(--sp-3); } + .aed-nav__item[aria-current="true"] { border-color: var(--dk-accent-text); } + .aed-card, .aed-part:not([data-bare]) { scroll-margin-top: 140px; } +} +@media (max-width: 760px) { + .aed-bar { grid-template-columns: minmax(0, 1fr); padding: var(--sp-2) var(--sp-3); gap: var(--sp-2); } + .aed-bar__acts { justify-content: flex-start; } + .aed-bar__acts { flex-wrap: wrap; } + .aed-bar__acts > .dk-btn--primary { flex: 1 1 auto; justify-content: center; } + .aed-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } + .aed-bar__sub--status { display: block; } + .aed-chips { display: none; } + .aed-avatar { width: 32px; height: 32px; } + .aed-bar__sub:not(.aed-bar__sub--status) { display: none; } + .aed-card { padding: var(--sp-4); } + .aed-part__body[data-pair] { grid-template-columns: minmax(0, 1fr); } +} +@media (prefers-reduced-motion: reduce) { .aed-nav__item { transition: none; } } +.aed-card__needs { font-size: var(--fs-meta); font-weight: 500; color: var(--dk-warn-text); }