Skip to content

Commit ff28841

Browse files
authored
Merge pull request #2753 from DeusData/codex/fix-ui-reader-focus-loading
fix(ui): hide stale focus warning while arranging graph
2 parents 72c3604 + 1a0afcd commit ff28841

2 files changed

Lines changed: 34 additions & 7 deletions

File tree

‎graph-ui/src/galaxy/GalaxyPanel.reader-focus.test.tsx‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createRoot, type Root } from 'react-dom/client';
44
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
55
import GalaxyPanel, { type GalaxyPanelProps } from './GalaxyPanel';
66
import type { GraphData, GraphNode } from './types';
7+
import type { OrganicLayoutRequest, OrganicLayoutResponse } from './organic-layout.worker';
78

89
/** The layout can be capped; focused relationships come independently from RPC. */
910
function graphFetch(nodes: GraphNode[], edges: GraphData['edges'], total = nodes.length) {
@@ -102,10 +103,24 @@ it('opens a file hierarchy without a caret symbol and follows literal marked ran
102103
expect(fetchLayout.mock.calls.filter(([url]) => String(url).includes('/api/layout'))).toHaveLength(1);
103104
});
104105

105-
it('does not report a file missing from a capped layout while its indexed scope is loading', async () => {
106+
it.each([true, false])('does not report a file missing while its scope or layout is loading (indexed: %s)', async indexed => {
107+
let worker!: LayoutWorker;
108+
class LayoutWorker {
109+
onmessage: ((event: MessageEvent<OrganicLayoutResponse>) => void) | null = null;
110+
requests: OrganicLayoutRequest[] = [];
111+
constructor() { worker = this; }
112+
postMessage(request: OrganicLayoutRequest) { this.requests.push(request); }
113+
terminate() {}
114+
reply() {
115+
const { sequence, data } = this.requests.at(-1)!;
116+
this.onmessage?.(new MessageEvent<OrganicLayoutResponse>('message', { data: { sequence,
117+
result: { data, groups: [], groupByNode: new Map() } } }));
118+
}
119+
}
120+
vi.stubGlobal('Worker', LayoutWorker);
106121
const node: GraphNode = { id: 15, name: 'outside', qualified_name: 'sample.outside', label: 'Function',
107122
file_path: 'src/a.ts', start_line: 1, end_line: 10, x: 0, y: 0, z: 0, size: 1, color: '#999999' };
108-
const source = graphFetch([node], []);
123+
const source = graphFetch(indexed ? [node] : [], []);
109124
let release!: () => void;
110125
const pending = new Promise<void>(resolve => { release = resolve; });
111126
const fetchImpl = vi.fn(async (url: RequestInfo | URL, init?: RequestInit) => {
@@ -114,15 +129,27 @@ it('does not report a file missing from a capped layout while its indexed scope
114129
return source(url, init);
115130
});
116131
await act(async () => root.render(<GalaxyPanel project="sample" visible focusFilePath="src/a.ts" onOpenNode={vi.fn()} fetch={fetchImpl} />));
117-
// Hand test K8: the status names the layer that is loading instead of an endless "Loading relationships".
118-
// It may first say "Checking index…" while the index generation is read; wait for the load itself.
119-
await vi.waitFor(() => expect(host.textContent).toContain('Loading layer 1…'));
132+
expect(host.textContent).toContain('Loading layer 1…');
133+
// Finish the empty capped-layout preview while the indexed scope is held.
134+
// This leaves a missing-file note in state, hidden until the scope finishes.
135+
expect(worker.requests).toHaveLength(1);
136+
expect(worker.requests[0]!.data.nodes).toEqual([]);
137+
await act(async () => worker.reply());
120138
expect(host.querySelector('[data-testid="atlas-galaxy-note"]')).toBeNull();
121139
await act(async () => { release(); });
122140
expect(host.textContent).toContain('All indexed direct dependencies included.');
141+
// The scope is complete, but its new picture has not been arranged yet.
142+
expect(worker.requests).toHaveLength(2);
143+
expect(host.querySelector('[role="status"][data-state="arranging"]')).not.toBeNull();
144+
expect(host.querySelector('[data-testid="atlas-galaxy-note"]')).toBeNull();
123145
expect(host.textContent).not.toContain('not in the loaded graph');
146+
await act(async () => worker.reply());
147+
expect(host.querySelector('[role="status"][data-state="complete"]')).not.toBeNull();
148+
expect(JSON.parse(host.querySelector('[data-testid="graph-selection"]')!.textContent!)).toEqual(indexed ? [15] : []);
149+
if (indexed) expect(host.querySelector('[data-testid="atlas-galaxy-note"]')).toBeNull();
150+
else expect(host.querySelector('[data-testid="atlas-galaxy-note"]')?.textContent).toBe('src/a.ts is not in the loaded graph layout.');
124151
});
125-
afterEach(async () => { await act(async () => root.unmount()); host.remove(); globalThis.__atlasGalaxy = undefined; });
152+
afterEach(async () => { await act(async () => root.unmount()); host.remove(); globalThis.__atlasGalaxy = undefined; vi.unstubAllGlobals(); });
126153

127154
it('selects the whole file, narrows only to marked code, and restores the file when the mark clears', async () => {
128155
const common = { file_path: 'src/a.ts', x: 0, y: 0, z: 0, size: 1, color: '#999999' };

‎graph-ui/src/galaxy/GalaxyPanel.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3055,7 +3055,7 @@ export default function GalaxyPanel(props: GalaxyPanelProps): JSX.Element {
30553055
<summary>Selection details</summary>
30563056
<div className="atlas-galaxy-selection-details-body">{selectionContent}</div>
30573057
</details>}
3058-
{note.length > 0 && !scope.loading && (
3058+
{note.length > 0 && !scope.loading && !organicTask.loading && (
30593059
<p className="atlas-galaxy-note" data-testid="atlas-galaxy-note">
30603060
{props.workspaceExpanded && note === GALAXY_NO_FOCUS_NOTE
30613061
? 'Select a node or entry point to attach graph context to chat.' : note}

0 commit comments

Comments
 (0)