diff --git a/frontend/src/v2/__tests__/V2AgentMemory.test.tsx b/frontend/src/v2/__tests__/V2AgentMemory.test.tsx index 45e8bbd04..8be13898c 100644 --- a/frontend/src/v2/__tests__/V2AgentMemory.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentMemory.test.tsx @@ -108,8 +108,8 @@ describe('V2AgentMemory', () => { ); await waitFor(() => expect(screen.getByRole('heading', { name: /Journal/ })).toBeInTheDocument()); // Rules and How I work each carry the long_term time; Journal has none. - expect(document.querySelectorAll('.v2-agent-memory__gmeta')).toHaveLength(2); - expect(document.querySelectorAll('.v2-agent-memory__mnote .v2-agent-memory__gmeta')).toHaveLength(0); + expect(document.querySelectorAll('.v2m-saved')).toHaveLength(2); + expect(document.querySelectorAll('.v2m-mnote .v2m-saved')).toHaveLength(0); }); it('keeps the agent header, tabs and nav when the viewer cannot read entries, with the empty state inside', async () => { @@ -123,8 +123,57 @@ describe('V2AgentMemory', () => { ); await waitFor(() => expect(screen.getByRole('status')).toBeInTheDocument()); expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'Profile' })).toBeInTheDocument(); - expect(document.querySelector('.v2-agent-memory__nav')).not.toBeNull(); - expect(document.querySelector('.v2-agent-memory__body [role="status"]')).not.toBeNull(); + expect(screen.getByRole('button', { name: 'Profile' })).toBeInTheDocument(); + expect(document.querySelector('.v2m-mnav')).not.toBeNull(); + expect(document.querySelector('.v2m-body [role="status"]')).not.toBeNull(); + }); + + it('Talk to opens the agent room and navigates to it', async () => { + axios.get.mockResolvedValue({ data: { viewerRole: 'owner', totalEntries: 1, sections: [section('daily', 1)] } }); + axios.post.mockResolvedValue({ data: { room: { _id: 'room1' } } }); + render( + + + } /> + room opened} /> + + , + ); + await waitFor(() => expect(screen.getByRole('heading', { name: /Journal/ })).toBeInTheDocument()); + fireEvent.click(screen.getByRole('button', { name: /maya-claude/ })); + await waitFor(() => expect(screen.getByText('room opened')).toBeInTheDocument()); + expect(axios.post).toHaveBeenCalledWith('/api/agents/runtime/room', { agentName: 'maya-claude', instanceId: 'default' }); + }); + + it('shows an error and stays on the page when the room cannot be opened', async () => { + axios.get.mockResolvedValue({ data: { viewerRole: 'owner', totalEntries: 1, sections: [section('daily', 1)] } }); + axios.post.mockRejectedValue(new Error('500')); + render( + + + } /> + + , + ); + await waitFor(() => expect(screen.getByRole('heading', { name: /Journal/ })).toBeInTheDocument()); + fireEvent.click(screen.getByRole('button', { name: /maya-claude/ })); + await waitFor(() => expect(screen.getByRole('alert')).toBeInTheDocument()); + expect(screen.getByRole('button', { name: /maya-claude/ })).not.toBeDisabled(); + }); + + it('marks one nav item current: the first with notes, then the one clicked', async () => { + axios.get.mockResolvedValue({ data: { viewerRole: 'owner', totalEntries: 3, sections: [named('long_term', ['Rules', 'Host notes']), section('daily', 1)] } }); + render( + + + } /> + + , + ); + await waitFor(() => expect(screen.getByRole('heading', { name: /Journal/ })).toBeInTheDocument()); + const current = () => Array.from(document.querySelectorAll('.v2m-mnav a[aria-current]')).map((a) => a.textContent); + expect(current()).toEqual(['Rules1']); + fireEvent.click(screen.getByRole('link', { name: /Journal/ })); + expect(current()).toEqual(['Journal1']); }); }); diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 19c13c8e3..0b2a32b11 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -5099,28 +5099,35 @@ describe('agent Identity editor', () => { }); }); -describe('agent Memory tab (TASK-690 slice 1 and 2)', () => { - const css = read('../agents/v2-agent-settings.css'); - const desktop = blockContaining(css, '@media (min-width: 768px)', '.v2-agent-memory'); - const phone = blockContaining(css, '@media (max-width: 767px)', '.v2-agent-memory__nav ul'); +describe('agent Memory tab (TASK-732 lift of d215 #s-memory)', () => { + const css = read('../agents/V2AgentMemory.css'); + const desktop = blockContaining(css, '@media (min-width: 768px)', '.v2m-grp > summary'); + const phone = blockContaining(css, '@media (max-width: 767px)', '.v2m-mnav ul'); test('desktop keeps the group nav beside the notes at 200px', () => { - expect(ruleBody(css, '.v2-agent-memory')).toContain('display: grid'); - expect(ruleBody(css, '.v2-agent-memory', { within: desktop })).toContain('grid-template-columns: 200px minmax(0, 1fr)'); + expect(ruleBody(css, '.v2m-mem')).toContain('grid-template-columns: 200px minmax(0, 1fr)'); }); test('the page spans the pane instead of a centred column', () => { - const page = ruleBody(css, '.v2-agent-memory-page'); + const page = ruleBody(css, '.v2m-page'); expect(page).toContain('width: 100%'); - expect(page).toContain('max-width: none'); expect(page).toContain('align-self: stretch'); + expect(page).not.toContain('max-width'); + }); + test('the header is the frame agent bar: 76px tall, 28px left padding', () => { + const ph = ruleBody(css, '.v2m-page .v2p-ph'); + expect(ph).toContain('height: 76px'); + expect(ph).toContain('padding-left: 28px'); }); - test('phone hides the group nav and makes the groups collapsible', () => { - expect(ruleBody(css, '.v2-agent-memory__nav ul', { within: phone })).toContain('display: none'); - expect(ruleBody(css, '.v2-agent-memory__gtitle', { within: phone })).toContain('cursor: pointer'); + test('phone hides the group nav and makes the groups collapsible rows', () => { + expect(ruleBody(css, '.v2m-mnav ul', { within: phone })).toContain('display: none'); + expect(ruleBody(css, '.v2m-grp > summary', { within: phone })).toContain('cursor: pointer'); + expect(ruleBody(css, '.v2m-gtitle', { within: phone })).toContain('height: 44px'); }); test('desktop group titles do not take pointer input, and the chevron is phone-only', () => { - const titles = blockContaining(css, '@media (min-width: 768px)', '.v2-agent-memory__gtitle'); - expect(ruleBody(css, '.v2-agent-memory__gtitle', { within: titles })).toContain('pointer-events: none'); - expect(ruleBody(css, '.v2-agent-memory__chev')).toContain('display: none'); + expect(ruleBody(css, '.v2m-grp > summary', { within: desktop })).toContain('pointer-events: none'); + expect(ruleBody(css, '.v2m-gtitle .v2m-chev')).toContain('display: none'); + }); + test('the old memory rules are gone from the agent settings sheet', () => { + expect(read('../agents/v2-agent-settings.css')).not.toContain('v2-agent-memory'); }); }); diff --git a/frontend/src/v2/__tests__/v2-phone-shell.test.ts b/frontend/src/v2/__tests__/v2-phone-shell.test.ts index 390f6fd50..de73eae1b 100644 --- a/frontend/src/v2/__tests__/v2-phone-shell.test.ts +++ b/frontend/src/v2/__tests__/v2-phone-shell.test.ts @@ -41,16 +41,21 @@ describe('D223 phone shell', () => { test('agent settings and memory pages get the same 52px bar inside the 760px query', () => { const agentCss = fs.readFileSync(path.join(__dirname, '../agents/v2-agent-settings.css'), 'utf8'); const tail = agentCss.slice(agentCss.indexOf('D223 (TASK-730)')); - expect(tail).toMatch(/\.v2-agent-settings > \.v2-page-head,\s*\.v2-agent-settings > \.v2-agent-memory__head\s*\{[^}]*min-height:\s*52px/); + expect(tail).toMatch(/\.v2-agent-settings > \.v2-page-head\s*\{[^}]*min-height:\s*52px/); + expect(agentCss).not.toContain('v2-agent-memory'); const at = agentCss.indexOf('D223 (TASK-730)'); expect(agentCss.slice(0, at).lastIndexOf('@media')).toBe(agentCss.lastIndexOf('@media (max-width: 760px)', at)); }); - test('the memory bar lets the name shrink and drops the tab nav margin so it stays 52px', () => { - const agentCss = fs.readFileSync(path.join(__dirname, '../agents/v2-agent-settings.css'), 'utf8'); - const tail = agentCss.slice(agentCss.indexOf('D223 (TASK-730)')); - expect(tail).toMatch(/__ident\s*\{\s*min-width:\s*0/); - expect(tail).toMatch(/__seg\s*\{[^}]*margin-top:\s*0/); + test('the memory bar is the same 52px phone bar, in the memory sheet on its v2m names', () => { + const memCss = fs.readFileSync(path.join(__dirname, '../agents/V2AgentMemory.css'), 'utf8'); + const start = memCss.indexOf('@media (max-width: 767px)'); + const phoneBlock = memCss.slice(start, memCss.indexOf('@media', start + 1)); + expect(phoneBlock).not.toContain('min-width: 768px'); + expect(phoneBlock).toMatch(/\.v2m-page \.v2p-ph\s*\{[^}]*min-height:\s*52px[^}]*flex-wrap:\s*nowrap/); + expect(phoneBlock).toMatch(/\.v2m-ahead\s*\{\s*min-width:\s*0/); + expect(phoneBlock).toMatch(/\.v2m-ahead h1\s*\{[^}]*white-space:\s*nowrap[^}]*text-overflow:\s*ellipsis/); + expect(phoneBlock).toMatch(/\.v2p-seg\s*\{[^}]*flex:\s*none/); }); test('the settings head spans the pane and only the body scrolls', () => { diff --git a/frontend/src/v2/agents/V2AgentMemory.css b/frontend/src/v2/agents/V2AgentMemory.css new file mode 100644 index 000000000..558308b6c --- /dev/null +++ b/frontend/src/v2/agents/V2AgentMemory.css @@ -0,0 +1,90 @@ +/* Agent memory (TASK-732, D223 lift). Lifted from docs/design/artboards/d215 + #s-memory: the artboard's .ahead, .mem, .mnav, .gtitle, .list, .mnote, .jday, + details.grp and .info blocks, with the frame's values and the --v2-* token + mapping from primitives.css. Class names keep the frame's names under a v2m- + prefix so they cannot collide with existing v2 rules. The header, segment, + list and group title come from the v2p- primitives. */ + +.v2m-page { width: 100%; min-width: 0; align-self: stretch; display: flex; flex-direction: column; box-sizing: border-box; } +.v2m-page .v2p-ph { height: 76px; padding-left: 28px; } +.v2m-ahead { display: block; min-width: 0; } +.v2m-ahead h1 { font: 800 24px/1 var(--v2-font-display); letter-spacing: -0.03em; margin: 0; white-space: nowrap; } +.v2m-ahead .st { display: flex; align-items: center; gap: 6px; font: 500 11.5px/1 var(--v2-font-mono); color: var(--v2-text-muted); margin: 5px 0 0; } +.v2m-pbody { flex: 1; min-height: 0; padding: 24px 28px; box-sizing: border-box; } + +.v2m-mem { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 24px; align-items: start; } +.v2m-mnav { display: grid; gap: 2px; align-content: start; } +.v2m-mnav ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; } +.v2m-mnav a, +.v2m-mnav .v2m-navitem { display: flex; align-items: center; gap: 9px; height: 34px; padding: 0 10px; border-radius: var(--v2-r-8); font-size: 13.5px; color: var(--v2-ink-2); text-decoration: none; } +.v2m-mnav a.on { background: var(--v2-surface-tint); font-weight: 600; color: var(--v2-text-primary); } +.v2m-mnav a:hover { background: var(--v2-surface-tint); } +.v2m-mnav a:focus-visible { box-shadow: var(--v2-focus-ring); outline: none; } +.v2m-mnav svg.i { width: 16px; height: 16px; flex: none; color: var(--v2-text-secondary); } +.v2m-mnav .mo { margin-left: auto; font-size: 11px; color: var(--v2-text-muted); } +.v2m-search { display: flex; align-items: center; gap: 7px; height: 32px; margin-top: 12px; box-sizing: border-box; padding: 0 8px 0 10px; min-width: 0; border-radius: var(--v2-r-10); background: var(--v2-page-bg); box-shadow: inset 0 0 0 1px var(--v2-border-soft); color: var(--v2-text-placeholder); font-size: 13px; } +.v2m-search svg.i { width: 15px; height: 15px; flex: none; } +.v2m-search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--v2-text-primary); font: inherit; } +.v2m-search:focus-within { box-shadow: var(--v2-focus-ring); } +.v2m-body { display: grid; min-width: 0; align-content: start; } + +/* Group title: the frame's .gtitle, with the saved time and the info tip at the end. */ +.v2m-grp { margin-bottom: 18px; } +.v2m-grp > summary { list-style: none; } +.v2m-grp > summary::-webkit-details-marker { display: none; } +.v2m-gtitle { display: flex; align-items: center; gap: 8px; margin: 0 4px 8px; font: 500 11.5px/1 var(--v2-font-mono); color: var(--v2-text-muted); } +.v2m-gheading { display: inline-flex; align-items: center; gap: 8px; } +.v2m-gtitle svg.i { width: 13px; height: 13px; flex: none; } +.v2m-gtitle .v2m-saved { margin-left: auto; font-weight: 400; } +.v2m-gtitle .v2m-chev { display: none; } + +.v2m-info { all: unset; cursor: help; position: relative; display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; color: var(--v2-text-placeholder); vertical-align: middle; } +.v2m-info svg.i { width: 15px; height: 15px; } +.v2m-info:hover, .v2m-info:focus-visible { color: var(--v2-text-primary); } +.v2m-info .tip { display: none; position: absolute; z-index: 30; top: 24px; right: -8px; width: 260px; padding: 10px 12px; border-radius: var(--v2-r-10); background: var(--v2-surface); box-shadow: var(--v2-shadow-pop); font: 400 12.5px/1.45 var(--v2-font); color: var(--v2-text-secondary); text-align: left; white-space: normal; } +.v2m-info:hover .tip, .v2m-info:focus-visible .tip { display: block; } + +/* Notes: the frame's .list rows. */ +.v2m-list { list-style: none; margin: 0; padding: 0; overflow: hidden; } +.v2m-mnote { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 10px; align-items: start; padding: 11px 14px; border-top: 1px solid var(--v2-border-soft); } +.v2m-mnote:first-child { border-top: 0; } +.v2m-mnote > svg.i { width: 16px; height: 16px; margin-top: 2px; color: var(--v2-text-secondary); } +.v2m-mnote .tx { font-size: 13.5px; line-height: 20px; color: var(--v2-text-primary); overflow-wrap: anywhere; } +.v2m-mnote .snip { font-size: 12.5px; line-height: 18px; color: var(--v2-text-secondary); overflow-wrap: anywhere; } +.v2m-jday { display: grid; grid-template-columns: 70px minmax(0, 1fr); gap: 12px; padding: 10px 14px; border-top: 1px solid var(--v2-border-soft); font-size: 13.5px; color: var(--v2-ink-2); } +.v2m-jday:first-child { border-top: 0; } +.v2m-jday .mo { font: 500 11.5px/20px var(--v2-font-mono); color: var(--v2-text-muted); } + +.v2m-state { margin: 0; padding: 14px; font-size: 13.5px; color: var(--v2-text-secondary); } + +/* Phone (frame 390): search first, groups become the frame's details.grp rows. */ +@media (max-width: 767px) { + /* D223 (TASK-730): the same 52px phone top bar feature pages get. The segment + stays in the bar, the name shrinks and ellipsizes. */ + .v2m-page .v2p-ph { height: auto; min-height: 52px; flex-wrap: nowrap; gap: 8px; padding: 0 10px 0 12px; } + .v2m-page .v2p-ph .sp { display: none; } + .v2m-page .v2p-ph .v2m-ahead { min-width: 0; flex: 1; } + .v2m-page .v2p-ph .v2m-ahead h1 { margin: 0; white-space: nowrap; font: 800 21px/1 var(--v2-font-display); letter-spacing: -0.03em; overflow: hidden; text-overflow: ellipsis; } + .v2m-page .v2p-ph .v2m-ahead .st { display: none; } + .v2m-page .v2p-ph .v2p-seg { flex: none; margin-top: 0; } + .v2m-pbody { padding: 12px; } + .v2m-mem { grid-template-columns: minmax(0, 1fr); gap: 10px; } + .v2m-mnav ul { display: none; } + .v2m-search { margin-top: 0; } + .v2m-body { gap: 10px; } + .v2m-grp { margin-bottom: 0; background: var(--v2-surface); border-radius: var(--v2-r-14); box-shadow: var(--v2-shadow-pane); } + .v2m-grp > summary { cursor: pointer; } + .v2m-gtitle { height: 44px; margin: 0; padding: 0 12px 0 14px; font: 600 13.5px/1 var(--v2-font); color: var(--v2-text-primary); } + .v2m-gtitle svg.i { width: 16px; height: 16px; color: var(--v2-text-secondary); } + .v2m-gtitle .v2m-saved { font: 500 11px/1 var(--v2-font-mono); color: var(--v2-text-muted); } + .v2m-gtitle .v2m-info { display: none; } + .v2m-gtitle .v2m-chev { display: inline-block; margin-left: 4px; color: var(--v2-text-placeholder); transition: transform 160ms; } + .v2m-grp[open] > summary .v2m-chev { transform: rotate(180deg); } + .v2m-grp .v2m-list { border-radius: 0 0 var(--v2-r-12) var(--v2-r-12); box-shadow: none; background: transparent; } +} + +/* Desktop: the groups are plain titles, not collapsible; only the info tip takes input. */ +@media (min-width: 768px) { + .v2m-grp > summary { pointer-events: none; cursor: default; } + .v2m-grp > summary .v2m-info { pointer-events: auto; } +} diff --git a/frontend/src/v2/agents/V2AgentMemory.tsx b/frontend/src/v2/agents/V2AgentMemory.tsx index 954e0262f..272cbf173 100644 --- a/frontend/src/v2/agents/V2AgentMemory.tsx +++ b/frontend/src/v2/agents/V2AgentMemory.tsx @@ -1,10 +1,11 @@ import React, { useEffect, useState } from 'react'; -import { Link, useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import axios from '../../utils/axiosConfig'; import V2Avatar from '../components/V2Avatar'; import { localizeRelativeTime } from '../utils/localizeRelativeTime'; -import './v2-agent-settings.css'; +import '../primitives/primitives.css'; +import './V2AgentMemory.css'; // Owner/admin memory index, as served by GET /api/agent-memory/:agentName/:instanceId. interface MemoryNote { header: string; snippet: string } @@ -64,8 +65,9 @@ export const groupSections = (sections: MemorySection[]) => { }; // Frame icons (docs/design/artboards/d215, #s-memory): stroke-only, 1.8 weight. -const ICON_PATHS: Record<'pin' | 'brain' | 'cal' | 'search' | 'info' | 'chev', React.ReactNode> = { +const ICON_PATHS: Record<'pin' | 'brain' | 'cal' | 'search' | 'info' | 'chev' | 'chat', React.ReactNode> = { chev: , + chat: , pin: , brain: , cal: <>, @@ -73,7 +75,7 @@ const ICON_PATHS: Record<'pin' | 'brain' | 'cal' | 'search' | 'info' | 'chev', R info: <>, }; const MemIcon: React.FC<{ name: keyof typeof ICON_PATHS; size?: number; className?: string }> = ({ name, size = 16, className }) => ( - @@ -82,36 +84,40 @@ const GROUP_ICON: Record = { rules: 'pin', how // The D215 frame without entries: same nav and body, the state sits inside it. const MemoryShell: React.FC<{ loading: boolean; message: string; loadingLabel: string; query: string; setQuery: (q: string) => void; searchLabel: string; navLabel: string }> = ({ loading, message, loadingLabel, query, setQuery, searchLabel, navLabel }) => ( -
- -
-

{loading ? loadingLabel : message}

+ +
+

{loading ? loadingLabel : message}

); const V2AgentMemory: React.FC = () => { const { t } = useTranslation(); + const navigate = useNavigate(); const { agentName = '', instanceId = '' } = useParams<{ agentName: string; instanceId: string }>(); const [index, setIndex] = useState(null); const [state, setState] = useState<'loading' | 'ready' | 'denied'>('loading'); const [query, setQuery] = useState(''); + const [activeGroup, setActiveGroup] = useState(null); + const [openingRoom, setOpeningRoom] = useState(false); + const [roomError, setRoomError] = useState(false); // Public header facts (name, avatar, pod count). Readable by any viewer, so the // shell renders the same for owners, admins and everyone else. const [head, setHead] = useState<{ displayName: string; profilePicture?: string; pods: number } | null>(null); @@ -143,25 +149,56 @@ const V2AgentMemory: React.FC = () => { return () => { cancelled = true; }; }, [agentName, instanceId]); + // Same idempotent room endpoint as the profile and Your Team entry points: + // returns the existing DM or opens one. + const talkTo = async () => { + setOpeningRoom(true); + setRoomError(false); + try { + const res = await axios.post<{ room?: { _id?: string } }>('/api/agents/runtime/room', { agentName, instanceId: instanceId || 'default' }); + const roomId = res.data?.room?._id; + if (!roomId) throw new Error('Agent room not returned'); + navigate(`/v2/pods/${roomId}`); + } catch { + setRoomError(true); + setOpeningRoom(false); + } + }; + const profileHref = `/v2/agent/${encodeURIComponent(agentName)}/${encodeURIComponent(instanceId)}`; const settingsHref = `/v2/agents/${encodeURIComponent(agentName)}/${encodeURIComponent(instanceId)}`; + const memoryLabel = t('agentSettings.memory', { defaultValue: 'Memory' }); + const searchLabel = t('agentProfile.memory.search', { defaultValue: 'Search memory' }); return ( -
-
+
+
-
+

{head?.displayName || agentName}

- {head &&

{t('agentProfile.hero.activeInPods', { count: head.pods })}

} + {head &&

{t('agentProfile.hero.activeInPods', { count: head.pods })}

}
-
-
- {state === 'ready' && index ? null : } + {roomError &&

{t('agentProfile.hero.openRoomFailed')}

} +
+ {state === 'ready' && index ? null : } {state === 'ready' && index && (() => { const needle = query.trim().toLowerCase(); const groups = groupSections(index.sections) @@ -176,62 +213,65 @@ const V2AgentMemory: React.FC = () => { })) .filter((sec) => sec.notes.length > 0), })); + const firstShown = groups.find((g) => g.sections.length > 0)?.id; + // The frame marks one nav item as current: the one clicked, else the first with notes. + const current = groups.find((g) => g.id === activeGroup && g.sections.length > 0)?.id ?? firstShown; return ( -
- -
+ +
{groups.filter((g) => g.sections.length > 0).map((group) => { // One time per group: the newest section update in it. Never per note. const updated = group.sections.map((s) => s.updatedAt).filter(Boolean).sort().pop() ?? null; return ( -
- - +
+ + {group.label} {updated && ( - + {t('agentProfile.memory.sectionUpdated', { defaultValue: 'Section updated {{when}}', when: localizeRelativeTime(updated, t) })} )} - + {group.id === firstShown && ( + + )} + -
    +
      {group.sections.flatMap((sec) => sec.notes).map((n, i) => ( group.id === 'journal' ? ( -
    • - {n.header} +
    • + {n.header} {n.snippet}
    • ) : ( -
    • +
    • -
      {n.header}
      - {n.snippet &&
      {n.snippet}
      } +
      {n.header}
      + {n.snippet &&
      {n.snippet}
      }
    • ) diff --git a/frontend/src/v2/agents/v2-agent-settings.css b/frontend/src/v2/agents/v2-agent-settings.css index f85c977e0..7a90d1ee1 100644 --- a/frontend/src/v2/agents/v2-agent-settings.css +++ b/frontend/src/v2/agents/v2-agent-settings.css @@ -140,82 +140,10 @@ .v2-agent-settings__key-meta { display: grid; gap: 4px; } /* D223 (TASK-730): the same 52px phone top bar feature pages get from .v2-feature__header. The page pads 16px, so the bar bleeds out of it. */ - .v2-agent-settings > .v2-page-head, - .v2-agent-settings > .v2-agent-memory__head { + .v2-agent-settings > .v2-page-head { display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; min-height: 52px; margin: -16px -16px 12px; padding: 0 10px 0 12px; border-bottom: 1px solid var(--v2-border-soft); } - .v2-agent-settings > .v2-page-head h1, - .v2-agent-settings > .v2-agent-memory__head .v2-agent-memory__ident { min-width: 0; } - .v2-agent-settings > .v2-agent-memory__head .v2-agent-memory__seg { flex: none; margin-top: 0; } - .v2-agent-settings > .v2-agent-memory__head .v2-agent-memory__ident h1 { - margin: 0; font: 800 21px/1 var(--v2-font-display); letter-spacing: -0.03em; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - } -} - -/* Agent memory tab (TASK-690 slice 1): frame metrics from docs/design/artboards/d215 - #s-memory at 1440. Group nav at 200px beside the lists, stacked below 768. - Tags and recency are not drawn: the API has no data for them (slice 2 adds only - the section-level update time). */ -.v2-agent-memory { display: grid; gap: 20px; } -@media (min-width: 768px) { - .v2-agent-memory { grid-template-columns: 200px minmax(0, 1fr); gap: 24px; align-items: start; } -} -.v2-agent-memory__nav { display: grid; gap: 2px; align-content: start; } -.v2-agent-memory__nav ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; } -.v2-agent-memory__nav a { display: flex; align-items: center; gap: 9px; height: 34px; padding: 0 10px; border-radius: var(--v2-r-8); color: var(--v2-text-secondary); text-decoration: none; font-size: 13.5px; } -.v2-agent-memory__nav a:hover { background: var(--v2-surface-hover); } -.v2-agent-memory__icon { flex: none; } -.v2-agent-memory__nav a .v2-agent-memory__icon { width: 16px; height: 16px; } -.v2-agent-memory__count { margin-left: auto; font-size: 11px; color: var(--v2-text-tertiary); } -.v2-agent-memory__search { display: flex; align-items: center; gap: 7px; height: 34px; margin-top: 12px; box-sizing: border-box; padding: 0 8px 0 10px; border-radius: var(--v2-r-10); border: 1px solid var(--v2-border-soft); background: var(--v2-surface); box-shadow: var(--v2-shadow-pane); color: var(--v2-text-tertiary); font-size: 13px; } -.v2-agent-memory__search .v2-agent-memory__icon { width: 15px; height: 15px; } -.v2-agent-memory__search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--v2-text-primary); font: inherit; } -.v2-agent-memory__search:focus-within { box-shadow: var(--v2-focus-ring); } -.v2-agent-memory__body { display: grid; min-width: 0; } -.v2-agent-memory__group { margin-bottom: 18px; } -.v2-agent-memory__gtitle { display: flex; align-items: center; gap: 8px; margin: 0 4px 8px; font: 500 11.5px/1 var(--v2-font-mono); color: var(--v2-text-tertiary); list-style: none; } -.v2-agent-memory__gtitle::-webkit-details-marker { display: none; } -.v2-agent-memory__gheading { display: inline-flex; align-items: center; gap: 8px; } -.v2-agent-memory__gtitle .v2-agent-memory__icon { width: 13px; height: 13px; } -.v2-agent-memory__gmeta { margin-left: auto; font-weight: 400; font-size: 11px; } -/* Desktop: the groups are not collapsible, so the summary is a plain title. */ -.v2-agent-memory__chev { display: none; } -@media (min-width: 768px) { - .v2-agent-memory__gtitle { pointer-events: none; } -} -/* Phone: search above the groups, group list hidden, groups collapsible. */ -@media (max-width: 767px) { - .v2-agent-memory__nav ul { display: none; } - .v2-agent-memory__gtitle { cursor: pointer; pointer-events: auto; } - .v2-agent-memory__chev { display: inline-block; margin-left: 4px; transition: transform 0.15s; } - .v2-agent-memory__group[open] > .v2-agent-memory__gtitle .v2-agent-memory__chev { transform: rotate(180deg); } -} -.v2-agent-memory__list { list-style: none; margin: 0; padding: 0; border-radius: var(--v2-r-12); border: 1px solid var(--v2-border-soft); background: var(--v2-surface); overflow: hidden; } -.v2-agent-memory__list > li { border-top: 1px solid var(--v2-border-soft); } -.v2-agent-memory__list > li:first-child { border-top: 0; } -.v2-agent-memory__mnote { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 10px; align-items: start; padding: 11px 14px; } -.v2-agent-memory__mnote .v2-agent-memory__icon { width: 16px; height: 16px; color: var(--v2-text-secondary); margin-top: 2px; } -.v2-agent-memory__tx { font-size: 13.5px; line-height: 20px; color: var(--v2-text-primary); } -.v2-agent-memory__snip { font-size: 12.5px; line-height: 18px; color: var(--v2-text-secondary); } -.v2-agent-memory__jday { display: grid; grid-template-columns: 70px minmax(0, 1fr); gap: 12px; padding: 10px 14px; font-size: 13.5px; color: var(--v2-text-secondary); } -.v2-agent-memory__jdate { font-size: 11.5px; color: var(--v2-text-tertiary); line-height: 20px; } - -/* Memory page header: the D215 agent bar (avatar, name, pods, segment). */ -.v2-agent-memory__head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 20px; } -.v2-agent-memory__ident h1 { margin: 0; font-size: 20px; } -.v2-agent-memory__ident p { margin: 2px 0 0; font-size: 12.5px; color: var(--v2-text-tertiary); } -.v2-agent-memory__seg { margin-left: auto; margin-bottom: 0; } -.v2-agent-memory__navitem { display: flex; align-items: center; gap: 9px; height: 34px; padding: 0 10px; font-size: 13.5px; color: var(--v2-text-tertiary); } - -/* Frame width: the agent bar spans the pane, the body fills it (nav left, sections across). */ -.v2-agent-memory-page { width: 100%; max-width: none; align-self: stretch; box-sizing: border-box; } -.v2-agent-memory-page .v2-agent-memory__head { margin: 0; padding: 12px 28px; min-height: 76px; border-bottom: 1px solid var(--v2-border-soft); box-sizing: border-box; } -.v2-agent-memory-page .v2-agent-memory__seg { margin-top: 0; border-bottom: 0; } -.v2-agent-memory-page__body { width: 100%; padding: 20px 28px; box-sizing: border-box; } -@media (max-width: 767px) { - .v2-agent-memory-page .v2-agent-memory__head { padding: 12px 16px; } - .v2-agent-memory-page__body { padding: 16px; } + .v2-agent-settings > .v2-page-head h1 { min-width: 0; } }