Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions apps/desktop/e2e/workhub-layout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,37 @@ test('WorkHub uses its coordination model and shared attachment composer', async
return conversation.left >= 0 && conversation.right <= innerWidth + 1;
})).toBe(true);
}
const shellFloor = await page.locator('.maka-shell-astryx').evaluate((element) =>
Math.round(parseFloat(getComputedStyle(element).minWidth)));
const desktopConversationFloor = await page.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--maka-conversation-min-width').trim());
const workhubConversationFloor = await workhub.evaluate(() =>
getComputedStyle(document.documentElement).getPropertyValue('--maka-conversation-min-width').trim());
expect(workhubConversationFloor).toBe(desktopConversationFloor);
await expect.poll(() => workhub.locator('.workHubLive').evaluate((element) =>
getComputedStyle(element).minWidth)).toBe(desktopConversationFloor);
const dockLeft = await page.locator('.workHubDock').evaluate((element) =>
Math.round(element.getBoundingClientRect().left));
let frozenDockWidth: number | undefined;
for (const width of [shellFloor - 10, shellFloor - 40]) {
const contentWidth = await mainWindow.evaluate((window, nextWidth) => {
window.setBounds({ width: nextWidth });
return window.getContentSize()[0];
}, width);
await expect.poll(() => page.evaluate(() => innerWidth)).toBe(contentWidth);
expect(contentWidth).toBeLessThan(shellFloor);
const dockWidth = await page.locator('.workHubDock').evaluate((element) =>
Math.round(element.getBoundingClientRect().width));
expect(await page.locator('.workHubDock').evaluate((element) =>
Math.round(element.getBoundingClientRect().left))).toBe(dockLeft);
frozenDockWidth ??= dockWidth;
expect(dockWidth).toBe(frozenDockWidth);
await expect.poll(() => workhub.evaluate(() => innerWidth)).toBeLessThan(dockWidth);
await expect.poll(() => workhub.locator('.workHubLive').evaluate((element) =>
Math.round(element.getBoundingClientRect().width))).toBe(dockWidth);
await expect.poll(() => workhub.locator('.workHubLive').evaluate((element) =>
Math.round(element.getBoundingClientRect().left))).toBe(0);
}
const restoredContentWidth = await mainWindow.evaluate((window, bounds) => {
window.setBounds(bounds);
return window.getContentSize()[0];
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@
"react": 1
},
"importSpecifiers": 98,
"nonTriviaTokens": 12729
"nonTriviaTokens": 12713
},
"src/renderer/use-app-shell-composer-quotes.ts": {
"importDeclarations": 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ async function mountRegion(): Promise<{
Object.assign(document, { getSelection });
Object.assign(window, {
getSelection,
getComputedStyle: () =>
({
direction: 'ltr',
writingMode: 'horizontal-tb',
getPropertyValue: () => '',
}) as unknown as CSSStyleDeclaration,
matchMedia: () =>
({ matches: false, addEventListener() {}, removeEventListener() {} }) as unknown as MediaQueryList,
});
Expand Down
89 changes: 89 additions & 0 deletions apps/desktop/src/main/__tests__/live-context-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@

import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import { act, createElement, type ReactElement } from 'react';
import { createRoot } from 'react-dom/client';
import { parseHTML } from 'linkedom';
import type { SessionEvent } from '@maka/core/events';
import type { ContextDiagnosticsResult } from '@maka/runtime-host/protocol';
import type { SessionInspectorService } from '../../renderer/application/contracts/session-inspector/service.js';
import { useLiveContextUsageState } from '../../renderer/application/contracts/session-inspector/use-live-context-usage.js';
import {
createLiveContextUsageTracker,
liveContextUsageFromDiagnostics,
Expand Down Expand Up @@ -239,12 +244,14 @@ describe('createLiveContextUsageTracker', () => {
const timer = fakeTimer();
const query = scriptedQuery();
const seen: unknown[] = [];
let failures = 0;
const tracker = createLiveContextUsageTracker({
query: query.query,
delayMs: 400,
schedule: timer.schedule,
cancel: timer.cancel,
onChange: (usage) => seen.push(usage),
onReadFailure: () => { failures += 1; },
});
tracker.setTarget({ sessionId: 's1', route: ROUTE });
query.pending[0]!.resolve(available());
Expand All @@ -255,6 +262,7 @@ describe('createLiveContextUsageTracker', () => {
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(seen, [undefined, { usageTokens: 79_436, contextWindow: 128_000 }]);
assert.equal(failures, 1);
tracker.dispose();
});

Expand Down Expand Up @@ -409,3 +417,84 @@ describe('createLiveContextUsageTracker', () => {
assert.deepEqual(seen, [undefined]);
});
});

it('reports pending rather than another target usage during a session switch', async () => {
const original = {
document: globalThis.document,
window: globalThis.window,
Element: globalThis.Element,
HTMLElement: globalThis.HTMLElement,
IS_REACT_ACT_ENVIRONMENT: (globalThis as typeof globalThis & {
IS_REACT_ACT_ENVIRONMENT?: boolean;
}).IS_REACT_ACT_ENVIRONMENT,
};
const { document, window } = parseHTML('<div id="root"></div>');
Object.assign(globalThis, {
document,
window,
Element: window.Element,
HTMLElement: window.HTMLElement,
IS_REACT_ACT_ENVIRONMENT: true,
});
type ContextResult = Awaited<ReturnType<SessionInspectorService['context']>>;
const pending: Array<{ sessionId: string; resolve: (value: ContextResult) => void }> = [];
const inspector: SessionInspectorService = {
trace: async () => { throw new Error('not used'); },
summary: async () => { throw new Error('not used'); },
context: (sessionId: string) =>
new Promise<ContextResult>((resolve) => pending.push({ sessionId, resolve })),
subscribeSessionEvents: () => () => undefined,
subscribeUsageChanges: () => () => undefined,
};
const container = document.querySelector('#root');
assert.ok(container);
const root = createRoot(container);
let renders: Array<{
sessionId: string;
status: 'pending' | 'available' | 'unavailable';
usageTokens: number | undefined;
}> = [];
function Probe(props: { sessionId: string }): ReactElement {
const usage = useLiveContextUsageState({
inspector,
sessionId: props.sessionId,
model: ROUTE.model,
providerType: ROUTE.providerType,
});
renders.push({
sessionId: props.sessionId,
status: usage.status,
usageTokens: usage.status === 'available' ? usage.usage.usageTokens : undefined,
});
return createElement('span');
}

try {
await act(() => root.render(createElement(Probe, { sessionId: 's1' })));
await act(async () => {
pending[0]?.resolve({ ok: true, data: available({ inputTokens: 1_000 }) });
await Promise.resolve();
});
assert.equal(renders.at(-1)?.usageTokens, 1_000);

renders = [];
await act(() => root.render(createElement(Probe, { sessionId: 's2' })));
assert.equal(renders.at(-1)?.status, 'pending');
assert.equal(
renders.some((render) => render.usageTokens === 1_000),
false,
'the old session usage must not appear in any render for the new target',
);
await act(async () => {
pending[1]?.resolve({
ok: true,
data: { status: 'unavailable', reason: 'no_completed_request' },
});
await Promise.resolve();
});
assert.equal(renders.at(-1)?.status, 'unavailable');
} finally {
await act(() => root.unmount());
Object.assign(globalThis, original);
}
});
31 changes: 31 additions & 0 deletions apps/desktop/src/main/__tests__/quote-companion-disposal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
abandonPendingCompanionCopy,
cleanupCompanionCopy,
createFakeWorkbarServices,
dismissCompanionCopy,
ensureCompanionFork,
performCompanionTurn,
type PerformCompanionTurnDeps,
Expand Down Expand Up @@ -88,6 +89,36 @@ afterEach(async () => {
});

describe('quote companion disposal fencing', () => {
it('waits for an interrupted fork to become idle before removing it', async () => {
const defaults = createFakeWorkbarServices();
const running = session('running-side-conversation');
running.runningTurnIds = ['turn-1'];
let listCount = 0;
const cleaned: string[] = [];
const sideChat = {
...defaults.sideChat,
listSessions: async () => {
listCount += 1;
return listCount < 2 ? [running] : [{ ...running, runningTurnIds: [] }];
},
cleanupSessionCopy: async (sessionId: string) => {
cleaned.push(sessionId);
},
};

assert.equal(
await dismissCompanionCopy(
sideChat,
sourceSession.id,
panelId,
running.id,
),
true,
);
assert.deepEqual(cleaned, [running.id]);
assert.ok(listCount >= 2);
});

it('creates a WorkHub companion from an empty boundary without reading coordination turns', async () => {
const defaults = createFakeWorkbarServices();
const coordinationSession = session(
Expand Down
31 changes: 31 additions & 0 deletions apps/desktop/src/main/__tests__/workhub-presentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ test('moves a shared native container while keeping renderer and browser coordin
assert.ok(container.children.has(renderer));
assert.deepEqual({ ...container.boundsUpdates.at(-1) }, host.rect);
assert.deepEqual({ ...renderer.boundsUpdates.at(-1) }, { x: 0, y: 0, width: 800, height: 760 });
h.main.setBounds({ x: 0, y: 0, width: 650, height: 800 });
assert.deepEqual({ ...container.boundsUpdates.at(-1) }, { x: 200, y: 40, width: 450, height: 760 });
assert.deepEqual({ ...renderer.boundsUpdates.at(-1) }, { x: 0, y: 0, width: 450, height: 760 },
'the native viewport clips to Desktop while CSS preserves the inner layout');
await h.command(renderer.webContents, 'detach');
assert.equal(h.container, container);
assert.ok(h.windows[1]!.children.has(container));
Expand Down Expand Up @@ -322,6 +326,9 @@ test('opens an empty floating conversation at its composer height', async () =>
await h.command(view.webContents, 'conversation-layout', { expanded: false, compactHeight: 160 });
assert.equal(h.windows[1]!.resizable, false);
assert.equal(h.windows[1]!.bounds.height, 160, 'compact input still grows programmatically');
h.windows[1]!.setBounds({ ...h.windows[1]!.bounds, width: 320 });
await h.command(view.webContents, 'conversation-layout', { expanded: false, compactHeight: 160 });
assert.equal(h.windows[1]!.bounds.width, 360, 'programmatic compact layout keeps the native minimum width');
await h.command(view.webContents, 'dock');
await h.command(view.webContents, 'conversation-layout', { expanded: false, compactHeight: 110 });
h.movePointer({ x: 1600, y: -900, width: 1000, height: 800 });
Expand Down Expand Up @@ -994,6 +1001,30 @@ test('editing progress grows at its existing bottom and opening interpolates bot
h.controller.dispose();
});

test('external floating bounds changes are not overwritten by an in-flight layout animation', async () => {
const h = await harness(true);
await h.controller.toggle(true);
const view = h.views[0]!;
const floating = h.windows[1]!;
floating.setBounds({ ...floating.bounds, width: 360 });
await h.command(view.webContents, 'conversation-layout', { expanded: true, compactHeight: 160 });
const nativeSetBounds = floating.setBounds.bind(floating);
let deferredBounds: Electron.Rectangle | undefined;
floating.setBounds = (bounds) => {
if (bounds.width === 520 && !deferredBounds) {
deferredBounds = bounds;
return;
}
nativeSetBounds(bounds);
};
floating.setBounds({ ...floating.bounds, width: 520 });
h.advance(100);
nativeSetBounds(deferredBounds!);
h.advance(500);
assert.equal(floating.bounds.width, 520);
h.controller.dispose();
});

test('late progress measurements and send acknowledgements cannot revive a dismissed card or invalidate the next card paint', async () => {
const h = await harness();
await h.controller.prepareControl('first');
Expand Down
8 changes: 4 additions & 4 deletions apps/desktop/src/main/main-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,11 +408,11 @@ export function createMainWindowController(deps: MainWindowControllerDeps): Main
// (see `app-region-hygiene-contract.test.ts`) cover the
// renderer side of the same gate.
resizable: true,
// #824: enforce the sanitizeBounds restore floor at runtime resize too,
// #824: enforce the sanitizeBounds height floor at runtime resize too,
// so the both-present dvh layout fix can't be defeated by dragging the
// window shorter than the 320px restore minimum. Shares SAFE_MIN_HEIGHT
// with sanitizeBounds so the resize floor and the restore floor can't
// drift apart (locked by app-region-hygiene-contract.test.ts).
// window below the restore minimum. Width deliberately remains native-
// resizable below SAFE_MIN_WIDTH; the renderer freezes its conversation
// layout at its own floor and lets the outer shell clip it.
minHeight: SAFE_MIN_HEIGHT,
backgroundColor: initialBg,
// The window stays hidden until `ready-to-show`, so the first visible
Expand Down
Loading
Loading