@@ -30,6 +30,18 @@ interface PixelLabProps {
3030 * Sourced from the project summary endpoint, not from SSE.
3131 */
3232 liveTokens ?: number ;
33+ /**
34+ * Per-agent pool of recent artifact claims for the active project.
35+ * The lab pulls one of these (with low probability and trimmed) into
36+ * an idle bubble so the room reads as "talking about this project's
37+ * actual research", not the bundled placeholder lines. Empty when
38+ * the project has no artifacts yet — the lab falls back to the
39+ * generic IDLE_BUBBLE_LINES in that case.
40+ *
41+ * Keyed by the agent id whose `author` produced the artifact. Lab
42+ * code matches case-sensitively; callers should not normalize.
43+ */
44+ agentClaims ?: Record < string , string [ ] > ;
3345}
3446
3547// Compact 16:9.5 virtual canvas — feels like a single screen rather than a
@@ -219,7 +231,8 @@ export function PixelLab({
219231 selectedArtifactId,
220232 mode = "demo" ,
221233 phaseEvents,
222- liveTokens
234+ liveTokens,
235+ agentClaims
223236} : PixelLabProps ) {
224237 const hostRef = useRef < HTMLDivElement | null > ( null ) ;
225238 const floorPlanForUi = useMemo (
@@ -232,6 +245,11 @@ export function PixelLab({
232245 const eventStartedAtRef = useRef ( performance . now ( ) / 1000 ) ;
233246 const motionsRef = useRef < Record < string , AgentMotion > > ( { } ) ;
234247 const bubblesRef = useRef < SpeechBubble [ ] > ( [ ] ) ;
248+ // Mutable ref so the long-running Pixi tick loop sees the latest
249+ // claims without being torn down every time the artifact list
250+ // refreshes from the API.
251+ const agentClaimsRef = useRef < Record < string , string [ ] > > ( agentClaims ?? { } ) ;
252+ agentClaimsRef . current = agentClaims ?? { } ;
235253 const cardsRef = useRef < FlyingCard [ ] > ( [ ] ) ;
236254 const trayRef = useRef < TrayState > ( { count : 0 , recent : [ ] } ) ;
237255 const bannersRef = useRef < Banner [ ] > ( [ ] ) ;
@@ -470,7 +488,8 @@ export function PixelLab({
470488 spriteAtlas : spriteAtlasRef . current ,
471489 banners : bannersRef . current ,
472490 confetti : confettiRef . current ,
473- hud : hudRef . current
491+ hud : hudRef . current ,
492+ agentClaims : agentClaimsRef . current
474493 } ,
475494 now ,
476495 dt ,
@@ -591,6 +610,8 @@ function drawScene(
591610 banners : Banner [ ] ;
592611 confetti : Confetti [ ] ;
593612 hud : HudState ;
613+ /** Per-agent pool of recent artifact claims; see PixelLabProps. */
614+ agentClaims ?: Record < string , string [ ] > ;
594615 } ,
595616 seconds : number ,
596617 dt : number ,
@@ -651,7 +672,10 @@ function drawScene(
651672 state . mode === "demo" ||
652673 ( state . mode === "live" && noActiveRole ) ;
653674 if ( idleLike ) {
654- stepIdleAutonomy ( motions , agent , home , enabledSameFloorAgents , bubbles , seconds , dt ) ;
675+ stepIdleAutonomy (
676+ motions , agent , home , enabledSameFloorAgents , bubbles , seconds , dt ,
677+ state . agentClaims ,
678+ ) ;
655679 } else {
656680 stepAgentMotion ( motions , agent , home , index , state . activeEvent , seconds , progress , dt ) ;
657681 }
@@ -1400,14 +1424,48 @@ const IDLE_BUBBLE_LINES = [
14001424 "what day is it"
14011425] ;
14021426
1427+ const IDLE_BUBBLE_MAX_CHARS = 56 ;
1428+ /** Probability the idle bubble pulls a real artifact claim (when one
1429+ * is available) instead of a generic line. Tuned by feel: too high and
1430+ * the lab becomes a wall of project text with no breathing room; too
1431+ * low and the project context never shows. ~55% reads as "the lab is
1432+ * actually working on something specific". */
1433+ const IDLE_CLAIM_PROB = 0.55 ;
1434+
1435+ function pickIdleBubbleText (
1436+ agent : AgentConfig ,
1437+ agentClaims : Record < string , string [ ] > | undefined
1438+ ) : string {
1439+ // Try the active project's artifacts first: prefer claims authored
1440+ // by this agent itself, then any author. Fall back to the generic
1441+ // pool. We pick the agent's own claims first so the room reads as
1442+ // "Numerical is mumbling about its own measurement", not "Numerical
1443+ // is randomly quoting the Skeptic".
1444+ if ( agentClaims && Math . random ( ) < IDLE_CLAIM_PROB ) {
1445+ const own = agentClaims [ agent . id ] ;
1446+ if ( own && own . length > 0 ) {
1447+ return truncate ( own [ Math . floor ( Math . random ( ) * own . length ) ] , IDLE_BUBBLE_MAX_CHARS ) ;
1448+ }
1449+ // Pool of claims from any agent in this project. Fine to sample
1450+ // across — even a Skeptic mumbling another agent's claim reads
1451+ // as "actively engaged with the project".
1452+ const all = Object . values ( agentClaims ) . flat ( ) ;
1453+ if ( all . length > 0 ) {
1454+ return truncate ( all [ Math . floor ( Math . random ( ) * all . length ) ] , IDLE_BUBBLE_MAX_CHARS ) ;
1455+ }
1456+ }
1457+ return IDLE_BUBBLE_LINES [ Math . floor ( Math . random ( ) * IDLE_BUBBLE_LINES . length ) ] ;
1458+ }
1459+
14031460function stepIdleAutonomy (
14041461 motions : Record < string , AgentMotion > ,
14051462 agent : AgentConfig ,
14061463 home : AgentHome ,
14071464 enabledAgents : AgentConfig [ ] ,
14081465 bubbles : SpeechBubble [ ] ,
14091466 seconds : number ,
1410- dt : number
1467+ dt : number ,
1468+ agentClaims ?: Record < string , string [ ] >
14111469) {
14121470 let motion = motions [ agent . id ] as IdleMotion | undefined ;
14131471 if ( ! motion ) {
@@ -1424,7 +1482,7 @@ function stepIdleAutonomy(
14241482 if ( motion . idle . kind === "chat" || ( motion . idle . kind === "loiter" && Math . random ( ) < 0.25 ) ) {
14251483 bubbles . push ( {
14261484 agentId : agent . id ,
1427- text : IDLE_BUBBLE_LINES [ Math . floor ( Math . random ( ) * IDLE_BUBBLE_LINES . length ) ] ,
1485+ text : pickIdleBubbleText ( agent , agentClaims ) ,
14281486 startedAt : seconds ,
14291487 durationSec : 2.4
14301488 } ) ;
0 commit comments