Skip to content

Commit 285fd47

Browse files
fix(desktop): isolate agent graph feature
Generated-by: OpenAI Codex Generated-by: Claude Code
1 parent d0efb20 commit 285fd47

25 files changed

Lines changed: 443 additions & 145 deletions

apps/desktop/renderer-architecture.json

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
{
22
"version": 1,
33
"legacyRendererFiles": [
4-
"src/renderer/agent-graph-panel-visibility.ts",
5-
"src/renderer/agent-graph-panel.tsx",
6-
"src/renderer/agent-graph-refresh.ts",
74
"src/renderer/app-shell-app-update.ts",
85
"src/renderer/app-shell-chat-actions.ts",
96
"src/renderer/app-shell-chrome-actions.tsx",
@@ -880,7 +877,6 @@
880877
"dependencyPaths": {
881878
"../preload/bridge-contract.js": 1,
882879
"../preload/transcript-contract.js": 1,
883-
"./agent-graph-panel": 1,
884880
"./app-shell-app-update": 1,
885881
"./app-shell-chat-actions": 1,
886882
"./app-shell-chrome-actions": 1,
@@ -908,6 +904,7 @@
908904
"./desktop-execution-boundary-surface": 1,
909905
"./desktop-slash-command": 1,
910906
"./error-boundary": 1,
907+
"./features/agent-graph": 1,
911908
"./features/conversation": 1,
912909
"./features/goals": 1,
913910
"./features/module-hub": 1,
@@ -1133,57 +1130,6 @@
11331130
"actionFactories": [],
11341131
"dependencyPaths": {}
11351132
},
1136-
"src/renderer/agent-graph-panel-visibility.ts": {
1137-
"bridgePaths": {},
1138-
"environmentCapabilities": {},
1139-
"hookCalls": {},
1140-
"lifecycleMethods": {},
1141-
"unresolvedDependencies": 0,
1142-
"actionFactories": [],
1143-
"dependencyPaths": {}
1144-
},
1145-
"src/renderer/agent-graph-panel.tsx": {
1146-
"bridgePaths": {
1147-
"window.maka.graphs.getSnapshot": 1,
1148-
"window.maka.graphs.listCurrentEpochs": 1,
1149-
"window.maka.graphs.listEpochs": 2,
1150-
"window.maka.graphs.stop": 1,
1151-
"window.maka.graphs.subscribe": 1
1152-
},
1153-
"environmentCapabilities": {},
1154-
"hookCalls": {
1155-
"useEffect": 2,
1156-
"useRef": 4,
1157-
"useState": 9
1158-
},
1159-
"lifecycleMethods": {},
1160-
"unresolvedDependencies": 0,
1161-
"actionFactories": [],
1162-
"dependencyPaths": {
1163-
"./agent-graph-panel-visibility.js": 1,
1164-
"./agent-graph-refresh.js": 1,
1165-
"@astryxdesign/core/Banner": 1,
1166-
"@astryxdesign/core/Button": 1,
1167-
"@astryxdesign/core/EmptyState": 1,
1168-
"@astryxdesign/core/Spinner": 1,
1169-
"@maka/core/ui-locale": 1,
1170-
"@maka/runtime-host/client": 1,
1171-
"@maka/runtime-host/protocol": 1,
1172-
"@maka/runtime/stream-graph-read-model": 1,
1173-
"@maka/ui": 1,
1174-
"@maka/ui/icons": 1,
1175-
"react": 1
1176-
}
1177-
},
1178-
"src/renderer/agent-graph-refresh.ts": {
1179-
"bridgePaths": {},
1180-
"environmentCapabilities": {},
1181-
"hookCalls": {},
1182-
"lifecycleMethods": {},
1183-
"unresolvedDependencies": 0,
1184-
"actionFactories": [],
1185-
"dependencyPaths": {}
1186-
},
11871133
"src/renderer/app-update-install.ts": {
11881134
"bridgePaths": {},
11891135
"environmentCapabilities": {},

apps/desktop/src/main/__tests__/agent-graph-panel-visibility.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
isAgentGraphPanelDismissible,
2525
reconcileAgentGraphPanelDismissals,
2626
shouldShowAgentGraphPanel,
27-
} from '../../renderer/agent-graph-panel-visibility.js';
27+
} from '../../renderer/features/agent-graph/testing.js';
2828

2929
describe('isAgentGraphPanelDismissible', () => {
3030
it('allows hiding a graph that no longer has active work', () => {

apps/desktop/src/main/__tests__/agent-graph-panel.test.ts

Lines changed: 83 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import { act, createElement } from 'react';
2424
import { createRoot, type Root } from 'react-dom/client';
2525
import type { AgentGraphClientSnapshot } from '@maka/runtime/stream-graph-read-model';
2626
import type { AgentGraphEpochSummary } from '@maka/runtime-host/protocol';
27-
import { AgentGraphPanel } from '../../renderer/agent-graph-panel.js';
27+
import {
28+
AgentGraphPanel,
29+
AgentGraphServicesProvider,
30+
type AgentGraphServices,
31+
} from '../../renderer/features/agent-graph/testing.js';
2832

2933
type GraphListener = () => void;
3034

@@ -112,6 +116,7 @@ function installGraphRenderer(
112116
renderSession(sessionId: string): Promise<void>;
113117
holdNextEpochList(sessionId: string): DeferredRead;
114118
holdNextSnapshot(graphId: string): DeferredRead;
119+
holdNextInspection(operatorId: string): DeferredRead;
115120
holdNextStop(sessionId: string): DeferredRead;
116121
setCurrentWithoutNotification(next: AgentGraphClientSnapshot): void;
117122
notify(): void;
@@ -161,6 +166,7 @@ function installGraphRenderer(
161166
const listeners = new Set<GraphListener>();
162167
const epochListGates = new Map<string, DeferredReadGate>();
163168
const snapshotGates = new Map<string, DeferredReadGate>();
169+
const inspectionGates = new Map<string, DeferredReadGate>();
164170
const stopGates = new Map<string, DeferredReadGate>();
165171
const stopCalls: Array<{ sessionId: string; expectedGraphId: string }> = [];
166172
let fullEpochReads = 0;
@@ -184,7 +190,7 @@ function installGraphRenderer(
184190
truncated: false,
185191
};
186192
};
187-
(window as unknown as { maka: unknown }).maka = {
193+
const services: AgentGraphServices = {
188194
graphs: {
189195
listEpochs: async (sessionId: string) => {
190196
fullEpochReads += 1;
@@ -216,6 +222,12 @@ function installGraphRenderer(
216222
},
217223
inspectOperator: async (sessionId: string, operatorId: string, graphId?: string) => {
218224
inspectionReads.set(operatorId, (inspectionReads.get(operatorId) ?? 0) + 1);
225+
const gate = inspectionGates.get(operatorId);
226+
if (gate) {
227+
inspectionGates.delete(operatorId);
228+
gate.markStarted();
229+
await gate.waitForRelease;
230+
}
219231
const snapshot = graphId ? snapshots.get(graphId) : undefined;
220232
const operator = snapshot?.operators.find((candidate) => candidate.operatorId === operatorId);
221233
if (!snapshot || snapshot.rootSessionId !== sessionId || !operator) {
@@ -275,6 +287,7 @@ function installGraphRenderer(
275287
},
276288
},
277289
};
290+
(window as unknown as { maka: unknown }).maka = services;
278291

279292
const container = document.querySelector('#root');
280293
assert.ok(container);
@@ -312,12 +325,16 @@ function installGraphRenderer(
312325
async renderSession(sessionId) {
313326
await act(async () => {
314327
root.render(
315-
createElement(AgentGraphPanel, {
316-
rootSessionId: sessionId,
317-
enabled: true,
318-
locale: 'en',
319-
onOpenSession: () => undefined,
320-
}),
328+
createElement(
329+
AgentGraphServicesProvider,
330+
{ services },
331+
createElement(AgentGraphPanel, {
332+
rootSessionId: sessionId,
333+
enabled: true,
334+
locale: 'en',
335+
onOpenSession: () => undefined,
336+
}),
337+
),
321338
);
322339
await Promise.resolve();
323340
});
@@ -332,6 +349,11 @@ function installGraphRenderer(
332349
snapshotGates.set(graphId, gate);
333350
return gate;
334351
},
352+
holdNextInspection(operatorId) {
353+
const gate = deferredReadGate();
354+
inspectionGates.set(operatorId, gate);
355+
return gate;
356+
},
335357
holdNextStop(sessionId) {
336358
const gate = deferredReadGate();
337359
stopGates.set(sessionId, gate);
@@ -347,12 +369,16 @@ async function renderPanel(
347369
const harness = installGraphRenderer(initial);
348370
await act(async () => {
349371
harness.root.render(
350-
createElement(AgentGraphPanel, {
351-
rootSessionId: 'session-1',
352-
enabled: true,
353-
locale: 'en',
354-
onOpenSession: () => undefined,
355-
}),
372+
createElement(
373+
AgentGraphServicesProvider,
374+
{ services: (window as unknown as { maka: AgentGraphServices }).maka },
375+
createElement(AgentGraphPanel, {
376+
rootSessionId: 'session-1',
377+
enabled: true,
378+
locale: 'en',
379+
onOpenSession: () => undefined,
380+
}),
381+
),
356382
);
357383
await Promise.resolve();
358384
});
@@ -536,6 +562,47 @@ describe('AgentGraphPanel dismiss', () => {
536562
await act(async () => harness.root.unmount());
537563
});
538564

565+
it('keeps inspected details visible while a live refresh is pending', async () => {
566+
const graph = snapshot({
567+
graphId: 'graph-inspection-refresh',
568+
status: 'active',
569+
operators: [graphOperator('a', ['work-a'])],
570+
work: [graphWork('work-a', 'a', 'Initial inspection detail')],
571+
});
572+
const harness = await renderPanel(graph);
573+
const node = harness.container.querySelector('.maka-agent-graph-node');
574+
assert.ok(node);
575+
await act(async () => {
576+
(node as HTMLElement).click();
577+
await Promise.resolve();
578+
});
579+
const details = harness.container.querySelector(
580+
'[aria-label="Operator details: agent-a"]',
581+
);
582+
assert.ok(details);
583+
assert.match(details.textContent ?? '', /Initial inspection detail/);
584+
585+
const inspection = harness.holdNextInspection('a');
586+
await harness.setSnapshot({
587+
...graph,
588+
snapshotVersion: '2',
589+
work: [graphWork('work-a', 'a', 'Refreshed inspection detail')],
590+
});
591+
await inspection.started;
592+
593+
assert.equal(details.getAttribute('aria-busy'), 'true');
594+
assert.match(details.textContent ?? '', /Initial inspection detail/);
595+
assert.doesNotMatch(details.textContent ?? '', /Refreshed inspection detail/);
596+
597+
await act(async () => {
598+
inspection.release();
599+
await Promise.resolve();
600+
});
601+
assert.equal(details.getAttribute('aria-busy'), 'false');
602+
assert.match(details.textContent ?? '', /Refreshed inspection detail/);
603+
await act(async () => harness.root.unmount());
604+
});
605+
539606
it('keeps keyboard order visual and reveals a retained selection', async () => {
540607
const harness = await renderPanel(
541608
snapshot({
@@ -670,17 +737,7 @@ describe('AgentGraphPanel dismiss', () => {
670737
const current = snapshot({ graphId: 'graph-2', status: 'active' });
671738
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
672739
const harness = installGraphRenderer(current, [previous]);
673-
await act(async () => {
674-
harness.root.render(
675-
createElement(AgentGraphPanel, {
676-
rootSessionId: 'session-1',
677-
enabled: true,
678-
locale: 'en',
679-
onOpenSession: () => undefined,
680-
}),
681-
);
682-
await Promise.resolve();
683-
});
740+
await harness.renderSession('session-1');
684741

685742
const selector = harness.container.querySelector('[role="combobox"]');
686743
assert.ok(selector);
@@ -869,17 +926,7 @@ describe('AgentGraphPanel dismiss', () => {
869926
const current = snapshot({ graphId: 'graph-2', status: 'active' });
870927
const previous = snapshot({ graphId: 'graph-1', status: 'completed' });
871928
const harness = installGraphRenderer(current, [previous]);
872-
await act(async () => {
873-
harness.root.render(
874-
createElement(AgentGraphPanel, {
875-
rootSessionId: 'session-1',
876-
enabled: true,
877-
locale: 'en',
878-
onOpenSession: () => undefined,
879-
}),
880-
);
881-
await Promise.resolve();
882-
});
929+
await harness.renderSession('session-1');
883930

884931
const selector = harness.container.querySelector('[role="combobox"]');
885932
assert.ok(selector);

apps/desktop/src/main/__tests__/agent-graph-refresh.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import { deferred } from '@maka/core/test-only/async-primitives';
2121
import { strict as assert } from 'node:assert';
2222
import { describe, it } from 'node:test';
23-
import { createAgentGraphRefreshScheduler } from '../../renderer/agent-graph-refresh.js';
23+
import { createAgentGraphRefreshScheduler } from '../../renderer/features/agent-graph/testing.js';
2424
async function tick(): Promise<void> {
2525
await new Promise((resolve) => setImmediate(resolve));
2626
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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 { strict as assert } from 'node:assert';
21+
import { describe, it } from 'node:test';
22+
import type { MakaBridge } from '../../preload/bridge-contract.js';
23+
import { createDesktopAgentGraphServices } from '../../renderer/platform/desktop/create-agent-graph-services.js';
24+
25+
describe('createDesktopAgentGraphServices', () => {
26+
it('maps the narrow Graph contract to the Desktop bridge', async () => {
27+
const calls: Array<{ name: string; args: unknown[] }> = [];
28+
let changeHandler: (() => void) | undefined;
29+
let changes = 0;
30+
let disposed = 0;
31+
const graphs = new Proxy(
32+
{},
33+
{
34+
get: (_target, property) =>
35+
(...args: unknown[]) => {
36+
calls.push({ name: String(property), args });
37+
if (property === 'subscribe') {
38+
changeHandler = args[1] as () => void;
39+
return () => {
40+
disposed += 1;
41+
};
42+
}
43+
return Promise.resolve(property);
44+
},
45+
},
46+
);
47+
const services = createDesktopAgentGraphServices({ graphs } as unknown as Pick<
48+
MakaBridge,
49+
'graphs'
50+
>);
51+
52+
await services.graphs.listEpochs('session-1');
53+
await services.graphs.listCurrentEpochs('session-1');
54+
await services.graphs.getSnapshot('session-1', { graphId: 'graph-1' });
55+
await services.graphs.inspectOperator('session-1', 'operator-1', 'graph-1');
56+
await services.graphs.stop('session-1', 'graph-1');
57+
const unsubscribe = services.graphs.subscribe('session-1', () => {
58+
changes += 1;
59+
});
60+
changeHandler?.();
61+
unsubscribe();
62+
63+
assert.deepEqual(calls, [
64+
{ name: 'listEpochs', args: ['session-1'] },
65+
{ name: 'listCurrentEpochs', args: ['session-1'] },
66+
{ name: 'getSnapshot', args: ['session-1', { graphId: 'graph-1' }] },
67+
{ name: 'inspectOperator', args: ['session-1', 'operator-1', 'graph-1'] },
68+
{ name: 'stop', args: ['session-1', 'graph-1'] },
69+
{ name: 'subscribe', args: ['session-1', changeHandler] },
70+
]);
71+
assert.equal(changes, 1);
72+
assert.equal(disposed, 1);
73+
});
74+
});

apps/desktop/src/main/__tests__/agent-graph-topology.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import {
3030
layoutAgentGraph,
3131
revealAgentGraphNode,
3232
scheduledWorkPresentation,
33-
} from '../../renderer/agent-graph-topology.js';
33+
} from '../../renderer/features/agent-graph/testing.js';
3434

3535
describe('layoutAgentGraph', () => {
3636
it('preserves graph operator status semantics', () => {

apps/desktop/src/renderer/app-shell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ import {
8585
import { deriveWorkspaceReadinessRecovery } from './workspace-readiness-recovery';
8686
import { LiveTurnReconciler } from './live-turn-reconciler';
8787
import { useAppShellSessionUiReads } from './use-app-shell-session-ui-reads';
88-
import { AgentGraphPanel } from './agent-graph-panel';
88+
import { AgentGraphPanel } from './features/agent-graph';
8989
import { ChatComposerRegion } from './chat-composer-region';
9090
import {
9191
WorkbarHost,

0 commit comments

Comments
 (0)