mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-08 08:53:43 +08:00
feat(ui): show the local agent as a robot with a status lamp
The header read "Agent active · Qwen2.5 Coder 0.5B" next to the chat toggle. It is now a robot icon drawn on the same grid as the chat toggle, with the status lamp at its corner; a click still opens the agent configuration. The state and the model name move into the tooltip and the accessible name, and the model is chosen in the configuration. The header's agent and chat labels now come from the workspace strings, which removes the seven hardcoded chrome strings the style gate found in AtlasChrome.tsx. Signed-off-by: Bernhard Jackiewicz <bj@techport.io>
This commit is contained in:
@@ -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<HTMLButtonElement>('[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<HTMLButtonElement>('.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: <button>Project</button> }));
|
||||
const button = container.querySelector<HTMLButtonElement>('[aria-label="Local agent settings"]')!;
|
||||
expect(button.textContent).toBe(label);
|
||||
const button = container.querySelector<HTMLButtonElement>('.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<HTMLInputElement>('[aria-label="Chat draft"]')!;
|
||||
draft.value = 'retained draft';
|
||||
const settingsButton = group?.querySelector<HTMLButtonElement>('[aria-label="Local agent settings"]')!;
|
||||
const settingsButton = group?.querySelector<HTMLButtonElement>('.atlas-browser-ai-action')!;
|
||||
const openButton = group?.querySelector<HTMLButtonElement>('[aria-label="Open chat"]')!;
|
||||
expect(openButton.getAttribute('aria-expanded')).toBe('false');
|
||||
await act(async () => settingsButton.click());
|
||||
|
||||
@@ -677,13 +677,15 @@ export default function AtlasChrome(props: AtlasChromeProps): JSX.Element {
|
||||
))}
|
||||
</div>
|
||||
{props.projectSwitcher}
|
||||
{(props.onOpenBrowserAi !== undefined || props.onToggleBrowserAi !== undefined) && <div className="atlas-agent-controls" role="group" aria-label="Local agent">
|
||||
{props.onOpenBrowserAi !== undefined && <button type="button" className="atlas-browser-ai-action" data-state={props.agentState ?? 'off'} aria-label="Local agent settings" title="Configure the browser-local agent" onClick={props.onOpenBrowserAi}>
|
||||
{(props.onOpenBrowserAi !== undefined || props.onToggleBrowserAi !== undefined) && <div className="atlas-agent-controls" role="group" aria-label={workspaceStrings.agentControls}>
|
||||
{props.onOpenBrowserAi !== undefined && <button type="button" className="atlas-browser-ai-action" data-state={props.agentState ?? 'off'}
|
||||
aria-label={workspaceStrings.agentSettings(workspaceStrings.agentStatus[props.agentState ?? 'off'], props.agentModelName || undefined)}
|
||||
title={workspaceStrings.agentSettingsTitle(workspaceStrings.agentStatus[props.agentState ?? 'off'], props.agentModelName || undefined)} onClick={props.onOpenBrowserAi}>
|
||||
{/* Derselbe Strich und dasselbe Raster wie der Chat-Schalter daneben; den Zustand traegt die Lampe. */}
|
||||
<svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M10 3v3" /><circle cx="10" cy="2.5" r="1" fill="currentColor" stroke="none" /><rect x="3.5" y="6" width="13" height="10" rx="2.5" /><path d="M1.5 10v3M18.5 10v3" /><circle cx="7.5" cy="10.5" r="1.2" fill="currentColor" stroke="none" /><circle cx="12.5" cy="10.5" r="1.2" fill="currentColor" stroke="none" /><path d="M8 13.5h4" /></svg>
|
||||
<span aria-hidden="true" />
|
||||
{{ off: 'Enable agent', loading: 'Agent loading', active: 'Agent active', busy: 'Agent working', error: 'Agent error' }[props.agentState ?? 'off']}
|
||||
{props.agentModelName && <small className="atlas-agent-model-name" title={props.agentModelName}>{props.agentModelName}</small>}
|
||||
</button>}
|
||||
{props.onToggleBrowserAi !== undefined && <button type="button" className="atlas-agent-chat-toggle" aria-expanded={props.chatOpen === true} aria-label={props.chatOpen ? 'Hide chat' : 'Open chat'} title={props.chatOpen ? 'Hide chat' : 'Open chat'} onClick={props.onToggleBrowserAi}>
|
||||
{props.onToggleBrowserAi !== undefined && <button type="button" className="atlas-agent-chat-toggle" aria-expanded={props.chatOpen === true} aria-label={props.chatOpen ? workspaceStrings.hideChat : workspaceStrings.openChat} title={props.chatOpen ? workspaceStrings.hideChat : workspaceStrings.openChat} onClick={props.onToggleBrowserAi}>
|
||||
<svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="2" y="3" width="16" height="14" rx="2" /><path d="M12 3v14" /><path className="atlas-agent-chat-toggle-pane" d="M12 3h4a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-4z" stroke="none" /></svg>
|
||||
</button>}
|
||||
</div>}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user