Skip to content
Open
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 plugins/minimax-ai/minimax-pet/.minimax-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"name": "minimax-pet",
"displayName": "MiniMax Pet",
"version": "0.9.0",
"version": "0.9.1",
"description": "轻量桌宠,聚合主窗口可见会话的任务状态、需要回答的提醒和最新结果,支持外观切换及图集导入。",
"author": "MiniMax",
"icon": "icon.png",
Expand Down
3 changes: 3 additions & 0 deletions plugins/minimax-ai/minimax-pet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ A lightweight desktop companion for MiniMax Code, by **MiniMax**. It shows visib

The pet does not run Agent turns, create independent chats, write to chat inputs, comment on every session, stop tasks, or answer questionnaires. Selecting a task only reads its details; the main window switches only when you click **打开对话** or **去回答**.

Version **0.9.1** fixes dragging the pet in the popped-out desktop window. The window now goes where the pointer is, minus the offset it was grabbed at. The pointer is measured inside the window, so the target is the origin the Host confirmed plus the pointer offset from the grab point. While an update is in flight only the latest pointer position is kept; when the Host answers, the target is recomputed from the confirmed origin. One window update is in flight at a time, and the release waits for the update in flight before the final position goes out. The pet follows the pointer, a held pointer leaves the window where it is, a refused move continues from the position the Host kept, and a refused update never rolls the window back past a later accepted one. The running animation uses the same target.

Version **0.9.0** adds two settings. **Task notifications** sets how many seconds a desktop card stays after a task finishes or fails, entered as a number and saved on the device; the default is 10 and **0** keeps the card until you close it. A card hiding never marks the task read, cards that need your answer never hide, and pointing at or focusing the cards pauses the countdown. **Language** follows the MiniMax Code interface language by default and can be pinned to 简体中文 or English; the whole panel and desktop copy switch, while conversation titles and replies stay as the Agent wrote them.

Version **0.9.0** also gives the desktop window one fixed size, tall enough for the open card. Earlier versions grew the window when a card opened, which moved it on screen and made the pet jump for a frame; now opening or closing a card only shows or hides it. The area above the pet is transparent and passes clicks through to whatever is behind it.
Expand Down Expand Up @@ -47,6 +49,7 @@ Earlier local prototypes use a different plugin ID. The Host gives `minimax-pet`
- An earlier UI version was exercised on macOS in a MiniMax Code development build: three tabs, wide/narrow layouts, task reading, real session/ask activity, import/delete, and desktop round trips. Version 0.6.0 has Node/DOM tests (including the open and theme paths against a simulated Host), an isolated HTTP smoke test, and a browser check of the panel layout outside the Host.
- The default atlas is checked by actual PNG decoding, frame coverage and deterministic rebuild tests. The separate octopus icon is checked by PNG decoding and visual review. On 2026-10-26, the packaged Skill was exercised with a real Agent in the macOS development build to create, validate, preview and import a custom v2 pet; all nine animation previews and the four cardinal mouse-look directions were checked. This does not replace released-client acceptance.
- Version 0.9.0 has Node/DOM tests for the card timer, the preference round trip and the copy of both languages (every key the page uses must exist in both), plus a browser check of the panel and desktop window in both languages against a simulated Host. Not verified for 0.9.0: the language the real client reports on any given install, and both languages inside a real MiniMax Code window.
- Version 0.9.1 has Node tests for the desktop drag: a held pointer leaves the window where it is, a moving pointer is followed, a refused move continues from the position the Host kept, a refused update does not roll the window back past a later accepted one, a fast drag counts every movement once, a pointermove that arrives while the Host is still moving the window does not step the window back, a pending move of one or two pointer steps keeps the target on the pointer, the release waits for the update in flight before the final position, and the run direction follows the window target. Not verified for 0.9.1: dragging the pet inside a real MiniMax Code desktop window, and the timing of the Host's answer to a window update.
- Not verified for 0.6.0: opening a conversation or following the theme inside a real MiniMax Code 3.1.11+ window (panel or desktop pet), any Windows GUI, and released-client installation or GUI acceptance on 3.1.6. Automated tests use synthetic data and do not prove those paths.

## Data & access
Expand Down
4 changes: 4 additions & 0 deletions plugins/minimax-ai/minimax-pet/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

**0.5.11** 新增可选内置宠物 **MMX 章鱼娘**,是一位橙发、白背心、棕格纹短裙的 Q 版全身章鱼娘。在“外观 → 选择宠物”中选择她即可;薄荷色机器人仍为默认宠物。她采用 v1 图集,包含九种动作,不含十六个视线方向,因此鼠标跟随对她不生效;沿用六帧、6.6 秒一轮的慢速待机,不新增权限或改变播放器。图集已通过校验。创作流程和贡献者许可见[素材说明](ARTWORK.md)。

**0.9.1** 修复独立窗口里拖动桌宠的问题。窗口改为跟着指针走,减去抓取时的偏移。指针是在窗口内测量的,所以目标位置 = 宿主确认的窗口原点 + 指针相对抓取点的偏移。窗口更新在途期间只记录最新的指针坐标,宿主应答后再用确认的原点重算目标位置。同一时刻只有一个窗口更新在途,抬手时等在途的那次更新结束,再把最终位置发出去。桌宠跟着指针走,按住不动时窗口停在原地,被宿主拒绝的移动会从宿主保留的位置继续,被拒绝的更新也不会把窗口位置回退到更早一次成功移动之前。跑动方向的动画改用同一个目标位置。

**0.9.0** 新增两项设置。“任务通知”用数字框设置桌面卡片在任务完成或异常后显示多少秒,默认 10 秒,填 0 表示一直显示到手动关闭;设置保存在本机。卡片自动收起不会把任务标记为已读,需要你回答的卡片不会自动收起,鼠标悬停或焦点停留时会暂停计时。“语言”默认跟随 MiniMax Code 的界面语言,也可以固定为简体中文或 English;面板和桌面文案整体切换,对话标题和回复保持 Agent 原文。

**0.9.0** 把桌面窗口固定成一个尺寸,本来就放得下展开的卡片。之前点开卡片会把窗口撑高,窗口在屏幕上跟着移动,桌宠会跳一下;现在开合卡片只显示或隐藏,不动窗口。桌宠上方是透明区域,点击会穿透到底下的窗口。
Expand All @@ -38,6 +40,8 @@

任务监控使用 `agent.sessions/session/observe/cancel`,需要 `session.read` 授权。保留运行时能力和权限检查,缺少接口或关闭授权时显示不可用。“打开对话/去回答”使用 `host.navigation.open`,主题跟随使用 `host.ui.subscribe`,两者都从 MiniMax Code **3.1.11** 起提供;2026-11-10 已确认 3.1.11 的 macOS 与 Windows 安装包在两个正式 CDN 上均可下载。两项调用已对照 MiniMax Code 源码核对:打开插件已持有的会话不需要 `hostAccess` 或其他授权,必须在点击中发起,并会把主窗口带到前台。

0.9.1 的拖拽有 Node 测试:按住不动窗口停在原地,指针移动时窗口跟随,被宿主拒绝的移动从宿主保留的位置继续,被拒绝的更新不会把窗口回退到更早一次成功移动之前,快速拖拽只把同一段位移算一遍,宿主还在移动窗口时到达的 pointermove 不会让窗口往回退一步,在途位移等于一个或两个指针步长时目标位置仍落在指针上,抬手时等在途更新结束再发最终位置,跑动方向跟随窗口目标。未在真实的 MiniMax Code 桌面窗口里验收拖拽,也没有实测宿主对窗口更新的应答时序。

更早的 UI 版本已在 macOS 开发版验收三页签、窄宽布局、真实任务与 ask、导入删除和桌面切换;0.6.0 运行 Node/DOM 测试(含模拟 Host 下的打开对话与主题路径)、隔离 HTTP smoke,并在 Host 之外用浏览器检查了面板布局。默认图集通过 PNG 解码、动画帧覆盖与确定性重建检查;独立的章鱼图标通过 PNG 解码和视觉检查。2026-10-26,已用 macOS 开发版中的真实 Agent 调用随包 Skill,完成自定义 v2 宠物的生成、校验、预览和导入,并检查九种动作预览与鼠标上/右/下/左四向注视。0.6.0 尚未在真实的 MiniMax Code 3.1.11+ 窗口(面板或桌面宠物)中验收打开对话和主题跟随,也未验收 Windows GUI、正式客户端安装或 3.1.6 正式版 GUI;合成数据测试不能证明这些路径。

## 数据与能力边界
Expand Down
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-pet/miniapp/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ <h2 id="connection-title" data-i18n="settings.connection">任务连接</h2>
<details class="panel-help">
<summary data-i18n="settings.help">版本与帮助</summary>
<div class="help-content">
<p class="muted">MiniMax Pet 0.9.0</p>
<p class="muted">MiniMax Pet 0.9.1</p>
<div class="row">
<span id="version-badge" class="badge" hidden></span>
<span id="source-badge" class="badge"></span>
Expand Down
94 changes: 81 additions & 13 deletions plugins/minimax-ai/minimax-pet/miniapp/client/pet-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -896,24 +896,43 @@ function onSpritePointerDown(e) {
} catch (error) {
console.warn('[pet] pointer_capture_failed:', errCode(error), error);
}
const origin = app.winPos ? { ...app.winPos } : null;
app.drag = {
pointerId: e.pointerId,
startX: e.screenX,
startY: e.screenY,
win: app.winPos ? { ...app.winPos } : null,
// The pointer is measured inside the window, so the window follows the
// pointer offset from the grab point.
grabX: e.clientX,
grabY: e.clientY,
// Latest client coordinates of the pointer.
pointer: { x: e.clientX, y: e.clientY },
// How many pointer events arrived; the request in flight remembers the
// count it was built from.
moves: 0,
sentMoves: -1,
// Window origin the Host confirmed; null when it reported no position.
origin: origin ? { ...origin } : null,
// Whether an update is in flight.
sending: false,
// Reference for the run direction: the window origin when it is known,
// otherwise the pointer position inside the window.
ref: origin ? origin.x : e.clientX,
active: false,
dir: null,
extreme: e.screenX,
extreme: origin ? origin.x : e.clientX,
};
}

function onSpritePointerMove(e) {
const d = app.drag;
if (!d || e.pointerId !== d.pointerId) return;
const dx = e.screenX - d.startX;
const dy = e.screenY - d.startY;
// While an update is in flight only the latest pointer position is kept:
// the target is recomputed from the confirmed origin when the Host answers.
d.pointer = { x: e.clientX, y: e.clientY };
d.moves += 1;
if (!d.active) {
if (Math.hypot(dx, dy) <= DRAG_THRESHOLD) return;
// How far the pointer travelled on screen, not inside the window.
const travel = { x: e.clientX - d.grabX, y: e.clientY - d.grabY };
if (Math.hypot(travel.x, travel.y) <= DRAG_THRESHOLD) return;
d.active = true;
app.dragging = true;
app.oneShotToken++;
Expand All @@ -922,14 +941,55 @@ function onSpritePointerMove(e) {
hideQuickbar();
els.sprite.classList.add('is-dragging');
refreshInteractivity();
if (!d.win) console.warn('[pet] drag_without_window_position: no window position yet, animation only');
if (!d.origin) console.warn('[pet] drag_without_window_position: no window position yet, animation only');
}
updateDragDirection(d, e.screenX);
if (!d.win) return;
callBridge(() => win.update({ x: Math.round(d.win.x + dx), y: Math.round(d.win.y + dy) })).catch((error) => {
if (!d.origin) {
// No window position: the pet still runs, but nothing can move.
updateDragDirection(d, e.clientX);
return;
}
flushDrag(d);
}

// Where the window belongs: the origin the Host confirmed plus the pointer
// offset from the grab point. The Host answers with the position it holds, so
// the target is recomputed from the confirmed origin whenever it changes.
function dragTarget(d) {
return {
x: Math.round(d.origin.x + (d.pointer.x - d.grabX)),
y: Math.round(d.origin.y + (d.pointer.y - d.grabY)),
};
}

// One window update at a time. While it is in flight the pointer keeps
// moving, so the target is recomputed from the confirmed origin when the Host
// answers, and a refused update never rolls the window back past a later
// accepted one.
function flushDrag(d) {
if (d.sending || !d.origin) return;
const target = dragTarget(d);
if (target.x === d.origin.x && target.y === d.origin.y) return;
d.sending = true;
d.sentMoves = d.moves;
if (app.drag === d) updateDragDirection(d, target.x);
callBridge(() => win.update({ x: target.x, y: target.y })).then((state) => {
d.sending = false;
const moved = state?.window;
if (moved) {
d.origin = { x: moved.x, y: moved.y };
app.winPos = { x: moved.x, y: moved.y };
} else if (app.winPos) d.origin = { ...app.winPos };
// A pointer event that arrived after the request means the pointer may
// have moved: recompute the target from the confirmed origin.
if (d.moves !== d.sentMoves) flushDrag(d);
}, (error) => {
d.sending = false;
// The Host refused the move and kept the window where it was.
if (app.winPos) d.origin = { ...app.winPos };
const code = errCode(error);
if (code === 'not_permitted') console.debug('[pet] drag_move_rejected: not_permitted (target is off every display)');
else console.warn('[pet] drag_move_failed:', code, error);
if (d.moves !== d.sentMoves) flushDrag(d);
});
}

Expand All @@ -947,8 +1007,8 @@ function updateDragDirection(d, sx) {
d.dir = 'right';
d.extreme = sx;
}
} else if (Math.abs(sx - d.startX) > DIRECTION_HYSTERESIS / 2) {
d.dir = sx > d.startX ? 'right' : 'left';
} else if (Math.abs(sx - d.ref) > DIRECTION_HYSTERESIS / 2) {
d.dir = sx > d.ref ? 'right' : 'left';
d.extreme = sx;
}
if (d.dir !== before) {
Expand All @@ -960,6 +1020,14 @@ function finishDrag(e, cancelled) {
const d = app.drag;
if (!d || e.pointerId !== d.pointerId) return;
app.drag = null;
if (!cancelled) {
// The release position is the most accurate one: the pointer may have
// moved since the last pointermove. While an update is in flight the
// final position goes out once the Host answers it.
d.pointer = { x: e.clientX, y: e.clientY };
d.moves += 1;
flushDrag(d);
}
try {
if (els.sprite.hasPointerCapture(e.pointerId)) els.sprite.releasePointerCapture(e.pointerId);
} catch (error) {
Expand Down
2 changes: 1 addition & 1 deletion sources/minimax-ai/minimax-pet/.minimax-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"name": "minimax-pet",
"displayName": "MiniMax Pet",
"version": "0.9.0",
"version": "0.9.1",
"description": "轻量桌宠,聚合主窗口可见会话的任务状态、需要回答的提醒和最新结果,支持外观切换及图集导入。",
"author": "MiniMax",
"icon": "icon.png",
Expand Down
1 change: 1 addition & 0 deletions sources/minimax-ai/minimax-pet/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ npm run check:package
- Resizing the desktop pet first brings the transparent window inside the available screen area so smaller sprites remain reachable.
- Click-through follows the pixels, never a whole mode: the same rule covers the pet, the bubble, the notification stack, the quick bar, the menu and the open card. An expanded card window is 300x488 around a 284x147 card, so treating "card open" as "window interactive" swallows every click in the margin. Only a drag or an open menu justify the whole window, and a card opened from the keyboard (no pointer sample yet) stays interactive.
- Opening or closing the card must never change the window geometry. The window is one fixed 280 x `deskHeight(scale)` size that already fits the open card; resizing on click moves the native origin, and the previous layout is repainted at the new top-left for a frame, so the pet visibly jumps up (and left, while the card was wider). `dev/desk-interactivity.test.mjs` fails if `openWorkbench` or `closeWorkbench` touches geometry, and checks the fixed height still leaves room for the card at every pet scale. The area above the pet is transparent and forwards clicks, so a permanently tall window costs no desktop clicks.
- Dragging the pet moves the window to where the pointer is, minus the offset it was grabbed at, never by a screen coordinate: `screenX` in the popped-out window is relative to its own frame. The pointer is measured inside the window, so its position is read against the frame the event was measured in: the origin the Host confirmed, or the origin the Host already moved to while the answer is still pending. One `win.update` is in flight at a time; while it is, only the latest target waits, and a refused update falls back to the Host-confirmed position. `dev/window-drag.test.mjs` fails if a refused update rolls the window back past a later accepted one, if a fast drag counts a movement twice, or if a pointermove that arrives while the Host is moving the window steps the window back.
- Opening the card must not change the running sprite animation. Hover runs the `jumping` row; reverting to the base state on open restarts it at frame 0, which reads as a flicker on every click. The menu still takes the pet back to its base animation, because the menu is a deliberate choice.
- Every user-visible string comes from `miniapp/client/i18n.mjs` through `t()`; static markup carries `data-i18n` / `data-i18n-attr`. Keys are `<domain>.<context>.<name>`, interpolation is `{{name}}`, a missing key renders as the key itself, and `dev/i18n.test.mjs` fails when a key the client uses is missing from either language. Conversation titles, replies and error text from the Host are data and stay in the language the Agent wrote them.
- Text created once when a view is built (segmented labels, compact-view buttons) must be re-applied on a language change, or it keeps the language of the moment it was built.
Expand Down
Loading
Loading