diff --git a/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json b/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json index 1bbcabb..1fe4e75 100644 --- a/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json +++ b/plugins/minimax-ai/minimax-bot/.minimax-plugin/plugin.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "name": "minimax-bot", "displayName": "MiniMax Bot", - "version": "0.7.6", + "version": "0.7.7", "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", diff --git a/plugins/minimax-ai/minimax-bot/README.md b/plugins/minimax-ai/minimax-bot/README.md index 287d3f8..c287ee8 100644 --- a/plugins/minimax-ai/minimax-bot/README.md +++ b/plugins/minimax-ai/minimax-bot/README.md @@ -14,7 +14,7 @@ Copy this directory, including the hidden `.minimax-plugin/` directory, into the - **Chief Bot**: created on first launch to help you build your team. Chief can create, edit and delete Bots and groups; other Bots are refused when they try to do so. - **Group chats**: the group admin answers by default. `@Name` calls on a specific member, and a Bot that `@`-mentions another member in its reply hands the turn over. Members with nothing to add reply `[PASS]` and are skipped. Each member keeps a separate session per group, and groups cannot see private chats. There is no fixed reply cap. Discussions finish naturally, or wait for more instructions after repeated handoffs without tool progress. - **Hand-offs between Bots**: `message_bot` hands work to another Bot and immediately returns an admission receipt; the completed result returns to the source chat. `post_group` is **private chat only**: it posts to a group and returns immediately while the discussion continues according to the group's reply mode. From a private chat, Chief can post to any group without joining it; other Bots can only post to groups they belong to. During a group turn, `post_group` is disabled for everyone, including Chief and admins, for both same-group and cross-group posts. Use `@Name` in a reply to hand off within the group; start cross-group work from a private chat. -- **Background work**: when an isolated execution context is useful, a Bot calls `start_helper` to run it in the background in a fresh, separate session; the Bot's turn ends at once and you can keep chatting. The Bot simply says something like "let me look into it" without internal jargon or IDs. When the job finishes, its report comes back to the Bot as a new turn, and the Bot reviews it and passes it on in its own words (in groups too, where it can keep handing off with `@`). At most 2 background jobs run at once (one slot is always kept free for chat) and the rest queue; each Bot can have at most 4 queued or running. Running jobs are shown above the chat input and can be stopped at any time; when more than three are queued, the jobs that are already working stay in the open and the rest fold into one line that says how many are left, and one button below the rows opens the whole queue; tool approvals from a background job appear in its chat; jobs still running when the Mini App restarts are marked interrupted. Bots can also manage jobs with `list_helpers` / `stop_helper`. "Allow background helpers automatically" is on by default in Bot settings; when off, a Bot only does so when you explicitly ask. +- **Background work**: when an isolated execution context is useful, a Bot calls `start_helper` to run it in the background in a fresh, separate session; the Bot's turn ends at once and you can keep chatting. The Bot simply says something like "let me look into it" without internal jargon or IDs. When the job finishes, its report comes back to the Bot as a new turn, and the Bot reviews it and passes it on in its own words (in groups too, where it can keep handing off with `@`). At most 2 background jobs run at once (one slot is always kept free for chat) and the rest queue; each Bot can have at most 4 queued or running. Running jobs are shown above the chat input and can be stopped at any time; the strip orders them by how far along they are, the ones already working first, then the ones waiting for another job, then the queued ones; when more than three jobs are listed, it keeps the first three in the open and folds the rest into one line that says how many are left, and one button below the rows opens the whole queue; tool approvals from a background job appear in its chat; jobs still running when the Mini App restarts are marked interrupted. Bots can also manage jobs with `list_helpers` / `stop_helper`. "Allow background helpers automatically" is on by default in Bot settings; when off, a Bot only does so when you explicitly ask. - **Per-Bot model and reasoning effort**: pick a model and reasoning effort in Bot settings (only the levels the model supports are listed, and thinking can be turned off where supported; Bots that follow the default model can still set their own effort). Chief can also set them with the `model` (a model ID, `provider/model` or display name; `default` follows the default) and `effort` (`low`, `high`, `off`, ...) parameters of `create_bot` / `update_bot`. Requires a MiniMax Code version with `agent.models()` / `run({ model })`; older versions prompt you to upgrade. - **Images and files**: `` and `![](absolute path)` in a Bot reply render as images, audio/video players or file cards. Local HTML/HTM cards open an isolated preview inside the Mini App; MD/Markdown cards show a formatted reading view (plain text above 2 MiB). The owning Mini App fetches the file so a regular browser tab does not lose Host authentication and show a blank 401 page. Failed loads show an error and Retry; files above 16 MiB can be saved with the preview toolbar’s Download action. Other files still download. Local files are served through `/api/media`, limited to paths the Bot referenced in that conversation. HTML previews allow page scripts and HTTP(S) styles and display resources in an isolated sandbox, without access to Bot storage, fetch API requests or form submissions. Other local files referenced by HTML are not exposed; embed those resources in the document. Remote links follow the destination server response. - **Attachments**: attach files to your message; the Bot receives their local paths. diff --git a/plugins/minimax-ai/minimax-bot/miniapp/client/app.js b/plugins/minimax-ai/minimax-bot/miniapp/client/app.js index 75c4336..3551890 100644 --- a/plugins/minimax-ai/minimax-bot/miniapp/client/app.js +++ b/plugins/minimax-ai/minimax-bot/miniapp/client/app.js @@ -1237,15 +1237,24 @@ let helpersExpanded = false; /** A job that is already working comes first; one that still waits can wait a little longer. */ const HELPER_STATUS_ORDER = { running: 0, waiting_for_tasks: 1, queued: 2 }; +/** How far along a job is. A status the strip does not know yet comes last. */ +function helperRank(item) { + return HELPER_STATUS_ORDER[item.status] ?? Number.MAX_SAFE_INTEGER; +} + +/** When the job arrived: when it started, or when it was queued. */ +function helperArrival(item) { + return item.startedAt ?? item.createdAt ?? 0; +} + /** - * The rows that stay in the open. Which rows those are is our choice, the order they appear in - * is not: a job that is already working comes first, so the row the reader cares about is never - * the one that folds away, and the rows keep the order the official build gives them. + * The rows of the strip, ordered by how far along the job is: the ones already working first, + * the ones waiting for another job next, the queued ones last. The fold keeps the first rows, + * so the order decides which jobs stay in the open. Jobs of the same kind keep the order they + * arrived in. */ -function visibleHelperRows(items) { - const ranked = [...items].sort((a, b) => HELPER_STATUS_ORDER[a.status] - HELPER_STATUS_ORDER[b.status]); - const keep = new Set(ranked.slice(0, HELPER_ROWS_PREVIEW)); - return items.filter((item) => keep.has(item)); +function orderedHelperRows(items) { + return [...items].sort((a, b) => helperRank(a) - helperRank(b) || helperArrival(a) - helperArrival(b)); } /** Background helpers working for this chat: one line each, with a stop button. */ @@ -1261,9 +1270,10 @@ function renderHelpers() { // A queue that fits in the open rows has nothing to fold, so an open queue ends with it and // the next busy chat starts folded again. if (items.length <= HELPER_ROWS_PREVIEW) helpersExpanded = false; - const preview = helpersExpanded ? items : visibleHelperRows(items); - const hidden = items.length - preview.length; const foldable = items.length > HELPER_ROWS_PREVIEW; + const ordered = orderedHelperRows(items); + const preview = helpersExpanded ? ordered : ordered.slice(0, HELPER_ROWS_PREVIEW); + const hidden = items.length - preview.length; // The rows keep the scroll area they had, and the fold button sits below it, so the button // stays reachable however long the queue is. let rows = ui.helpers.querySelector('.helper-rows'); diff --git a/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs b/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs index 7b3e2da..b33cf4e 100644 --- a/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs +++ b/plugins/minimax-ai/minimax-bot/miniapp/node/mcp.mjs @@ -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.6' }, + serverInfo: { name: 'minimax-bot', version: '0.7.7' }, instructions: t('mcp.instructions'), }; break; diff --git a/plugins/minimax-ai/minimax-bot/package.json b/plugins/minimax-ai/minimax-bot/package.json index 205dca2..fc67f58 100644 --- a/plugins/minimax-ai/minimax-bot/package.json +++ b/plugins/minimax-ai/minimax-bot/package.json @@ -1,6 +1,6 @@ { "name": "minimax-bot", - "version": "0.7.6", + "version": "0.7.7", "private": true, "type": "module", "mcode": { diff --git a/plugins/minimax-ai/minimax-bot/tests/client-reliability.test.mjs b/plugins/minimax-ai/minimax-bot/tests/client-reliability.test.mjs index 907665e..4063caa 100644 --- a/plugins/minimax-ai/minimax-bot/tests/client-reliability.test.mjs +++ b/plugins/minimax-ai/minimax-bot/tests/client-reliability.test.mjs @@ -729,8 +729,8 @@ test('the background task strip keeps the official one line rows and folds a lon await settle(); const strip = document.querySelector('.helpers'); const rows = strip.querySelectorAll('.helper-row'); - // The rows are the ones the official build draws, in its order. The first three stay in the - // open, the rest fold into one line. + // The rows are the ones the official build draws. The fold keeps three of them in the open, + // the ones that are already working first, and the rest fold into one line. assert.equal(rows.length, 3); // A row is one line: who is doing what, then the elapsed time, then the stop button. const running = rows[0]; @@ -763,8 +763,9 @@ test('the background task strip keeps the official one line rows and folds a lon assert.equal(strip.querySelector('.helper-more'), button, 'the redraw reuses the button node'); assert.equal(document.activeElement, button, 'the redraw keeps the focus on it'); - // A job that is already working is never the one that folds away, even when the queue in - // front of it is long. + // The rows are ordered by how far along the job is, and the fold keeps the first three: a job + // that is already working is never the one that folds away, even when the queue in front of + // it is long. button.click(); await settle(); emit({ @@ -773,9 +774,35 @@ test('the background task strip keeps the official one line rows and folds a lon }); await settle(); const visible = [...strip.querySelectorAll('.helper-row')].map((row) => row.textContent); - assert.ok(visible.some((text) => /Answer the ticket/.test(text)), 'a running task stays in the open'); + assert.equal(visible.length, 3); + assert.match(visible[0], /Rebuild the truth table/, 'a running job comes first'); + assert.match(visible[1], /Answer the ticket/, 'the running task comes next'); + assert.match(visible[2], /Awaiting dependencies/, 'the waiting job comes after the running ones'); assert.ok(!visible.some((text) => /Queued task/.test(text)), 'a queued job gives way to the running one'); + // Two jobs and no fold: the strip still orders them by how far along they are, so a queued + // job never sits above a job that is already working. + emit({ type: 'tasks.changed', tasks: [] }); + emit({ type: 'helpers.changed', helpers: [queued[0], helpers[0]] }); + await settle(); + const plain = [...strip.querySelectorAll('.helper-row')].map((row) => row.textContent); + assert.equal(plain.length, 2); + assert.match(plain[0], /Rebuild the truth table/, 'the running job comes first even without a fold'); + assert.match(plain[1], /Queued task 1/); + + // Jobs of the same kind keep the order they arrived in, whichever list they came from. + emit({ + type: 'helpers.changed', + helpers: [ + { id: 'hlp_new', botId: 'bot_a', chatId: chat, title: 'Queued task 9', task: 'Queued task 9', status: 'queued', createdAt: Date.now() }, + { id: 'hlp_old', botId: 'bot_a', chatId: chat, title: 'Queued task 8', task: 'Queued task 8', status: 'queued', createdAt: Date.now() - 60000 }, + ], + }); + await settle(); + const sameKind = [...strip.querySelectorAll('.helper-row')].map((row) => row.textContent); + assert.match(sameKind[0], /Queued task 8/, 'the job that arrived first comes first'); + assert.match(sameKind[1], /Queued task 9/); + // The open queue ends with the queue: when it shrinks to three jobs the fold button goes, // and the next busy chat starts folded again. emit({ type: 'tasks.changed', tasks: [] });