Skip to content
3 changes: 2 additions & 1 deletion openplanter-desktop/frontend/src/api/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export async function solve(objective: string, sessionId: string): Promise<void>
return invoke("solve", { objective, sessionId });
}

export async function getSessionHistory(sessionId: string): Promise<ReplayEntry[]> {
export async function getSessionHistory(sessionId: string | null): Promise<ReplayEntry[]> {
if (!sessionId) return [];
return invoke("get_session_history", { sessionId });
}

Expand Down
6 changes: 6 additions & 0 deletions openplanter-desktop/frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ export interface OverviewActionView {
export interface OverviewRevelationProvenanceView {
source: string;
step_index?: number;
turn_id?: string;
event_id?: string;
replay_seq?: number;
replay_line?: number;
source_refs?: string[];
evidence_refs?: string[];
}

export interface OverviewRevelationView {
Expand Down
11 changes: 7 additions & 4 deletions openplanter-desktop/frontend/src/components/GraphPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,17 @@ export function createGraphPane(): HTMLElement {

// --- Drawer open/close ---
function openWikiDrawer(detail: OpenWikiDrawerDetail): void {
const wikiPath = detail.wikiPath.trim();
if (!wikiPath) return;

hideDetail();
currentDrawerWikiPath = detail.wikiPath;
currentDrawerWikiPath = wikiPath;

const sourceNode = findSourceNodeByPath(detail.wikiPath);
const sourceNode = findSourceNodeByPath(wikiPath);
drawerTitle.textContent =
detail.requestedTitle ||
sourceNode?.label ||
fallbackTitleFromPath(detail.wikiPath);
fallbackTitleFromPath(wikiPath);
drawerBody.innerHTML = '<span style="color:var(--text-muted)">Loading...</span>';
drawerBackdrop.classList.add("visible");
drawer.classList.add("visible");
Expand All @@ -309,7 +312,7 @@ export function createGraphPane(): HTMLElement {
}

const loadSeq = ++drawerLoadSeq;
readWikiFile(detail.wikiPath).then((content) => {
readWikiFile(wikiPath).then((content) => {
if (loadSeq !== drawerLoadSeq) return;
drawerBody.innerHTML = md.render(content);
interceptDrawerLinks();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ vi.mock("./GraphPane", () => ({
vi.mock("../graph/sessionBaseline", () => sessionBaselineMocks);

import { appState } from "../state/store";
import { OPEN_WIKI_DRAWER_EVENT } from "../wiki/drawerEvents";
import { createInvestigationPane } from "./InvestigationPane";

describe("createInvestigationPane", () => {
Expand All @@ -38,6 +39,7 @@ describe("createInvestigationPane", () => {
});

afterEach(() => {
vi.useRealTimers();
sessionBaselineMocks.primeGraphSessionBaseline.mockClear();
sessionBaselineMocks.resetGraphSessionState.mockClear();
appState.set(originalState);
Expand Down Expand Up @@ -98,4 +100,54 @@ describe("createInvestigationPane", () => {
expect(sessionBaselineMocks.resetGraphSessionState).not.toHaveBeenCalled();
expect(sessionBaselineMocks.primeGraphSessionBaseline).not.toHaveBeenCalled();
});

it("re-dispatches wiki drawer events after lazy-mounting the graph pane", async () => {
const pane = createInvestigationPane();
document.body.appendChild(pane);
const timerSpy = vi.spyOn(window, "setTimeout");

window.dispatchEvent(new CustomEvent(OPEN_WIKI_DRAWER_EVENT, {
detail: {
wikiPath: "wiki/acme.md",
source: "chat",
},
}));

await new Promise((resolve) => setTimeout(resolve, 0));

expect(appState.get().investigationViewTab).toBe("graph");
expect(pane.querySelector(".graph-pane")).not.toBeNull();
expect(timerSpy).toHaveBeenCalledTimes(1);
timerSpy.mockRestore();
});

it("drops stale queued wiki re-dispatches when a newer event arrives first", () => {
vi.useFakeTimers();

const pane = createInvestigationPane();
document.body.appendChild(pane);
const dispatchSpy = vi.spyOn(window, "dispatchEvent");

window.dispatchEvent(new CustomEvent(OPEN_WIKI_DRAWER_EVENT, {
detail: {
wikiPath: "wiki/first.md",
source: "chat",
},
}));
window.dispatchEvent(new CustomEvent(OPEN_WIKI_DRAWER_EVENT, {
detail: {
wikiPath: "wiki/second.md",
source: "chat",
},
}));

vi.runAllTimers();

const drawerDispatches = dispatchSpy.mock.calls.filter(([event]) => {
return event instanceof CustomEvent && event.type === OPEN_WIKI_DRAWER_EVENT;
});

expect(drawerDispatches).toHaveLength(2);
dispatchSpy.mockRestore();
});
});
31 changes: 31 additions & 0 deletions openplanter-desktop/frontend/src/components/InvestigationPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
primeGraphSessionBaseline,
resetGraphSessionState,
} from "../graph/sessionBaseline";
import { OPEN_WIKI_DRAWER_EVENT, type OpenWikiDrawerDetail } from "../wiki/drawerEvents";
import { createGraphPane } from "./GraphPane";
import { createOverviewPane } from "./OverviewPane";

Expand All @@ -29,6 +30,7 @@ export function createInvestigationPane(): HTMLElement {

let graphPane: HTMLElement | null = null;
let activeTab = "";
let pendingWikiRedispatch: number | null = null;

function ensureGraphPane(): HTMLElement {
if (!graphPane) {
Expand Down Expand Up @@ -87,6 +89,35 @@ export function createInvestigationPane(): HTMLElement {
void primeGraphSessionBaseline();
}) as EventListener);

window.addEventListener(OPEN_WIKI_DRAWER_EVENT, ((e: CustomEvent<OpenWikiDrawerDetail>) => {
const detail = e.detail;
if (!detail) return;
if (pendingWikiRedispatch != null) {
window.clearTimeout(pendingWikiRedispatch);
pendingWikiRedispatch = null;
}
// Capture this before switching tabs. The state update below synchronously mounts the
// graph pane via the store subscription, but listeners added during the current dispatch
// will not observe this event.
const needsRedispatch = !graphPane;

if (appState.get().investigationViewTab !== "graph") {
appState.update((state) => ({
...state,
investigationViewTab: "graph",
}));
}

if (needsRedispatch) {
pendingWikiRedispatch = window.setTimeout(() => {
pendingWikiRedispatch = null;
window.dispatchEvent(
new CustomEvent<OpenWikiDrawerDetail>(OPEN_WIKI_DRAWER_EVENT, { detail }),
);
}, 0);
}
}) as EventListener);

tabs.append(overviewTab, graphTab);
pane.append(tabs, content);

Expand Down
Loading
Loading