mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] canvas: a collapsed card shows the newest ShowUI a running skill emits; staleness is now work-after-the-widget, not its name (ENG-272)
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// Run: node --test (via frontend/scripts/run-tests.mjs)
|
||||
//
|
||||
// ENG-272. The collapsed pill hid a plan/progress widget whenever the session was running, on the
|
||||
// assumption that a mid-turn plan is stale. That is true when work followed it and false when the
|
||||
// widget IS the newest thing the agent said, which is exactly what a long-running skill does when it
|
||||
// re-emits its tracker. Result: the minimized view showed older state all day. Staleness is now
|
||||
// "did real work land after this widget", which these pin.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { hasWorkAfterLatestShowUi } from './showUiPayload.ts';
|
||||
|
||||
const showUi = (id: string) => ({ role: 'tool_call', content: { id, tool: 'mcp__openswarm-core__ShowUI', input: { component: 'plan', props: {} } } });
|
||||
const bash = (id: string) => ({ role: 'tool_call', content: { id, tool: 'Bash', input: { command: 'ls' } } });
|
||||
const say = (text: string) => ({ role: 'assistant', content: text });
|
||||
const user = (text: string) => ({ role: 'user', content: text });
|
||||
|
||||
test('a widget that is the newest thing is NOT stale', () => {
|
||||
assert.equal(hasWorkAfterLatestShowUi([user('go'), bash('b1'), showUi('u1')]), false);
|
||||
});
|
||||
|
||||
test('a re-emitted widget is not stale either, which is the whole bug', () => {
|
||||
// The send-text case: work, widget, more work, widget again. The newest widget stands.
|
||||
assert.equal(hasWorkAfterLatestShowUi([showUi('u1'), bash('b1'), say('sent the batch'), showUi('u2')]), false);
|
||||
});
|
||||
|
||||
test('a tool call after the widget DOES make it stale', () => {
|
||||
assert.equal(hasWorkAfterLatestShowUi([showUi('u1'), bash('b1')]), true);
|
||||
});
|
||||
|
||||
test('a spoken answer after the widget makes it stale', () => {
|
||||
assert.equal(hasWorkAfterLatestShowUi([showUi('u1'), say('all done')]), true);
|
||||
});
|
||||
|
||||
test('an empty assistant message is not work', () => {
|
||||
assert.equal(hasWorkAfterLatestShowUi([showUi('u1'), say(' ')]), false);
|
||||
});
|
||||
|
||||
test('a user message after the widget is not agent work', () => {
|
||||
// The user typing does not make the agent's own widget stale; the agent has not acted yet.
|
||||
assert.equal(hasWorkAfterLatestShowUi([showUi('u1'), user('any update?')]), false);
|
||||
});
|
||||
|
||||
test('no widget at all reports no work-after', () => {
|
||||
assert.equal(hasWorkAfterLatestShowUi([user('go'), bash('b1')]), true);
|
||||
assert.equal(hasWorkAfterLatestShowUi([]), false);
|
||||
});
|
||||
@@ -125,6 +125,26 @@ export function extractPendingAskUi(messages: Array<{ id: string; role: string;
|
||||
return null;
|
||||
}
|
||||
|
||||
/** True when real agent work landed AFTER the newest ShowUI, which is what makes that widget stale.
|
||||
*
|
||||
* A tool call or a spoken answer after the widget means the plan it drew has moved on. A widget that
|
||||
* is still the last thing in the transcript is the agent's CURRENT statement, however it is named,
|
||||
* and a long-running skill re-emitting its tracker lands exactly there (ENG-272).
|
||||
*/
|
||||
export function hasWorkAfterLatestShowUi(messages: Array<{ role: string; content: unknown }>): boolean {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg.role === 'tool_call') {
|
||||
const body = (typeof msg.content === 'object' && msg.content !== null ? msg.content : {}) as { tool?: unknown };
|
||||
// The ShowUI itself ends the scan: anything below it is older than the widget.
|
||||
if (/(^|__)ShowUI$/.test(String(body.tool || ''))) return false;
|
||||
return true;
|
||||
}
|
||||
if (msg.role === 'assistant' && typeof msg.content === 'string' && msg.content.trim()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function extractLatestShowUi(messages: Array<{ role: string; content: unknown }>): ShowUiPayload | null {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const msg = messages[i];
|
||||
|
||||
@@ -43,7 +43,7 @@ import { openCardContextMenu, isNativeMenuTarget } from '../desktop/openCardCont
|
||||
import { agentCardMenuRows } from './agentCardMenuRows';
|
||||
import { extractLatestTodos } from '../desktop/agentTodos';
|
||||
import { extractLiveSteps } from '../desktop/agentLiveSteps';
|
||||
import { extractLatestShowUi, extractPendingAskUi, freezeIfDone, artifactName } from '@/app/pages/AgentChat/tool-ui/showUiPayload';
|
||||
import { extractLatestShowUi, extractPendingAskUi, freezeIfDone, artifactName, hasWorkAfterLatestShowUi } from '@/app/pages/AgentChat/tool-ui/showUiPayload';
|
||||
import { useDragEndBackstops } from '../hooks/interaction/useDragEndBackstops';
|
||||
import { useBrowserPillShot } from '../desktop/useBrowserPillShot';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
@@ -724,11 +724,18 @@ const AgentCard: React.FC<Props> = ({
|
||||
[session.messages, session.status],
|
||||
);
|
||||
const pillArtifact = useMemo(() => {
|
||||
const artifact = extractLatestShowUi(session.messages || []);
|
||||
const msgs = session.messages || [];
|
||||
const artifact = extractLatestShowUi(msgs);
|
||||
if (!artifact) return null;
|
||||
// A plan/progress widget posted mid-turn goes stale the moment work continues; while running the
|
||||
// pill prefers living surfaces (browser shot, live steps, todos). Answer widgets still win.
|
||||
if (session.status === 'running' && /plan|progress/i.test(artifactName(artifact))) return null;
|
||||
// A plan/progress widget posted mid-turn goes stale the moment work continues, so while running
|
||||
// the pill prefers living surfaces (browser shot, live steps, todos). But "stale" means NOTHING
|
||||
// HAPPENED SINCE, not "it is named plan": a skill whose whole job is a live-updating tracker
|
||||
// re-emits it as the newest thing in the transcript, and hiding that left the collapsed view
|
||||
// showing older state all day (ENG-272). So only suppress it when real work followed it.
|
||||
if (session.status === 'running' && /plan|progress/i.test(artifactName(artifact))
|
||||
&& hasWorkAfterLatestShowUi(msgs)) {
|
||||
return null;
|
||||
}
|
||||
return freezeIfDone(artifact, session.status === 'running');
|
||||
}, [session.messages, session.status]);
|
||||
const pillAskPair = useMemo(
|
||||
|
||||
Reference in New Issue
Block a user