diff --git a/graph-ui/src/app/AtlasChrome.test.tsx b/graph-ui/src/app/AtlasChrome.test.tsx index 4bbbd853b..de834f9f1 100644 --- a/graph-ui/src/app/AtlasChrome.test.tsx +++ b/graph-ui/src/app/AtlasChrome.test.tsx @@ -89,28 +89,34 @@ describe('AtlasChrome', () => { expect(onOpenConfig).not.toHaveBeenCalled(); }); - it('shows the selected model subtly in the agent status button and opens its configuration directly', async () => { + it('shows a robot with a status lamp and keeps the model name in the tooltip and the label', async () => { const settings = vi.fn(), toggle = vi.fn(); await render(props({ agentState: 'active', agentModelName: 'Qwen 3.5 0.8B', onOpenBrowserAi: settings, onToggleBrowserAi: toggle })); - const button = container.querySelector('[aria-label="Local agent settings"]')!; - expect(button.textContent).toContain('Agent active'); - expect(button.querySelector('small')?.textContent).toBe('Qwen 3.5 0.8B'); + const button = container.querySelector('.atlas-browser-ai-action')!; + expect(button.textContent).toBe(''); + expect(button.querySelector('svg[aria-hidden="true"]')).not.toBeNull(); + expect(button.querySelector(':scope > span[aria-hidden="true"]')).not.toBeNull(); + expect(button.dataset.state).toBe('active'); + expect(button.getAttribute('aria-label')).toBe('Local agent settings: Agent active, Qwen 3.5 0.8B'); + expect(button.title).toBe('Agent active · Qwen 3.5 0.8B. Configure the browser-local agent.'); await act(async () => button.click()); expect(settings).toHaveBeenCalledOnce(); expect(toggle).not.toHaveBeenCalled(); expect(container.querySelector('[role="menu"]')).toBeNull(); }); - it('omits the model label when no model is selected', async () => { + it('omits the model from the label when no model is selected', async () => { await render(props({ agentState: 'off', onOpenBrowserAi: vi.fn() })); - expect(container.querySelector('.atlas-agent-model-name')).toBeNull(); + expect(container.querySelector('.atlas-browser-ai-action')?.getAttribute('aria-label')).toBe('Local agent settings: Agent off'); }); - it.each([['off', 'Enable agent'], ['active', 'Agent active'], ['busy', 'Agent working'], ['loading', 'Agent loading'], ['error', 'Agent error']] as const)('shows truthful local agent state %s and opens settings beside the project', async (agentState, label) => { + it.each([['off', 'Agent off'], ['active', 'Agent active'], ['busy', 'Agent working'], ['loading', 'Agent loading'], ['error', 'Agent error']] as const)('shows truthful local agent state %s and opens settings beside the project', async (agentState, label) => { const settings = vi.fn(); await render(props({ agentState, onOpenBrowserAi: settings, projectSwitcher: })); - const button = container.querySelector('[aria-label="Local agent settings"]')!; - expect(button.textContent).toBe(label); + const button = container.querySelector('.atlas-browser-ai-action')!; + expect(button.dataset.state).toBe(agentState); + expect(button.getAttribute('aria-label')).toBe(`Local agent settings: ${label}`); + expect(button.title).toContain(label); expect(button.parentElement?.previousElementSibling?.textContent).toBe('Project'); await act(async () => button.click()); expect(settings).toHaveBeenCalledOnce(); @@ -124,7 +130,7 @@ describe('AtlasChrome', () => { expect(group?.querySelectorAll('button')).toHaveLength(2); const draft = container.querySelector('[aria-label="Chat draft"]')!; draft.value = 'retained draft'; - const settingsButton = group?.querySelector('[aria-label="Local agent settings"]')!; + const settingsButton = group?.querySelector('.atlas-browser-ai-action')!; const openButton = group?.querySelector('[aria-label="Open chat"]')!; expect(openButton.getAttribute('aria-expanded')).toBe('false'); await act(async () => settingsButton.click()); diff --git a/graph-ui/src/app/AtlasChrome.tsx b/graph-ui/src/app/AtlasChrome.tsx index bc8cfc157..becdfc58e 100644 --- a/graph-ui/src/app/AtlasChrome.tsx +++ b/graph-ui/src/app/AtlasChrome.tsx @@ -677,13 +677,15 @@ export default function AtlasChrome(props: AtlasChromeProps): JSX.Element { ))} {props.projectSwitcher} - {(props.onOpenBrowserAi !== undefined || props.onToggleBrowserAi !== undefined) &&
- {props.onOpenBrowserAi !== undefined && } - {props.onToggleBrowserAi !== undefined && }
} diff --git a/graph-ui/src/app/agent-status.css b/graph-ui/src/app/agent-status.css index 278217f1e..4f1a442a2 100644 --- a/graph-ui/src/app/agent-status.css +++ b/graph-ui/src/app/agent-status.css @@ -7,10 +7,32 @@ } .atlas-agent-controls .atlas-browser-ai-action { + position: relative; + justify-content: center; + min-width: 36px; + padding: 6px 8px; border: 0; border-radius: 5px 0 0 5px; } +.atlas-agent-controls .atlas-browser-ai-action:hover { + background: var(--atlas-select); + color: var(--atlas-fg); +} + +.atlas-agent-controls .atlas-browser-ai-action > span { + position: absolute; + top: 4px; + right: 5px; + width: 7px; + height: 7px; + box-shadow: 0 0 0 2px var(--atlas-bg); +} + +.atlas-agent-controls .atlas-browser-ai-action[data-state=active] > span { + box-shadow: 0 0 0 2px var(--atlas-bg), 0 0 8px var(--atlas-glow); +} + .atlas-agent-chat-toggle { display: grid; place-items: center; @@ -32,13 +54,3 @@ .atlas-agent-chat-toggle-pane { fill: transparent; } .atlas-agent-chat-toggle[aria-expanded="true"] .atlas-agent-chat-toggle-pane { fill: currentColor; opacity: .3; } .atlas-agent-controls button:focus-visible { outline: 2px solid var(--atlas-phosphor); outline-offset: 2px; } - -.atlas-agent-model-name { - max-width: 22ch; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - color: var(--atlas-dim); - font-size: 10px; - font-weight: 400; -} diff --git a/graph-ui/src/app/workspace-strings.ts b/graph-ui/src/app/workspace-strings.ts index 4b3fe5c59..f4b888f38 100644 --- a/graph-ui/src/app/workspace-strings.ts +++ b/graph-ui/src/app/workspace-strings.ts @@ -48,6 +48,13 @@ export const workspaceStrings = { changeLater: 'Switch workspaces at any time.', localAi: 'Set up browser AI', browserAi: 'Chat', + agentControls: 'Local agent', + agentStatus: { off: 'Agent off', loading: 'Agent loading', active: 'Agent active', busy: 'Agent working', error: 'Agent error' }, + /** The header shows a robot and a lamp; state and model are read out and shown on hover. */ + agentSettings: (status: string, model?: string) => `Local agent settings: ${status}${model ? `, ${model}` : ''}`, + agentSettingsTitle: (status: string, model?: string) => `${status}${model ? ` · ${model}` : ''}. Configure the browser-local agent.`, + openChat: 'Open chat', + hideChat: 'Hide chat', daemon: 'Daemon', daemonNavigation: (state: string) => `Open System: daemon ${state}`, chatWidth: 'Width of local chat',