fix(webui): sp11 item 3, Resume assistant shows only when the bus instances read says the thread's agent runs no process; a failed read or an unlisted agent keeps the control (#1873, lane sp11-fixes)
This commit is contained in:
1 parent
fb3bd16052
commit
d50d79cddd
2 files changed
+40
-4
No files matched your search
@@ -90,6 +90,16 @@
|
||||
signedOut = true; forget();
|
||||
document.dispatchEvent(new Event('mosaic-signed-out'));
|
||||
}
|
||||
// SP11: whether the thread's agent runs no process, from the bus instances read the Agents page uses.
|
||||
// true: paused or stopped; false: running; null: the read failed or does not list the agent.
|
||||
async function agentIdle(instance, signal) {
|
||||
try {
|
||||
const res = await fetch(pane ? `/api/orgs/${encodeURIComponent(env.org)}/bus/instances` : '/api/bus/instances', { cache: 'no-store', redirect: 'manual', signal });
|
||||
if (!res.ok || res.type === 'opaqueredirect') return null;
|
||||
const item = (await res.json())?.rows?.[0]?.instances?.[instance];
|
||||
return item && typeof item === 'object' && Array.isArray(item.running) ? item.running.length === 0 : null;
|
||||
} catch (err) { if (err.name === 'AbortError') throw err; return null; }
|
||||
}
|
||||
async function request(path, value, signal) {
|
||||
if (out()) throw new ChatError('signed-out');
|
||||
let res;
|
||||
@@ -317,8 +327,10 @@
|
||||
const messages = s.rows.map((m, i) => `<li class="${m.from_role === 'human' ? 'chat-message-human' : 'chat-message-agent'}" data-message="${esc(m.id)}"><div class="chat-message-meta"><span class="chat-avatar" aria-hidden="true">${txt(String(m.from_role || '?').slice(0, 1).toUpperCase())}</span><strong>${txt(m.from_role === 'human' ? 'You' : m.from_role)}</strong> ${stamp(m.at)}</div>${messageBody(m)}${!redesigned && withRequest.has(m.id) ? mount(m) : ''}</li>${redesigned ? (mountsAfter.get(i) || []).map(origin => `<li class="project-request-item">${mount(origin)}</li>`).join('') : ''}`).join('');
|
||||
const frame = pane ? '' : redesigned ? window.MosaicProjects.header(project, 'conversations', current.threads, t.name, true) : heading(t.name);
|
||||
// A pane leaves out the details: sessions and sharing are unavailable on an organization Console.
|
||||
const resume = threadWrites() ? `<button class="btn${pane ? ' pane-resume' : ''}" type="button" id="chat-resume"${disabledSession(s.pending || current.channelDisabled)}>Resume assistant</button>` : '';
|
||||
const details = pane ? `<p class="muted small">Reading never wakes the assistant. An accepted message is not an answer.</p>` : redesigned ? `<dialog class="chat-details project-dialog" id="project-conversation-dialog" aria-labelledby="project-conversation-title"><h2 id="project-conversation-title" tabindex="-1">Conversation details</h2><button class="btn" type="button" data-dialog-close>Close</button>${sessionControls(t)}${sharing(t)}<p>Reading never wakes the assistant. An accepted message is not an answer.</p>${threadWrites() ? `<button class="btn" type="button" id="chat-resume"${disabledSession(s.pending || current.channelDisabled)}>Resume assistant</button>` : ''}</dialog>` : `<details class="chat-details"><summary id="chat-details-toggle">Conversation details</summary>${sessionControls(t)}${sharing(t)}</details><p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p>${threadWrites() ? `<button class="btn" type="button" id="chat-resume"${disabledSession(s.pending || current.channelDisabled)}>Resume assistant</button>` : ''}`;
|
||||
// SP11: Resume shows unless the instances read says the agent is running.
|
||||
const resumable = threadWrites() && current.agentIdle !== false;
|
||||
const resume = resumable ? `<button class="btn${pane ? ' pane-resume' : ''}" type="button" id="chat-resume"${disabledSession(s.pending || current.channelDisabled)}>Resume assistant</button>` : '';
|
||||
const details = pane ? `<p class="muted small">Reading never wakes the assistant. An accepted message is not an answer.</p>` : redesigned ? `<dialog class="chat-details project-dialog" id="project-conversation-dialog" aria-labelledby="project-conversation-title"><h2 id="project-conversation-title" tabindex="-1">Conversation details</h2><button class="btn" type="button" data-dialog-close>Close</button>${sessionControls(t)}${sharing(t)}<p>Reading never wakes the assistant. An accepted message is not an answer.</p>${resumable ? `<button class="btn" type="button" id="chat-resume"${disabledSession(s.pending || current.channelDisabled)}>Resume assistant</button>` : ''}</dialog>` : `<details class="chat-details"><summary id="chat-details-toggle">Conversation details</summary>${sessionControls(t)}${sharing(t)}</details><p class="muted">Reading never wakes the assistant. An accepted message is not an answer.</p>${resumable ? `<button class="btn" type="button" id="chat-resume"${disabledSession(s.pending || current.channelDisabled)}>Resume assistant</button>` : ''}`;
|
||||
return `<section class="chat-conversation${redesigned ? ' project-chat' : ''}" data-project="${esc(t.project)}" data-thread="${esc(t.id)}" data-session="${esc(current.session)}">${pane ? '' : `<p>${link('projects', t.project, current.projects.find(p => p.id === t.project)?.name ?? 'Project')} / ${txt(t.instance)}</p>`}${frame}${libraryLine(libraryOf(current, t))}${details}${redesigned ? '<p class="small muted" data-conversation-request-status></p>' : ''}<ol class="chat-log" tabindex="0" aria-label="Thread conversation">${messages || (current.readFailed ? '<li class="muted">Messages unavailable.</li>' : '<li class="muted">No messages yet.</li>')}</ol><div class="chat-paging">${current.readFailed ? '<button type="button" class="btn" data-chat-retry>Read again</button>' : s.more ? '<p>Showing earlier history. Read forward to reach the current conversation.</p><button class="btn" type="button" id="chat-next">Read next messages</button>' : '<p class="muted">Caught up to the last read.</p>'}<button class="btn" type="button" id="chat-new"${s.follow ? ' hidden' : ''}>New messages — go to end</button></div>${threadWrites() ? `<form id="chat-send-form"><label for="chat-body">Message ${txt(t.instance)}</label><textarea id="chat-body" rows="4" placeholder="${redesigned ? `Message ${esc(t.instance)}` : 'Write a message…'}"${disabledSession(s.pending)}>${esc(s.draft)}</textarea><p class="chat-audience">To: ${redesigned ? `<span class="room-chip" aria-label="Recipient ${esc(t.instance)}">${txt(t.instance)}</span>` : `${txt(t.instance)} · Private conversation`}</p>${redesigned ? '<p class="project-composer-hint">Enter adds a new line.</p>' : ''}<button class="btn primary" type="submit"${disabledSession(s.pending || current.channelDisabled)}>Send</button></form>` : ''}<p id="chat-receipt" role="status">${txt(receipt)}</p>${pane ? resume : ''}${redesigned || pane ? '' : '<section class="chat-files"><h2>Files</h2><div data-chat-files>Files unavailable.</div></section><div data-chat-summary></div>'}</section>`;
|
||||
}
|
||||
// C2-10c: rooms reuse the same state, renderer and generation fences as threads.
|
||||
@@ -789,7 +801,7 @@
|
||||
paint(); return;
|
||||
}
|
||||
const old = current;
|
||||
current = { container, kind, id, orgArchived: old?.orgArchived ?? false, loading: true, archiveUnknown: kind === 'thread' && !user && !tester, settings: !!settings, section, path: originalPath, canSend, user, tester, chatGrants: [], thread: old && old.id === id ? old.thread : null, shared: old && old.id === id ? old.shared : false, projects: old?.projects ?? [], threads: old?.threads ?? [], repos: old?.repos ?? [], instances: old?.instances ?? [], instanceError: kind === 'project' ? 'Reading thread instances…' : '', error: '', readFailed: false, channelDisabled: old?.channelDisabled ?? false, refused: false, legacyDefault: old && old.id === id ? old.legacyDefault : false, sessions: old && old.id === id ? old.sessions ?? [] : [], session: match[3] ?? (old && old.id === id ? old.session : null), key: match[3] ? id + '/' + match[3] : old && old.id === id ? old.key : id };
|
||||
current = { container, kind, id, orgArchived: old?.orgArchived ?? false, loading: true, archiveUnknown: kind === 'thread' && !user && !tester, settings: !!settings, section, path: originalPath, canSend, user, tester, chatGrants: [], thread: old && old.id === id ? old.thread : null, shared: old && old.id === id ? old.shared : false, projects: old?.projects ?? [], threads: old?.threads ?? [], repos: old?.repos ?? [], instances: old?.instances ?? [], instanceError: kind === 'project' ? 'Reading thread instances…' : '', error: '', readFailed: false, channelDisabled: old?.channelDisabled ?? false, refused: false, legacyDefault: old && old.id === id ? old.legacyDefault : false, sessions: old && old.id === id ? old.sessions ?? [] : [], session: match[3] ?? (old && old.id === id ? old.session : null), key: match[3] ? id + '/' + match[3] : old && old.id === id ? old.key : id, agentIdle: old && old.id === id ? old.agentIdle : null };
|
||||
current.projectArchived = current.projects.some(p => p.id === (kind === 'project' ? id : current.thread?.project) && p.archived);
|
||||
if (kind === 'thread' && old?.kind === 'thread' && old.id === id && !navigated && old.archiveUnknown === false && old.projects.some(p => p.id === old.thread?.project)) current.archiveUnknown = false;
|
||||
if (current.projectArchived) current.archiveUnknown = false;
|
||||
@@ -873,6 +885,10 @@
|
||||
if (g !== generation || out()) return;
|
||||
current.shares = shares.rows;
|
||||
}
|
||||
if (kind === 'thread' && current.thread && threadWrites()) {
|
||||
current.agentIdle = await agentIdle(current.thread.instance, signal);
|
||||
if (g !== generation || out()) return;
|
||||
}
|
||||
let s = null, page = null, after = null;
|
||||
if (kind === 'thread' && catalog === null) {
|
||||
current.legacyDefault = true; current.sessions = []; current.session = null;
|
||||
|
||||
@@ -42,7 +42,7 @@ function orgs(state) {
|
||||
reads: { business: async () => ({ business: { id } }), settings: async () => ({}), busStatus: async () => ({ live: true, down: false }), queue: async () => ({ rows: [] }) },
|
||||
bus: {
|
||||
inbox: async () => [], agents: async () => [], tasks: async () => [], 'thread.claims': async () => [], messages: async () => ({ messages: [], unread: 0 }),
|
||||
instances: async () => ({ business: id, revision: 'a'.repeat(64), definitions: {}, instances: { assistant: { definition: 'assistant', vars: {} } } }),
|
||||
instances: async () => ({ business: id, revision: 'a'.repeat(64), definitions: {}, instances: { assistant: { definition: 'assistant', vars: {}, ...(state.run?.[id] ? { running: state.run[id] } : {}) } } }),
|
||||
},
|
||||
chat: {
|
||||
projects: async () => PROJECTS[id],
|
||||
@@ -304,6 +304,26 @@ test('SP7: a thread link on a project page opens the thread as a pane on the Ass
|
||||
assert.equal(await b.evaluate('location.hash'), `#/orgs/beta/projects/threads/${T.b1}`);
|
||||
});
|
||||
|
||||
// SP11 (#1873): Resume assistant shows only when the organization's instances read says the thread's
|
||||
// agent runs no process (paused or stopped). An agent the read does not say about keeps the control.
|
||||
test('Resume assistant shows only in a pane whose agent the instances read says is not running', async t => {
|
||||
const state = { down: new Set(), sent: [], created: [], run: { alpha: ['run-1'], beta: [] } };
|
||||
const { b, wait } = await open(t, { hash: `#/assistant?open=alpha:${T.a1},beta:${T.b1},home:${T.h1}`, state });
|
||||
const resume = sel => `!!${PANE(sel)}.querySelector('button.pane-resume')`;
|
||||
await wait(`${ready(`alpha:${T.a1}`)} && ${ready(`beta:${T.b1}`)} && ${ready(`home:${T.h1}`)}`);
|
||||
await wait(resume(`beta:${T.b1}`));
|
||||
assert.equal(await b.evaluate(resume(`alpha:${T.a1}`)), false, 'a running assistant offers no resume');
|
||||
assert.equal(await b.evaluate(resume(`home:${T.h1}`)), true, 'a read that does not say keeps the control');
|
||||
// Alpha's assistant stops: the next read shows the control.
|
||||
state.run.alpha = [];
|
||||
await b.evaluate('window.MosaicAssistant.refresh()');
|
||||
await wait(resume(`alpha:${T.a1}`));
|
||||
// Beta's starts again: its control goes.
|
||||
state.run.beta = ['run-2'];
|
||||
await b.evaluate('window.MosaicAssistant.refresh()');
|
||||
await wait(`!${resume(`beta:${T.b1}`)}`);
|
||||
});
|
||||
|
||||
test('SP7: with three open, a fourth thread replaces the first pane with no unsent message; when all three hold one, nothing is replaced and the page says why', async t => {
|
||||
const state = { down: new Set(), sent: [], created: [] };
|
||||
const { b, wait } = await open(t, { hash: `#/assistant?open=alpha:${T.a1},beta:${T.b1}`, state });
|
||||
|
||||
Reference in new issue
Block a user