Skip to content
Merged
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"name": "minimax-bot",
"displayName": "MiniMax Bot",
"version": "0.7.4",
"version": "0.7.5",
"description": "Turn MiniMax Code into an always-on team of Bots. A Chief Bot helps you build the team; in group chats the admin answers and @mentions hand work to other members. Every Bot has its own octopus avatar and memory, with scheduled tasks and usage stats.",
"author": "MiniMax Code",
"icon": "icon.png",
Expand Down
2 changes: 2 additions & 0 deletions plugins/minimax-ai/minimax-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Copy this directory, including the hidden `.minimax-plugin/` directory, into the
- **Chat filters**: the icon bar switches between all chats, unread, groups and direct chats, with unread counts; your choice is remembered.
- **Language**: English and Chinese. By default it follows the operating system's preferred language (the first English or Chinese entry in `navigator.languages`; other languages fall back to English), or you can fix it under Settings → General → Language. The interface, errors, system notices, Chief's default setup, the context Bots receive and the MCP tool descriptions all switch; replies the Bots wrote and text you entered stay as they are.
- **Reactions**: react to finished Bot replies in private and group chats. The quick bar offers 👍 ❤️ 🙌 😂 😭 😮, and more emoji can be browsed by category or searched in English or Chinese. Each message holds one reaction: picking a new one replaces it, and picking the same one again removes it. Reactions are saved locally, survive refreshes and restarts, and sync to other open windows. They do not trigger a Bot reply and are not sent to the model.
- **Reply actions**: the reaction, copy, quote-reply and open-session buttons sit at the bottom-left of a reply, under its last line. A long reply therefore keeps its actions within reach at the end of the text instead of pushing them off the top of the message. The reaction indicator sits directly above that row and is painted with the app's own tag colours, so it follows the light and dark themes.
- **Folded working messages**: the Host records a text part for every turn a Bot takes between two tool calls, so a reply that worked for a while arrives with many parts. The newest three bubbles stay in the open, exactly where the flat layout put them, the answer among them. The earlier ones keep their own shape but move into a folded area above them: one line says how many of them there are, and opening it shows them in a fixed height area with its own scrollbar, so the conversation above stays in reach. Hovering a bubble of such a reply tells what that bubble said, the way the task rows and the attachments do.

## 0.7.0 Reliable delegation and result delivery

Expand Down
98 changes: 72 additions & 26 deletions plugins/minimax-ai/minimax-bot/miniapp/client/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
--shadow-pill: 0 2px 8px -1px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
--scrim: rgba(10, 10, 10, 0.28);
--ring: var(--sidebar);
--reaction-ink: #fff;
color-scheme: light;
}

Expand Down Expand Up @@ -757,10 +756,12 @@ button {

.feed {
flex: 1;
/* Padding alone is 84px. Without a real floor, task rows (min-height: auto) shrink the transcript to that sliver. */
/* Padding alone is 112px. Without a real floor, task rows (min-height: auto) shrink the transcript to that sliver. */
min-height: min(220px, 48%);
overflow-y: auto;
padding: 72px 24px 12px;
/* The bottom room holds the action row of the last reply: 12px of padding plus the
28px the row needs, so scrolling to the end never clips the buttons. */
padding: 72px 24px 40px;
scroll-behavior: auto;
}

Expand Down Expand Up @@ -818,6 +819,55 @@ button {
max-width: 100%;
}

/* The earlier bubbles of a long reply. The Host records a text part per assistant turn, so a
reply that ran a dozen tools arrives with dozens of bubbles; they live here instead, behind a
one line summary. The area that opens has a fixed height and its own scrollbar, so opening
it never makes the feed grow. */
.bubble-fold {
max-width: min(88%, 680px);
margin: 2px 0;
border: 1px solid var(--line);
border-radius: 14px;
background: var(--tag);
overflow: hidden;
}

.bubble-fold > summary {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
font-size: 12.5px;
color: var(--muted);
cursor: pointer;
list-style: none;
user-select: none;
}

.bubble-fold > summary::-webkit-details-marker { display: none; }
.bubble-fold > summary:hover { color: var(--ink); background: var(--hover); }
.bubble-fold > summary svg { flex: none; transition: transform 120ms ease; }
.bubble-fold[open] > summary svg { transform: rotate(180deg); }

.bubble-fold-closed { display: inline; }
.bubble-fold-open { display: none; }
.bubble-fold[open] > summary .bubble-fold-closed { display: none; }
.bubble-fold[open] > summary .bubble-fold-open { display: inline; }

/* The fixed height with a scrollbar is the point of the fold: the bubbles inside it scroll,
the feed around it does not. */
.bubble-fold-body {
max-height: min(320px, 40vh);
overflow-y: auto;
overscroll-behavior: contain;
padding: 2px 10px 10px;
display: flex;
flex-direction: column;
gap: 6px;
}

.bubble-fold-body .bubble { max-width: 100%; }

.msg.user .bubble-wrap {
align-items: flex-end;
/* The width bound belongs here, not on .bubble: this wrapper is shrink-to-fit, so a
Expand All @@ -827,13 +877,17 @@ button {
max-width: min(88%, 680px);
}

/* The action row of a reply: reaction, copy, quote and open-session. It hangs below the
bubble at the left, the way a comment toolbar does, so a long reply cannot push the
buttons out of reach while the reader is at the end of it. */
.msg-tools {
display: flex;
gap: 2px;
height: 26px;
margin-top: 2px;
opacity: 0;
transition: opacity 120ms ease;
white-space: nowrap;
}

.msg:hover .msg-tools,
Expand Down Expand Up @@ -2951,34 +3005,30 @@ fieldset.field {
.avatar-errors { max-height: 100px; overflow-y: auto; padding-left: 20px; }
.oc-img { object-fit: cover; border-radius: 22%; }

/* Reactions sit on the last bubble; the picker uses the same compact chat scale. */
/* Reactions sit on the last bubble; the picker uses the same compact chat scale. The
indicator wears the app's own tag language — a soft surface and a hairline border — so
it reads as part of the bubble in either theme. */
.reaction-bar {
display: inline-flex;
align-items: center;
max-width: 100%;
padding: 2px;
border: 3px solid var(--bg);
padding: 1px;
border: 1px solid var(--line-strong);
border-radius: 999px;
background: var(--accent);
color: var(--reaction-ink);
background: var(--tag);
color: var(--muted);
}
.reaction-bar:empty { display: none; }
.bubble:has(> .msg-tools) {
max-width: min(calc(100% - var(--message-tools-width, 92px)), 680px);
}
.bubble > .msg-tools {
position: absolute;
left: calc(100% + 8px);
top: 7px;
margin: 0;
white-space: nowrap;
}
/* The indicator keeps the place a comment's reaction row takes: inside the message box, at
its bottom-left, and therefore above the reply's action row. The bubble reserves the
room for it so the last line of the reply stays readable. */
.bubble > .reaction-bar {
position: absolute;
right: -6px;
top: -22px;
left: 14px;
bottom: 6px;
}
.bubble:has(> .reaction-bar:not(:empty)) { margin-top: 14px; }
/* 6px below the bubble edge + a 32px indicator (28px chip + 2px padding + 2px border). */
.bubble:has(> .reaction-bar:not(:empty)) { padding-bottom: 40px; }
.reaction-chip {
display: inline-flex;
align-items: center;
Expand All @@ -2994,7 +3044,7 @@ fieldset.field {
line-height: 1;
}
.reaction-chip { font-size: 21px; }
.reaction-chip:hover { background: color-mix(in srgb, var(--reaction-ink) 20%, transparent); }
.reaction-chip:hover { background: var(--hover); color: var(--ink); }
.reaction-bar button:focus-visible { outline-color: var(--ink); }
.reaction-picker {
position: fixed;
Expand Down Expand Up @@ -3119,10 +3169,6 @@ fieldset.field {
.gmsg:hover .msg-tools,
.gmsg:focus-within .msg-tools,
.msg-tools:has([aria-expanded='true']) { opacity: 1; }
@media (max-width: 720px) {
.bubble:has(> .msg-tools) { max-width: min(88%, 680px); margin-bottom: 28px; }
.bubble > .msg-tools { top: calc(100% + 2px); left: 0; }
}
@media (hover: none) {
.msg-tools { opacity: 1; }
.emoji-quick .emoji-option { width: 40px; height: 40px; }
Expand Down
90 changes: 74 additions & 16 deletions plugins/minimax-ai/minimax-bot/miniapp/client/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -938,21 +938,90 @@ function legacyParts(m) {
return parts;
}

/**
* The Host opens a text part for every assistant turn between two tool calls, so a reply that
* worked for a while arrives with dozens of them, and one bubble per part filled the screen.
* The newest bubbles stay in the open, exactly where the flat layout put them; the earlier ones
* fold away behind a one line summary.
*/
const FOLD_AFTER_PARTS = 3;

/** The bubble's own text, short enough to be a hover hint. */
function hintOf(text) {
const flat = String(text ?? '').replace(/\s+/g, ' ').trim();
return flat.length > 160 ? `${flat.slice(0, 160)}…` : flat;
}

/**
* The earlier bubbles of a long reply: folded behind a one line summary, and the area that
* opens has a fixed height with its own scrollbar, so the feed keeps its length.
*/
function foldView(m, parts, presentation) {
const inner = () =>
parts.map((part, index) => ({
key: `text:${index}`,
signature: JSON.stringify([part.text, presentation]),
create: () => h('div', { class: 'bubble', title: hintOf(part.text) }, markdownNode(part.text, m.chatId)),
update: (bubble) => {
markdownNode(part.text, m.chatId, bubble.firstElementChild);
bubble.title = hintOf(part.text);
return bubble;
},
}));
const body = h('div', { class: 'bubble-fold-body' });
reconcileChildren(body, inner());
return {
key: 'fold',
signature: JSON.stringify([parts.map((part) => part.text), presentation, locale()]),
create: () =>
h(
'details',
{ class: 'bubble-fold' },
h(
'summary',
{ class: 'bubble-fold-summary', title: t('fold.hint', { n: parts.length }) },
icon('down', 12),
h('span', { class: 'bubble-fold-closed' }, t('fold.show', { n: parts.length })),
h('span', { class: 'bubble-fold-open' }, t('fold.hide')),
),
body,
),
update: (node) => {
reconcileChildren(node.querySelector('.bubble-fold-body'), inner());
const summary = node.querySelector('.bubble-fold-summary');
if (summary) summary.title = t('fold.hint', { n: parts.length });
const closed = node.querySelector('.bubble-fold-closed');
if (closed) closed.textContent = t('fold.show', { n: parts.length });
return node;
},
};
}

/** Keep part order exactly as recorded; matching prefixes are not evidence of duplication. */
function partViews(m, inGroup) {
const parts = m.key ? [{ type: 'text', text: displayText(m) }] : m.parts ?? legacyParts(m);
const lastText = parts.findLastIndex((part) => part.type === 'text' && part.text?.trim());
const views = [];
const presentation = presentationKey();
const textParts = [];
parts.forEach((part, index) => {
if (part.type === 'text' && part.text?.trim()) textParts.push(index);
});
// The newest bubbles stay in the open, where the flat layout had them; the earlier ones fold.
const folded = textParts.length > FOLD_AFTER_PARTS ? textParts.slice(0, -FOLD_AFTER_PARTS) : [];
const foldedSet = new Set(folded);
if (folded.length) views.push(foldView(m, folded.map((index) => parts[index]), presentation));
parts.forEach((part, index) => {
const key = `part:${index}`;
if (part.type === 'text' && part.text?.trim()) {
if (foldedSet.has(index)) return;
views.push({
key,
signature: JSON.stringify([part.text, presentation, index === lastText ? [m.status, m.userReactions, m.reactionsRevision, displayText(m), m.sessionId, botById(m.botId)?.name] : null]),
create: () => updateBubbleControls(h('div', { class: 'bubble' }, markdownNode(part.text, m.chatId)), m, index === lastText),
create: () => updateBubbleControls(h('div', { class: 'bubble', title: folded.length ? hintOf(part.text) : undefined }, markdownNode(part.text, m.chatId)), m, index === lastText),
update: (bubble) => {
markdownNode(part.text, m.chatId, bubble.firstElementChild);
if (folded.length) bubble.title = hintOf(part.text);
return updateBubbleControls(bubble, m, index === lastText);
},
});
Expand All @@ -971,22 +1040,9 @@ function partViews(m, inGroup) {
return views;
}

/** The bubble carries the reply text and the reaction badge; the action row lives below it. */
function updateBubbleControls(bubble, m, isLast) {
updateBubbleReactions(bubble, m, isLast);
const old = bubble.querySelector(':scope > .msg-tools');
if (!isLast || m.status === 'streaming') {
old?.remove();
bubble.style.removeProperty('--message-tools-width');
} else {
const signature = JSON.stringify([displayText(m), m.sessionId, botById(m.botId)?.name, locale()]);
if (old?.dataset.signature !== signature) {
const tools = msgTools(displayText(m), m);
tools.dataset.signature = signature;
if (old) old.replaceWith(tools);
else bubble.append(tools);
bubble.style.setProperty('--message-tools-width', `${tools.children.length * 28 + 8}px`);
}
}
return bubble;
}

Expand Down Expand Up @@ -1103,7 +1159,9 @@ function botMessage(m, existing) {
notes.forEach((node, index) => views.push({ key: `note:${index}`, signature: node.outerHTML, create: () => node }));
if (m.question) views.push({ key: 'question', signature: JSON.stringify([m.question, m.answering, locale()]), create: () => questionCard(m) });
if (m.answerSummary?.length) views.push({ key: 'answers', signature: JSON.stringify(m.answerSummary), create: () => h('dl', { class: 'answered' }, m.answerSummary.map((a) => [h('dt', null, a.question), h('dd', null, a.answer)])) });
if (m.status !== 'streaming' && views.some((view) => view.key === 'reactions')) views.push({ key: 'tools', signature: JSON.stringify([displayText(m), m.sessionId, name, locale()]), create: () => msgTools(displayText(m), m) });
// The action row hangs below the reply, at its bottom-left. Anchoring it to the bubble's
// top pushed the buttons off-screen as soon as the reply grew past the viewport.
if (m.status !== 'streaming') views.push({ key: 'tools', signature: JSON.stringify([displayText(m), m.sessionId, name, locale()]), create: () => msgTools(displayText(m), m) });
const compatible = existing?.matches(inGroup ? 'article.gmsg' : 'article.msg');
const article = compatible ? existing : h('article', { class: inGroup ? 'gmsg' : 'msg' });
article.setAttribute('aria-label', t('a11y.reply', { name }));
Expand Down
10 changes: 8 additions & 2 deletions plugins/minimax-ai/minimax-bot/miniapp/client/reactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,14 @@ function replaceBar(bar, m) {
/** Keep the badge on the final text bubble without replacing its text or media. */
export function updateBubbleReactions(bubble, m, isLast) {
const bar = bubble.querySelector(':scope > .reaction-bar');
if (!isLast) bar?.remove();
else if (bar) replaceBar(bar, m);
if (!isLast) {
// The reply gained a new final part, so the badge moves to it. The picker opened from
// this bubble's action row would otherwise stay open over a reply that was replaced.
if (bar) {
bar.remove();
closeReactionPicker();
}
} else if (bar) replaceBar(bar, m);
else bubble.append(reactionBar(m));
}

Expand Down
8 changes: 8 additions & 0 deletions plugins/minimax-ai/minimax-bot/miniapp/client/strings-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,17 @@ export default {
'live.startHelper': '正在放到后台:{name}',
'live.listHelpers': '正在查看后台任务',
'live.stopHelper': '正在停止后台任务',
'helper.strip': '后台任务',
'helper.waitingForTasks': '{name} · 等待子任务:{title}',
'helper.strip': '后台任务',
'helper.running': '{name} 正在做「{title}」,你可以继续聊',
'helper.queued': '{name} 的任务在排队「{title}」',
'helper.justStarted': '刚开始',
'helper.minutes': '{n} 分钟',
'helper.stop': '停止',
'fold.show': '展开 {n} 条过程消息',
'fold.hide': '收起过程消息',
'fold.hint': '共 {n} 条过程消息,展开后在固定高度内滚动查看',
'event.helperStarted': '开始了:{title}',
'event.helper.done': '做完了:{title}',
'event.helper.blocked': '卡住了,需要你定一下:{title}',
Expand Down Expand Up @@ -458,13 +462,17 @@ export default {
'live.startHelper': 'Moving to the background: {name}',
'live.listHelpers': 'Checking background tasks',
'live.stopHelper': 'Stopping a background task',
'helper.strip': 'Background tasks',
'helper.waitingForTasks': '{name} · Awaiting dependencies: {title}',
'helper.strip': 'Background tasks',
'helper.running': '{name} is working on "{title}" — you can keep chatting',
'helper.queued': '{name} has this queued: "{title}"',
'helper.justStarted': 'just started',
'helper.minutes': '{n} min',
'helper.stop': 'Stop',
'fold.show': 'Show {n} earlier messages',
'fold.hide': 'Hide earlier messages',
'fold.hint': '{n} earlier messages, they scroll inside the opened area',
'event.helperStarted': 'Started: {title}',
'event.helper.done': 'Finished: {title}',
'event.helper.blocked': 'Stuck and needs your decision: {title}',
Expand Down
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ export function createMcpHandler({ team, engine }) {
result = {
protocolVersion: PROTOCOL_VERSIONS.includes(requested) ? requested : PROTOCOL_VERSIONS[0],
capabilities: { tools: { listChanged: false } },
serverInfo: { name: 'minimax-bot', version: '0.7.0' },
serverInfo: { name: 'minimax-bot', version: '0.7.5' },
instructions: t('mcp.instructions'),
};
break;
Expand Down
2 changes: 1 addition & 1 deletion plugins/minimax-ai/minimax-bot/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "minimax-bot",
"version": "0.7.4",
"version": "0.7.5",
"private": true,
"type": "module",
"mcode": {
Expand Down
Loading