Skip to content

Commit d0efb20

Browse files
feat(desktop): visualize agent graph topology
Generated-by: OpenAI Codex Generated-by: Claude Code
1 parent ce10a0e commit d0efb20

17 files changed

Lines changed: 2188 additions & 74 deletions
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import type { Locator } from '@playwright/test';
21+
import { expect, test } from './fixtures';
22+
23+
async function expectInsideViewport(target: Locator, viewport: Locator): Promise<void> {
24+
await expect
25+
.poll(async () => {
26+
const targetBox = await target.evaluate((element) => {
27+
const rect = element.getBoundingClientRect();
28+
return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom };
29+
});
30+
const viewportBox = await viewport.evaluate((element) => {
31+
const rect = element.getBoundingClientRect();
32+
return {
33+
left: rect.left + element.clientLeft,
34+
top: rect.top + element.clientTop,
35+
right: rect.left + element.clientLeft + element.clientWidth,
36+
bottom: rect.top + element.clientTop + element.clientHeight,
37+
};
38+
});
39+
const tolerance = 1;
40+
return Boolean(
41+
targetBox.left >= viewportBox.left - tolerance &&
42+
targetBox.top >= viewportBox.top - tolerance &&
43+
targetBox.right <= viewportBox.right + tolerance &&
44+
targetBox.bottom <= viewportBox.bottom + tolerance,
45+
);
46+
})
47+
.toBe(true);
48+
}
49+
50+
function metadataValue(scope: Locator, label: string): Locator {
51+
return scope
52+
.locator('dt')
53+
.filter({ hasText: label })
54+
.locator('xpath=following-sibling::dd[1]');
55+
}
56+
57+
async function expectMetadata(
58+
scope: Locator,
59+
label: string,
60+
value: string | RegExp,
61+
): Promise<void> {
62+
await expect(metadataValue(scope, label)).toHaveText(value);
63+
}
64+
65+
test('inspects and follows an operator across graph views', async ({
66+
agentGraphTopologyWindow: page,
67+
}) => {
68+
const panel = page.getByRole('region', { name: 'Agent Graph', exact: true });
69+
const topology = page.getByTestId('agent-graph-topology');
70+
71+
await expect
72+
.poll(() =>
73+
topology.evaluate((element) => ({
74+
horizontal: element.scrollWidth > element.clientWidth,
75+
vertical: element.scrollHeight > element.clientHeight,
76+
})),
77+
)
78+
.toEqual({ horizontal: true, vertical: true });
79+
80+
const initialPublisherNode = topology.getByRole('button', { name: /^publisher\./u });
81+
await initialPublisherNode.click();
82+
await expect(initialPublisherNode).toBeFocused();
83+
await expectInsideViewport(initialPublisherNode, topology);
84+
await expectInsideViewport(initialPublisherNode, panel);
85+
await expect(page.getByRole('region', { name: 'Operator details: publisher' })).toBeAttached();
86+
await initialPublisherNode.click();
87+
88+
await panel.getByRole('radio', { name: 'List' }).click();
89+
const publisherRow = panel
90+
.getByTestId('agent-graph-list')
91+
.locator(':scope > li')
92+
.filter({ has: page.getByText('publisher', { exact: true }) });
93+
await expect(publisherRow.getByText('Completed', { exact: true })).toBeVisible();
94+
await expect(publisherRow).toContainText('1 more work item omitted');
95+
96+
const detailsButton = publisherRow.getByRole('button', {
97+
name: 'View publisher details',
98+
});
99+
await detailsButton.click();
100+
await expect(detailsButton).toBeFocused();
101+
await expect(detailsButton).toHaveAttribute('aria-expanded', 'true');
102+
const details = page.getByRole('region', { name: 'Operator details: publisher' });
103+
await expect(details).toHaveAttribute('aria-busy', 'false');
104+
const collection = (name: string) =>
105+
details
106+
.locator('.maka-agent-graph-details-collection')
107+
.filter({ has: page.getByText(name, { exact: true }) });
108+
const publisherSessionId = /^\["[a-f0-9]{64}","child-publisher"\]$/u;
109+
const activations = collection('Activations');
110+
const activation = activations.locator('li');
111+
await expect(
112+
metadataValue(activation, 'firstEventTime').locator(
113+
'time[datetime="2026-05-22T02:59:57.000Z"]',
114+
),
115+
).toBeVisible();
116+
await expect(
117+
metadataValue(activation, 'lastEventTime').locator(
118+
'time[datetime="2026-05-22T02:59:59.000Z"]',
119+
),
120+
).toBeVisible();
121+
await expectMetadata(activation, 'lastRecordId', 'record-publisher-terminal');
122+
await expectMetadata(activation, 'terminalRecordId', 'record-publisher-terminal');
123+
await expectMetadata(activation, 'run.sessionId', publisherSessionId);
124+
await expectMetadata(activation, 'run.agentRunId', 'run-publisher');
125+
await expectMetadata(activation, 'run.turnId', 'turn-publisher');
126+
127+
const claims = collection('Claims');
128+
const claim = claims.locator('li').filter({ hasText: 'claim-publisher' });
129+
await expectMetadata(claim, 'intentId', 'intent-publisher');
130+
await expectMetadata(claim, 'childSessionId', publisherSessionId);
131+
await expect(
132+
metadataValue(claim, 'claimedAt').locator('time[datetime="2026-05-22T02:59:56.000Z"]'),
133+
).toBeVisible();
134+
await expectMetadata(claim, 'run.sessionId', publisherSessionId);
135+
await expectMetadata(claim, 'run.agentRunId', 'run-publisher');
136+
await expectMetadata(claim, 'run.turnId', 'turn-publisher');
137+
138+
const activity = collection('Recent activity');
139+
const permissionRecord = activity.locator('li').filter({ hasText: 'record-publisher-permission' });
140+
await expectMetadata(permissionRecord, 'activationId', 'activation-publisher');
141+
await expectMetadata(permissionRecord, 'signals', 'attention: permission request');
142+
await expect(
143+
metadataValue(permissionRecord, 'eventTime').locator(
144+
'time[datetime="2026-05-22T02:59:57.000Z"]',
145+
),
146+
).toBeVisible();
147+
await expectMetadata(permissionRecord, 'run.sessionId', publisherSessionId);
148+
await expectMetadata(permissionRecord, 'run.agentRunId', 'run-publisher');
149+
await expectMetadata(permissionRecord, 'run.turnId', 'turn-publisher');
150+
const terminalRecord = activity.locator('li').filter({ hasText: 'record-publisher-terminal' });
151+
await expectMetadata(terminalRecord, 'activationId', 'activation-publisher');
152+
await expectMetadata(terminalRecord, 'signals', 'terminal: completed');
153+
await expect(
154+
metadataValue(terminalRecord, 'eventTime').locator(
155+
'time[datetime="2026-05-22T02:59:59.000Z"]',
156+
),
157+
).toBeVisible();
158+
await expectMetadata(terminalRecord, 'run.sessionId', publisherSessionId);
159+
await expectMetadata(terminalRecord, 'run.agentRunId', 'run-publisher');
160+
await expectMetadata(terminalRecord, 'run.turnId', 'turn-publisher');
161+
await expectInsideViewport(detailsButton, panel);
162+
await expectInsideViewport(details.locator('.maka-agent-graph-details-heading'), panel);
163+
164+
await panel.getByRole('radio', { name: 'Topology' }).click();
165+
const selectedNode = topology.locator('.maka-agent-graph-node[data-selected="true"]');
166+
await expect(selectedNode).toContainText('publisher');
167+
await expect(selectedNode).toContainText('1 more work item omitted');
168+
await expectInsideViewport(selectedNode, topology);
169+
await expectInsideViewport(selectedNode, panel);
170+
await expect.poll(() => topology.evaluate((element) => element.scrollLeft)).toBeGreaterThan(0);
171+
172+
await panel.getByRole('button', { name: 'Collapse Agent Graph' }).click();
173+
await panel.getByRole('button', { name: 'Expand Agent Graph' }).click();
174+
await expectInsideViewport(selectedNode, topology);
175+
await expectInsideViewport(selectedNode, panel);
176+
177+
await panel.getByRole('radio', { name: 'List' }).click();
178+
await expectInsideViewport(detailsButton, panel);
179+
await expectInsideViewport(details.locator('.maka-agent-graph-details-heading'), panel);
180+
});

apps/desktop/e2e/fixtures.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@ type E2eTestFixtures = {
570570
projectSidebarWindow: Page;
571571
parentRemovalWindow: Page;
572572
railRenderWindow: Page;
573+
agentGraphTopologyWindow: Page;
573574
promptRailWindow: Page;
574575
partialHistoryWindow: Page;
575576
promptRailMotionWindow: Page;
@@ -719,6 +720,15 @@ export const test = base.extend<E2eTestFixtures, E2eWorkerFixtures>({
719720
use,
720721
);
721722
},
723+
agentGraphTopologyWindow: async ({}, use) => {
724+
await withE2eWindow({
725+
seed: false,
726+
readinessSelector: '[data-testid="agent-graph-topology"]',
727+
e2eFixtureScenario: 'agent-graph-topology',
728+
locale: 'en',
729+
showWindow: true,
730+
}, use);
731+
},
722732
// Keep this scenario's real Electron + Host composition warm for the worker,
723733
// while the test-scoped wrapper below restores Host and renderer state
724734
// between tests. Tests on it may run a Turn, so the reset is not read-only.

0 commit comments

Comments
 (0)