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)
Changes
@@@ -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; }@@@ -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 chatawait 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;
@@ -248,6 +248,8 @@ async function poll(deps = {}) {
@@ -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' }); }
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.
// 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(() => {});
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;
if (!DONE.has(state)) continue;
const r = task.result || {};
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);
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 = {}) {
@@ -283,6 +285,26 @@ async function askPerson(room, t, needs, deps = {}) {
return posted;
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. */
/** The room hears how a task ended, from its Vibe (or its first AI member), once. */
async function announce(room, t, deps = {}) {
async function announce(room, t, deps = {}) {
if (!t || t.posted_message || !DONE.has(t.status)) { changed(room, t && shape(t, room)); return null; }
if (!t || t.posted_message || !DONE.has(t.status)) { changed(room, t && shape(t, room)); return null; }
@@ -17,7 +17,7 @@ const turns = [];
@@ -17,7 +17,7 @@ const turns = [];
const actorCalls = [];
const actorCalls = [];
let actorTask = { id: 'tsk_r1', state: 'queued' };
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 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;
const realFetch = globalThis.fetch;
t('boot', async () => {
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
@@ -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();
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 () => {
t('refusals: an unknown mode, someone else\'s chat, no sign-in', async () => {
const start = await vibeApi('POST', '', ann, { message: 'mine' });
const start = await vibeApi('POST', '', ann, { message: 'mine' });
const slug = start.body.room.slug;
const slug = start.body.room.slug;