openvibers/openvibe.chat Commits

Vibe offers to watch and take over the agent's desk, once per task, in the person's Vibe chat (Actor's task record names the desk) (#204)

OP OpenVibers committed 18 days ago · 475e81403c · 2 files, +43 −1

Changes

ddiff --git a/server/rooms/workspace.js b/server/rooms/workspace.js
iindex 9d1d0bb..ae64c5c 100644
---- a/server/rooms/workspace.js
++++ b/server/rooms/workspace.js
@@@ -248,6 +248,8 @@ async function poll(deps = {}) {
if (state === 'running' && t.status !== 'running') { await setStatus(room, t.id, { status: 'running' }); changed(room, { id: Number(t.id), status: 'running' }); }
// The agent handed its person a step on its desk (Desks plan D4): Vibe asks them here, once per hand-off.
if (state === 'running' && task.needs_you && task.needs_you.url) await askPerson(room, t, task.needs_you, deps).catch(() => {});
++ // The agent works on a desk of its own (Desks plan): in a Vibe chat, Vibe offers to watch it and take it over, once.
++ if (state === 'running' && task.desk && task.desk.url && Number(room.assistant) === 1) await showDesk(room, t, task.desk, deps).catch(() => {});
if (!DONE.has(state)) continue;
const r = task.result || {};
const sources = (Array.isArray(r.sources) ? r.sources : []).map((s) => (typeof s === 'string' ? s : s && s.url)).filter((u) => /^https?:\/\//.test(String(u || ''))).slice(0, 12);
@@@ -283,6 +285,26 @@ async function askPerson(room, t, needs, deps = {}) {
return posted;
}
++// Tasks whose desk Vibe already offered (actor task), so a poll every few seconds offers it once.
++const shownDesks = new Set();
++
++/** Vibe offers the person the link to watch the agent's desk live and take it over (Desks plan: Watch, Take over). */
++async function showDesk(room, t, desk, deps = {}) {
++ const key = String(t.actor_task);
++ if (shownDesks.has(key) || !/^https:\/\//.test(String(desk.url || ''))) return null;
++ if (shownDesks.size > 2000) shownDesks.clear();
++ shownDesks.add(key);
++ const members = await db.all('SELECT * FROM room_ai_members WHERE room_id = ? AND enabled = 1 ORDER BY (handle = \'vibe\') DESC, id LIMIT 1', [room.id]);
++ const member = members[0];
++ if (!member) return null;
++ const what = desk.kind === 'browser' ? 'a browser of its own' : 'a desk of its own';
++ const text = `🖥️ Your agent is working on "${t.title}" in ${what}. Watch it live, and take it over whenever you like (it waits while you use it): ${oneLine(desk.url, LIMITS.url)}`;
++ const roomsMod = deps.rooms || rooms;
++ const posted = await roomsMod.postAsAi(room, member, text);
++ try { require('../chat/chat-server').broadcastToRoom(room.id, { type: 'room_message', room: room.slug, message: posted }); } catch { /* sockets are optional */ }
++ return posted;
++}
++
/** The room hears how a task ended, from its Vibe (or its first AI member), once. */
async function announce(room, t, deps = {}) {
if (!t || t.posted_message || !DONE.has(t.status)) { changed(room, t && shape(t, room)); return null; }
ddiff --git a/test/vibe-modes.test.js b/test/vibe-modes.test.js
iindex 8bf4bcc..2acf390 100644
---- a/test/vibe-modes.test.js
++++ b/test/vibe-modes.test.js
@@@ -17,7 +17,7 @@ const turns = [];
const actorCalls = [];
let actorTask = { id: 'tsk_r1', state: 'queued' };
const vibeApi = (method, path, u, body) => h.http(method, `/api/chat/vibe${path}`, { token: u && u.token, body });
--const messagesOf = async (slug) => ((await h.http('GET', `/api/chat/rooms/${slug}/messages`, { token: ann.token })).body.messages || []);
++const messagesOf = async (slug, as = ann) => ((await h.http('GET', `/api/chat/rooms/${slug}/messages`, { token: as.token })).body.messages || []);
const realFetch = globalThis.fetch;
t('boot', async () => {
@@@ -120,6 +120,26 @@ t('an agent that hands its person a step on its desk: Vibe asks them in the chat
await workspace.poll();
});
++t('an agent working on a desk: Vibe offers to watch it and take it over, once per task, with the link', async () => {
++ const start = await vibeApi('POST', '', bob, { message: 'hi' });
++ const slug = start.body.room.slug;
++ actorTask = { id: 'tsk_d1', state: 'queued' };
++ const r = await vibeApi('POST', `/${slug}/ask`, bob, { message: 'Compare three laptops on the store site', mode: 'agent' });
++ assert.strictEqual(r.status, 201, r.text);
++ await new Promise((res) => setTimeout(res, 150));
++ const desk = 'https://openvibe.bot/machines/dev_01M4W5JBFDG2TJ8C4FRDMCWBZ9/screen?desk=desk-9876543210';
++ actorTask = { id: 'tsk_d1', state: 'running' };
++ await workspace.poll();
++ const lines = async () => (await messagesOf(slug, bob)).filter((m) => m.ai_member_id && /Your agent is working on/.test(m.message)).map((m) => m.message);
++ assert.deepStrictEqual(await lines(), [], 'no desk yet: nothing offered');
++ actorTask = { id: 'tsk_d1', state: 'running', desk: { url: desk, kind: 'browser' } };
++ await workspace.poll();
++ await workspace.poll();
++ assert.deepStrictEqual(await lines(), [`🖥️ Your agent is working on "Compare three laptops on the store site" in a browser of its own. Watch it live, and take it over whenever you like (it waits while you use it): ${desk}`], 'once, however often the poller looks');
++ actorTask = { id: 'tsk_d1', state: 'cancelled' };
++ await workspace.poll();
++});
++
t('refusals: an unknown mode, someone else\'s chat, no sign-in', async () => {
const start = await vibeApi('POST', '', ann, { message: 'mine' });
const slug = start.body.room.slug;