You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/10 02:34:00 UTC
(maka) 02/02: fix(desktop): stabilize WorkHub fold motion without changing layout
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch feat/workhub-progress-input
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 09be211d494c98bd8e56e75e4ccf4bcabef1b194
Author: Wang <m4...@gmail.com>
AuthorDate: Thu Sep 10 10:33:50 2026 +0800
fix(desktop): stabilize WorkHub fold motion without changing layout
Generated-by: OpenAI Codex
---
apps/desktop/e2e/workhub-layout.spec.ts | 20 +++++++++++++++++++-
.../src/main/__tests__/workhub-presentation.test.ts | 5 ++++-
apps/desktop/src/main/workhub-presentation.ts | 6 +++++-
.../renderer/features/workhub/ui/workhub-root.tsx | 20 ++++++++++++++++++++
apps/desktop/src/renderer/styles/workhub.css | 6 ++++++
5 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/apps/desktop/e2e/workhub-layout.spec.ts b/apps/desktop/e2e/workhub-layout.spec.ts
index ad2b050257..fb799c552d 100644
--- a/apps/desktop/e2e/workhub-layout.spec.ts
+++ b/apps/desktop/e2e/workhub-layout.spec.ts
@@ -252,7 +252,25 @@ test('WorkHub uses its coordination model and shared attachment composer', async
expect(await workhub.evaluate(() => innerHeight)).toBe(longInputHeight);
await editor.fill('Keep this draft while folding the conversation.');
await expect.poll(() => workhub.evaluate(() => innerHeight)).toBe(compactHeight);
- await workhub.getByRole('button', { name: /展开对话|Expand conversation/ }).click();
+ // Sample the real native resize, including repeated folds. The composer
+ // must stay inside the window while its original 12px gutter interpolates.
+ for (const expanded of [true, false, true]) {
+ const motion = await workhub.getByRole('button', { name: expanded ? /展开对话|Expand conversation/ : /收起对话|Collapse conversation/ }).evaluate((button) => new Promise<{ bottom: number; left: number }[]>((resolve) => {
+ const frames: { bottom: number; left: number }[] = [];
+ const started = performance.now();
+ const sample = () => {
+ const rect = document.querySelector('.workHubComposerSurface')!.getBoundingClientRect();
+ frames.push({ bottom: innerHeight - rect.bottom, left: rect.left });
+ if (performance.now() - started < 500) requestAnimationFrame(sample);
+ else resolve(frames);
+ };
+ (button as HTMLButtonElement).click();
+ requestAnimationFrame(sample);
+ }));
+ expect(motion.every(({ bottom, left }) => bottom >= -0.5 && bottom <= 12.5 && left >= -0.5 && left <= 12.5)).toBe(true);
+ expect(motion.some(({ bottom }) => bottom > 0.5 && bottom < 11.5)).toBe(true);
+ expect(motion.at(-1)!.bottom).toBeCloseTo(expanded ? 12 : 0);
+ }
await expect(workhub.locator('.workHubHistory')).toBeVisible();
await expect.poll(() => workhub.evaluate(() => window.innerHeight)).toBe(expandedHeight);
await expect.poll(floatingBottom).toBe(anchoredBottom);
diff --git a/apps/desktop/src/main/__tests__/workhub-presentation.test.ts b/apps/desktop/src/main/__tests__/workhub-presentation.test.ts
index 91dea1bc0b..6bb419b91f 100644
--- a/apps/desktop/src/main/__tests__/workhub-presentation.test.ts
+++ b/apps/desktop/src/main/__tests__/workhub-presentation.test.ts
@@ -130,7 +130,9 @@ async function harness(animate = false, displayFrequency = 60) {
runInNewContext(output.outputFiles[0]!.text, {
module, exports: module.exports, console, process, URL, Error,
performance: { now: () => now },
- setTimeout: (callback: () => void, delay: number) => { timers.set(++timerId, { at: now + delay, callback }); return timerId; },
+ // Native timers truncate fractional delays, so callbacks can precede a
+ // display deadline. Model that instead of an ideal fractional clock.
+ setTimeout: (callback: () => void, delay: number) => { timers.set(++timerId, { at: now + Math.max(1, Math.floor(delay)), callback }); return timerId; },
clearTimeout: (id: number) => timers.delete(id),
require: (name: string) => name === 'electron' ? {
BrowserWindow: FakeWindow, WebContentsView: FakeView,
@@ -618,6 +620,7 @@ test('native resize callbacks do not submit duplicate view bounds and follow dis
const before = view.boundsUpdates.length;
h.advance(9);
assert.ok(view.boundsUpdates.length > before, 'a 120Hz display gets its next animation frame before 16ms');
+ assert.equal(view.boundsUpdates.length - before, 1, 'an early timer must not submit a second resize for the same display frame');
h.advance(411);
assert.equal(h.windows[1]!.bounds.height, 720);
for (let index = 1; index < view.boundsUpdates.length; index++) {
diff --git a/apps/desktop/src/main/workhub-presentation.ts b/apps/desktop/src/main/workhub-presentation.ts
index 304285508b..78718329d1 100644
--- a/apps/desktop/src/main/workhub-presentation.ts
+++ b/apps/desktop/src/main/workhub-presentation.ts
@@ -161,6 +161,7 @@ export function createWorkHubPresentation(deps: WorkHubPresentationDeps) {
const started = performance.now();
const frequency = screen.getDisplayMatching(bounds).displayFrequency;
const frameDuration = 1000 / (Number.isFinite(frequency) && frequency > 0 ? frequency : 60);
+ let frame = 0;
let previous = initial;
const tick = () => {
if (disposed || window.isDestroyed() || floating !== window || placement !== 'floating') {
@@ -185,7 +186,10 @@ export function createWorkHubPresentation(deps: WorkHubPresentationDeps) {
// Keep frame deadlines independent of native resize work; do not add
// another full frame's delay after every setBounds/resize callback.
const elapsed = performance.now() - started;
- const nextFrame = Math.min(RESIZE_DURATION, (Math.floor(elapsed / frameDuration) + 1) * frameDuration);
+ // Timers can fire just before their deadline. Always advance the frame
+ // index so an early callback cannot submit two native resizes per frame.
+ frame = Math.max(frame + 1, Math.floor(elapsed / frameDuration) + 1);
+ const nextFrame = Math.min(RESIZE_DURATION, frame * frameDuration);
resizeTimer = setTimeout(tick, Math.max(1, nextFrame - elapsed));
}
else cancelFloatingAnimation();
diff --git a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
index 5be98e92ec..bda68d5c4d 100644
--- a/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
+++ b/apps/desktop/src/renderer/features/workhub/ui/workhub-root.tsx
@@ -90,6 +90,26 @@ export function WorkHubRoot() {
const editingProgress = progress && editingProgressRequest === presentation.progressRequest;
const floating = presentation?.placement === 'floating';
const showConversation = !progress && (!floating || conversationExpanded);
+ const dockMotion = useRef<{ floating: boolean; progress: boolean; expanded: boolean; padding: Keyframe; animation?: Animation }>(undefined);
+ useLayoutEffect(() => {
+ const dock = composerSurface.current?.parentElement?.parentElement;
+ if (!dock) return;
+ const readPadding = (): Keyframe => {
+ const style = getComputedStyle(dock);
+ return { paddingInlineStart: style.paddingInlineStart, paddingInlineEnd: style.paddingInlineEnd, paddingBottom: style.paddingBottom };
+ };
+ const previous = dockMotion.current;
+ const from = previous?.animation?.playState === 'running' ? readPadding() : previous?.padding;
+ previous?.animation?.cancel();
+ const padding = readPadding();
+ // Animate only a live expand/collapse. Summoning a parked renderer must
+ // paint its final gutter immediately; interrupted motion resumes in place.
+ const animation = floating && previous?.floating && !progress && !previous.progress && previous.expanded !== showConversation && from && !window.matchMedia('(prefers-reduced-motion: reduce)').matches
+ ? dock.animate([from, padding], { duration: 420, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' })
+ : undefined;
+ dockMotion.current = { floating, progress, expanded: showConversation, padding, animation };
+ }, [floating, progress, showConversation]);
+ useEffect(() => () => dockMotion.current?.animation?.cancel(), []);
const editProgress = () => {
if (progress) setEditingProgressRequest(presentation.progressRequest);
};
diff --git a/apps/desktop/src/renderer/styles/workhub.css b/apps/desktop/src/renderer/styles/workhub.css
index 5591569493..e583485bbc 100644
--- a/apps/desktop/src/renderer/styles/workhub.css
+++ b/apps/desktop/src/renderer/styles/workhub.css
@@ -190,6 +190,12 @@
.workHubLive[data-placement='floating'][data-conversation-expanded='false']::before { opacity: 1; }
.workHubLive[data-placement='floating'] .workhub-anchor-rail { display: none; }
.workHubLive[data-placement='floating'] .maka-chat-layout { background: transparent; }
+ /* Keep the sticky dock bottom-aligned even before the native window has
+ grown enough to fit its composer, 32px scroll button and 12px gaps. */
+ .workHubLive[data-placement='floating'][data-conversation-expanded='true'] > .maka-chat-layout {
+ position: absolute; bottom: 0; width: 100%; height: calc(100% - 32px);
+ min-height: calc(var(--workhub-composer-height, 96px) + 56px);
+ }
.workHubLive[data-placement='floating'] .maka-composer {
padding: 0;
--_chat-composer-radius: 20px;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org