Skip to content
Closed
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Bots can be pinned as pixel Window Companions that play their replies, binding o

- Bot settings can export and inspect one verified complete `.botharness-backup`, including retained custom/deleted Memory, current attachments, model templates and independent plans, and the canonical purge checkpoint; the installed local command restores only into a new stopped environment ([#886](https://github.com/BotHarness/DeepSeekBot/issues/886), [guide](docs/settings.md#complete-environment-backup-and-restore)).

- Window Companion bubbles follow the displayed pixel bust's head through tilt, stretch and movement, retain an origin connector when cards move to avoid overlap or viewport edges, and keep text upright; resize updates the clamped character and bubble origin together, while image and unsupported-rig fallbacks use their visible box top ([#1177](https://github.com/BotHarness/DeepSeekBot/issues/1177)).
- Window Companion bubbles follow the displayed pixel bust's head through tilt, stretch and movement, retain an origin connector when cards move to avoid overlap or viewport edges, and keep text upright; resize, dragging, keyboard movement and reduced-motion release update the character and bubble origin together, while image and unsupported-rig fallbacks use their visible box top ([#1177](https://github.com/BotHarness/DeepSeekBot/issues/1177), [#1248](https://github.com/BotHarness/DeepSeekBot/issues/1248)).

- WeChat identity enablement and native typing can inherit independent global defaults or retain Bot-specific overrides; existing choices are preserved on upgrade, and unsupported group/Thread settings stay hidden ([#912](https://github.com/BotHarness/BotHarness/issues/912), [guide](docs/wechat-connection.md)).

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Bot 可以作为像素窗口伙伴固定在窗口里播放回复;绑定一个

- Bot 设置可导出、检查一个完整且已验证的 `.botharness-backup`,包含自定义及已删除身份保留的 Memory、当前附件、模型模板与独立计划,以及真实清除检查点;安装包提供的本地命令仅恢复至新的停止环境([#886](https://github.com/BotHarness/DeepSeekBot/issues/886), [guide](docs/settings.md#complete-environment-backup-and-restore))。

- 窗口伙伴气泡随像素半身的头部倾斜、拉伸与移动定位;卡片因避让或视口限位偏移时保留来源连线,文字保持直立;窗口缩小时人物与连线锚点同步限位,图片与不支持的形象使用可见盒子顶部锚点([#1177](https://github.com/BotHarness/DeepSeekBot/issues/1177))。
- 窗口伙伴气泡随像素半身的头部倾斜、拉伸与移动定位;卡片因避让或视口限位偏移时保留来源连线,文字保持直立;窗口缩放、拖拽、键盘移动与减少动效下的松手均同步更新人物和连线锚点,图片与不支持的形象使用可见盒子顶部锚点([#1177](https://github.com/BotHarness/DeepSeekBot/issues/1177)、[#1248](https://github.com/BotHarness/DeepSeekBot/issues/1248))。

- 微信身份启停与原生输入状态可分别继承全局默认值或保留 Bot 自定义;升级保留现有选择,界面不显示未支持的群聊/Thread 设置([#912](https://github.com/BotHarness/BotHarness/issues/912),[指南](docs/wechat-connection.md))。

Expand Down
27 changes: 27 additions & 0 deletions docs/assets/pr/1248-companion-release-anchor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Direct-motion anchor synchronization

Follow-up #1248 to merged #1183 and parent #1177. Real DSH 0.2.0-rc.1, formal pixel-avatar0.8.0, Chinese,1559×865. Baseline runtime4dbaa; corrected runtimefbb29752. The latter has an identical complete tracked tree to follow-up426167c9 from merged main85e9dfbe. New evidence commits do not change runtime code.

## Actual reduced-motion release

| Before, settled | After, settled |
| ------------------------------------ | -------------------------------------- |
| ![](release-before-settled-dark.png) | ![](reduced-released-reading-dark.png) |

These PNGs do not show the transient defect. A read-only QA observer compares the actual displayed head top-center and SVG connector origin in the first rAF callback after a trusted native pointerup. Before: character bottom865 but connector still points to screen y32 instead of769 (737px error). After: both origins match at y769 (0px). Both later120-sample reading/keyboard/theme/release workloads pass, illustrating why settled checks alone missed the defect. A mounted public View regression independently fails before the fix with300px error and passes after, without executing a subsequent animation frame.

[Continuous baseline recording](release-before.webm) · [Continuous corrected recording](release-after.webm) · [Sanitized qualification](qualification.json)

## Corrected reading and keyboard movement

| Light reading, genuine tool cover during initial reply | Light after native arrow keys | Dark reading |
| ------------------------------------------------------ | ------------------------------- | ----------------------------- |
| ![](reduced-reading-light.png) | ![](reduced-keyboard-light.png) | ![](reduced-reading-dark.png) |

Two exact new Bot-authored replies are independently checked against canonical Channel messages. Normal public character focus retains the reading list; native pause controls stop autonomous walking. Both original absent Avatar Appearance/image fields remain unchanged. All four phases stay in one document; cards remain upright and within the viewport. Console inspection finds0JavaScript errors and all owned test pages/browser are closed.

## Boundaries and preserved failures

Same Profile, identities, history, viewport, theme and scenario; reply marker and sidebar unread count differ. The first baseline setup timed out waiting for visible cards despite real replies. That attempt is retained privately. The next baseline adds live-stream and outside-focus preconditions, so recovery does not prove its cause. It exposes the737px first-release failure; the corrected run then passes the same scenario.

Recordings are continuous silent official MCP output, container-remuxed only. No synthetic reply/SSE/card injection, interpolation, dubbed audio or invented first-frame image. Sampling does not prove all compositor frames, performance, held/reversing drag, moving-hover/grace or genuine background behavior. Those broader #1177/#1167 gates remain separate. Quiet-idle presentation belongs to independent #1173.
120 changes: 120 additions & 0 deletions docs/assets/pr/1248-companion-release-anchor/qualification.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
{
"baselineRuntime": "4dbaa0ad07e1d00b9e3d51ad4e2427552bfa509c",
"correctedRuntime": "fbb29752abb56c02ece77c9e99c4c68285ec6fb7",
"equivalentFollowupCommit": "426167c98efe09c2f9c1bbe85f0d8ea0aba02979",
"followupBase": "85e9dfbee4e966049eea4a5e847b114764008e80",
"originalIntegratedMain": "66d2767a065af49afb999f3792d8754a50fc66ff",
"formalPixelAvatar": "0.8.0",
"viewport": [1559, 865],
"locale": "zh-CN",
"themes": ["light", "dark"],
"before": {
"phases": [
{
"label": "light-reduced-reading",
"samples": 30,
"maxOriginErrorPx": 0,
"allCardsUpright": true
},
{
"label": "keyboard-moved-reduced",
"samples": 30,
"maxOriginErrorPx": 0,
"allCardsUpright": true
},
{
"label": "dark-reduced-reading",
"samples": 30,
"maxOriginErrorPx": 0,
"allCardsUpright": true
},
{
"label": "native-reduced-release-reading",
"samples": 30,
"maxOriginErrorPx": 0,
"allCardsUpright": true
}
],
"firstObservedFrameAfterRelease": {
"policy": "reduce",
"phase": "rest",
"reading": "true",
"characterBottom": 865,
"viewportHeight": 865,
"expectedScreenPoint": {
"x": 745.484375,
"y": 769
},
"connectorScreenPoint": {
"x": 745.484375,
"y": 32
},
"originErrorPx": 737,
"visibleCards": 2,
"actualTrustedPointerup": true
},
"closedOwnedBrowser": true
},
"after": {
"phases": [
{
"label": "light-reduced-reading",
"samples": 30,
"maxOriginErrorPx": 0,
"allCardsUpright": true
},
{
"label": "keyboard-moved-reduced",
"samples": 30,
"maxOriginErrorPx": 0,
"allCardsUpright": true
},
{
"label": "dark-reduced-reading",
"samples": 30,
"maxOriginErrorPx": 0,
"allCardsUpright": true
},
{
"label": "native-reduced-release-reading",
"samples": 30,
"maxOriginErrorPx": 0,
"allCardsUpright": true
}
],
"firstObservedFrameAfterRelease": {
"policy": "reduce",
"phase": "rest",
"reading": "true",
"characterBottom": 865,
"viewportHeight": 865,
"expectedScreenPoint": {
"x": 745.484375,
"y": 769
},
"connectorScreenPoint": {
"x": 745.484375,
"y": 769
},
"originErrorPx": 0,
"visibleCards": 2,
"actualTrustedPointerup": true
},
"exactCanonicalBotReplies": 2,
"originalAppearanceUnchanged": true,
"sameDocument": true,
"consoleErrors": 0,
"closedOwnedBrowser": true
},
"limits": [
"Native official MCP drag/release and keyboard operations; two newly generated canonical Bot replies, no synthetic presentation, replies or SSE.",
"Normal public character focus retains reading cards; walking paused through native controls; OS reduced-motion preference selected at browser startup.",
"Dark before/after PNGs are settled states and do not show the transient failure. The rAF observer catches the first observed callback after trusted pointerup, not every compositor frame.",
"Same Profile, identities, history, viewport, theme and scenario; reply marker and sidebar unread count differ. Initial light capture includes a genuine transient tool cover.",
"Before: 120 later samples passed despite a 737px first-release mismatch. After: 120 samples plus the first release observation pass. This is bounded geometry qualification, not performance or proof about unsampled frames.",
"An earlier initial-setup attempt timed out waiting for two visible cards. Its native replies and failed capture are retained privately; no root cause is claimed. The next baseline attempt added both live-stream preconditions and outside focus before arrival, so recovery does not establish which change mattered.",
"Before runtime 4dbaa was merged by PR1183 while the correction was being prepared. The correction was moved to a dedicated follow-up from that squash merge; complete tracked trees fbb29752 and 426167c9 are identical.",
"Continuous silent official MCP recordings, remux only; no audio, frame interpolation or invented first-frame screenshot.",
"Held/reversing drag, moving hover/grace and genuine background qualification remain under1177; performance remains under1167. This fix does not qualify those scenarios."
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
26 changes: 17 additions & 9 deletions packages/client/src/client/window-companion-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function WindowCompanionView({
latest.current = view;
const [motion] = useState(() => new CompanionMotion());
const [point, setPoint] = useState(motion.point);
const presentPoint = useRef<(next: CompanionPoint) => void>(setPoint);
const displayedPoint = useRef(point);
displayedPoint.current = point;
const anchor = useRef<AvatarAnchor | undefined>(undefined);
Expand Down Expand Up @@ -119,7 +120,7 @@ export function WindowCompanionView({
if (!pointer.current || pointer.current.id !== event.pointerId || motion.point.phase !== 'drag')
return;
pointer.current = undefined;
setPoint(motion.release(performance.now(), reducedMotion(), true));
presentPoint.current(motion.release(performance.now(), reducedMotion(), true));
persistPosition();
};
const leave = (): void => {
Expand Down Expand Up @@ -196,6 +197,10 @@ export function WindowCompanionView({
}
}
};
presentPoint.current = (next) => {
placeBubble(next, anchor.current?.read());
setPoint(next);
};
const measure = () => {
const box = node.getBoundingClientRect();
const width = box.width;
Expand All @@ -219,8 +224,7 @@ export function WindowCompanionView({
drag.originX = next.x;
drag.originY = next.y;
}
setPoint(next);
placeBubble(next, anchor.current?.read());
presentPoint.current(next);
};
const tick = (now: number) => {
frame = 0;
Expand Down Expand Up @@ -272,7 +276,8 @@ export function WindowCompanionView({
const target = node.querySelector('.bh-companion');
if (target) observer?.observe(target);
const policy = new MutationObserver(() => {
if (reducedMotion()) setPoint(motion.advance(0, true, false, direction.current));
if (reducedMotion())
presentPoint.current(motion.advance(0, true, false, direction.current));
});
policy.observe(document.documentElement, {
attributes: true,
Expand All @@ -291,6 +296,7 @@ export function WindowCompanionView({
if (exit.current !== undefined) clearTimeout(exit.current);
if (clickReset.current !== undefined) clearTimeout(clickReset.current);
pointer.current = undefined;
presentPoint.current = setPoint;
if (latest.current.selection) bubbleOwner.remove(latest.current.selection.botId);
root.current = null;
};
Expand Down Expand Up @@ -592,7 +598,9 @@ export function WindowCompanionView({
}
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
event.preventDefault();
setPoint(motion.move(motion.point.x + (event.key === 'ArrowLeft' ? -24 : 24)));
presentPoint.current(
motion.move(motion.point.x + (event.key === 'ArrowLeft' ? -24 : 24)),
);
persistPosition();
}
}}
Expand All @@ -610,7 +618,7 @@ export function WindowCompanionView({
originY: motion.point.y,
moved: false,
};
setPoint(motion.grab(performance.now(), reducedMotion()));
presentPoint.current(motion.grab(performance.now(), reducedMotion()));
}}
onPointerMove={(event) => {
const drag = pointer.current;
Expand All @@ -621,7 +629,7 @@ export function WindowCompanionView({
drag.lastY = event.clientY;
drag.moved ||= Math.hypot(dx, dy) > 6;
if (!drag.moved) return;
setPoint(
presentPoint.current(
motion.drag(drag.originX + dx, drag.originY + dy, performance.now(), reducedMotion()),
);
}}
Expand All @@ -630,7 +638,7 @@ export function WindowCompanionView({
if (!drag || drag.id !== event.pointerId) return;
event.currentTarget.releasePointerCapture(event.pointerId);
if (drag.moved) {
setPoint(motion.release(performance.now(), reducedMotion()));
presentPoint.current(motion.release(performance.now(), reducedMotion()));
persistPosition();
event.preventDefault();
clickReset.current = setTimeout(() => {
Expand All @@ -639,7 +647,7 @@ export function WindowCompanionView({
}, 0);
} else {
pointer.current = undefined;
setPoint(motion.release(performance.now(), reducedMotion(), true));
presentPoint.current(motion.release(performance.now(), reducedMotion(), true));
}
}}
onPointerCancel={cancelDrag}
Expand Down
26 changes: 25 additions & 1 deletion packages/client/test/window-companion-view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ vi.mock('@deepseek-ai/dsh-client-ui-primitives', () => ({
IconNewChatOutlineRegular: () => null,
}));

it('projects the owning Avatar screen point into an offset overlay and remeasures it after resize', async () => {
it('keeps the Avatar tether aligned through resize and the first reduced-motion release commit', async () => {
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
const frames = new Map<number, FrameRequestCallback>();
let frameId = 0;
Expand Down Expand Up @@ -115,6 +115,29 @@ it('projects the owning Avatar screen point into an offset overlay and remeasure
expect(node.querySelector('.bh-companion-tether path')?.getAttribute('d')).toMatch(
/^M 264 404 /u,
);
await act(() => {
document.documentElement.dataset['botharnessMotion'] = 'reduce';
});
const character = node.querySelector<HTMLButtonElement>('.bh-companion-character')!;
character.setPointerCapture = vi.fn();
character.releasePointerCapture = vi.fn();
const pointer = (type: string, x: number, y: number) => {
const event = new MouseEvent(type, { bubbles: true, clientX: x, clientY: y, button: 0 });
Object.defineProperty(event, 'pointerId', { value: 1 });
character.dispatchEvent(event);
};
await act(() => pointer('pointerdown', 264, 450));
await act(() => pointer('pointermove', 164, 150));
await act(frame);
expect(node.querySelector<HTMLElement>('.bh-companion')?.style.bottom).toBe('300px');
expect(node.querySelector('.bh-companion-tether path')?.getAttribute('d')).toMatch(
/^M 164 104 /u,
);
await act(() => pointer('pointerup', 164, 150));
expect(node.querySelector<HTMLElement>('.bh-companion')?.style.bottom).toBe('0px');
expect(node.querySelector('.bh-companion-tether path')?.getAttribute('d')).toMatch(
/^M 164 404 /u,
);
const visibility = vi.spyOn(document, 'hidden', 'get').mockReturnValue(true);
await act(() => document.dispatchEvent(new Event('visibilitychange')));
expect(frames.size).toBe(0);
Expand All @@ -125,6 +148,7 @@ it('projects the owning Avatar screen point into an offset overlay and remeasure
node.remove();
measurement.mockRestore();
vi.unstubAllGlobals();
delete document.documentElement.dataset['botharnessMotion'];
}
});

Expand Down
Loading