Skip to content

Commit 997ab03

Browse files
committed
aa
1 parent 4a9bed9 commit 997ab03

14 files changed

Lines changed: 482 additions & 23 deletions

File tree

frontend/src/App.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "lucide-react";
1212
import { api } from "./api/client";
1313
import type { SettingsState } from "./api/client";
14-
import { useArchitecture, useProject, useProjects } from "./api/queries";
14+
import { useArchitecture, useArtifacts, useProject, useProjects } from "./api/queries";
1515
import { useLiveLabEvents } from "./api/sse";
1616
import { LabProjectPanel } from "./components/LabProjectPanel";
1717
import { PixelLab } from "./components/PixelLab";
@@ -88,6 +88,25 @@ export function App() {
8888
const liveLabProjectId =
8989
selectedProjectId && liveProject?.is_running ? selectedProjectId : null;
9090
const liveLab = useLiveLabEvents(liveLabProjectId, labArchitecture);
91+
// Fetch the active project's artifacts so PixelLab idle bubbles can
92+
// pull project-specific text instead of the generic placeholder
93+
// pool. Refreshes on a slow polling cadence (the underlying query
94+
// refetches when the iteration finishes via the queryKey invalidation
95+
// in useLiveLabEvents). Empty / unavailable projects produce an
96+
// empty map; PixelLab falls back to IDLE_BUBBLE_LINES.
97+
const { data: artifactsForLab } = useArtifacts(selectedProjectId);
98+
const labAgentClaims = (() => {
99+
if (!artifactsForLab) return undefined;
100+
const out: Record<string, string[]> = {};
101+
for (const a of artifactsForLab) {
102+
const claim = (a.claim || "").trim();
103+
if (!claim) continue;
104+
const author = a.author || "";
105+
if (!author) continue;
106+
(out[author] ??= []).push(claim);
107+
}
108+
return out;
109+
})();
91110
const labMode: "live" | "idle" | "demo" = selectedProjectId
92111
? liveProject?.is_running
93112
? "live"
@@ -266,6 +285,7 @@ export function App() {
266285
(liveProject.total_output_tokens ?? 0)
267286
: undefined
268287
}
288+
agentClaims={labAgentClaims}
269289
/>
270290
<div className="timeline">
271291
<LabStatusStrip

frontend/src/api/types.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,21 @@ export interface ArtifactView {
9696
author: string;
9797
kind: string;
9898
claim: string;
99-
confidence: number;
10099
assumptions: string[];
101100
evidence: string[];
102101
equations: string[];
103102
failure_modes: string[];
104103
next_steps: string[];
104+
/** Lifecycle state — replaces the old confidence float. Values:
105+
* provisional / accept / cite_only / defer / reject. See
106+
* src/halfseed/state/artifact_state.py for the rules. */
107+
state?: string;
108+
/** Critique severity ('blocker' | 'warning' | 'minor'); empty for
109+
* non-critique artifacts. */
110+
severity?: string;
111+
/** For critique-kind artifacts: id of the artifact this critique
112+
* targets. Empty for non-critique or paper-level critiques. */
113+
critique_target_id?: string;
105114
approval_status: ApprovalStatus;
106115
approval_note: string;
107116
code: string;
@@ -142,6 +151,9 @@ export interface StartIterationRequest {
142151
token_cap?: number;
143152
wall_clock_seconds?: number;
144153
model?: string | null;
154+
/** How many iterations to run back-to-back. Server caps at 20.
155+
* Default 1 = legacy single-iteration behaviour. */
156+
max_iterations?: number;
145157
}
146158

147159
export interface StartIterationResponse {

frontend/src/components/PixelLab.tsx

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
14031460
function 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
});

frontend/src/styles.css

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1804,6 +1804,33 @@ button {
18041804
font: inherit;
18051805
}
18061806

1807+
/* Multi-iteration progress strip. Renders during a run (showing
1808+
* "Iteration 2 of 5") and after a run completes (showing the stop
1809+
* reason once). Hidden when no run is active and target = 1.
1810+
* Sits between the runner options and the SSE event log. */
1811+
.runProgressStrip {
1812+
display: flex;
1813+
align-items: center;
1814+
gap: 12px;
1815+
padding: 6px 10px;
1816+
margin: 6px 0;
1817+
background: #f3eada;
1818+
border: 1px solid #c8b890;
1819+
border-radius: 4px;
1820+
font-size: 12px;
1821+
color: #4d4434;
1822+
}
1823+
.runProgressStrip code {
1824+
background: #fffaf0;
1825+
padding: 0 4px;
1826+
border-radius: 3px;
1827+
font-size: 11px;
1828+
}
1829+
.runProgressStrip .runProgressDone {
1830+
color: #1f513a;
1831+
font-weight: 600;
1832+
}
1833+
18071834
.runnerEvents {
18081835
flex: 1;
18091836
background: #2c2823;
@@ -2187,6 +2214,69 @@ a.iterationDiffToggle {
21872214
font-family: ui-monospace, monospace;
21882215
}
21892216

2217+
/* Lifecycle state badge — replaces the old confidence number. The
2218+
* five state values get distinct backgrounds so a reviewer can scan
2219+
* the artifact list and immediately see which artifacts are accepted
2220+
* vs. deferred vs. waiting in provisional. */
2221+
.artifactState {
2222+
padding: 2px 7px;
2223+
border-radius: 3px;
2224+
font-size: 10px;
2225+
font-weight: 700;
2226+
text-transform: uppercase;
2227+
letter-spacing: 0.04em;
2228+
border: 1px solid #c8b890;
2229+
background: #f3eada;
2230+
color: #6c604a;
2231+
}
2232+
.artifactState.state-accept {
2233+
background: #1f513a;
2234+
color: #fff;
2235+
border-color: #14392a;
2236+
}
2237+
.artifactState.state-cite_only {
2238+
background: #2a5279;
2239+
color: #fff;
2240+
border-color: #1c3955;
2241+
}
2242+
.artifactState.state-defer {
2243+
background: #d99032;
2244+
color: #fff;
2245+
border-color: #9c6724;
2246+
}
2247+
.artifactState.state-reject {
2248+
background: #6b3a36;
2249+
color: #fff;
2250+
border-color: #4a2825;
2251+
}
2252+
2253+
/* Critique severity badge: only renders on critique-kind artifacts
2254+
* once a severity is set. blocker / warning / minor map to red /
2255+
* orange / muted so a paper-quality scan is one glance. */
2256+
.artifactSeverity {
2257+
padding: 2px 7px;
2258+
border-radius: 3px;
2259+
font-size: 10px;
2260+
font-weight: 700;
2261+
text-transform: uppercase;
2262+
letter-spacing: 0.04em;
2263+
border: 1px solid #c8b890;
2264+
}
2265+
.artifactSeverity.severity-blocker {
2266+
background: #8b2a17;
2267+
color: #fff;
2268+
border-color: #5a1d10;
2269+
}
2270+
.artifactSeverity.severity-warning {
2271+
background: #d99032;
2272+
color: #fff;
2273+
border-color: #9c6724;
2274+
}
2275+
.artifactSeverity.severity-minor {
2276+
background: #f3eada;
2277+
color: #6c604a;
2278+
}
2279+
21902280
.approvalBadge {
21912281
margin-left: auto;
21922282
padding: 2px 8px;

frontend/src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ export interface ResearchArtifact {
2424
assumptions: string[];
2525
evidence: string[];
2626
equations: string[];
27-
confidence: number;
27+
/** Legacy: bundled sampleRun still carries this for the demo replay's
28+
* fake "stats". Real artifacts coming from the API don't have it
29+
* — see ArtifactView in api/types.ts and the state machine in
30+
* src/halfseed/state/artifact_state.py for the replacement. */
31+
confidence?: number;
2832
failure_modes: string[];
2933
next_steps: string[];
3034
}

frontend/src/utils/replay.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,16 @@ export function runStats(run: ResearchRun, architecture: LabArchitecture) {
7575
const riskCount =
7676
run.curated_report.open_risks.length +
7777
run.artifacts.reduce((total, artifact) => total + artifact.failure_modes.length, 0);
78+
// confidence is optional now (bundled sampleRun keeps it; real
79+
// artifacts use the structural state machine). Treat missing as 0
80+
// — the avgConfidence number is only used by the demo replay HUD.
7881
const avgConfidence =
7982
run.artifacts.length === 0
8083
? 0
81-
: run.artifacts.reduce((total, artifact) => total + artifact.confidence, 0) /
82-
run.artifacts.length;
84+
: run.artifacts.reduce(
85+
(total, artifact) => total + (artifact.confidence ?? 0),
86+
0,
87+
) / run.artifacts.length;
8388

8489
return {
8590
agents: architecture.agents.filter((agent) => agent.enabled).length,

frontend/src/views/ArtifactsView.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,19 @@ function ArtifactCard({
134134
(expanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />)}
135135
<span className="artifactAuthor">{artifact.author}</span>
136136
<span className="artifactKind">{artifact.kind}</span>
137-
<span className="artifactConfidence">conf {artifact.confidence.toFixed(2)}</span>
137+
{/* state replaces the old confidence float; severity is shown
138+
for critique-kind artifacts when the system has assigned one
139+
(blocker / warning / minor). See state/artifact_state.py. */}
140+
{artifact.state && (
141+
<span className={"artifactState state-" + artifact.state}>
142+
{artifact.state}
143+
</span>
144+
)}
145+
{artifact.severity && (
146+
<span className={"artifactSeverity severity-" + artifact.severity}>
147+
{artifact.severity}
148+
</span>
149+
)}
138150
<span className={"approvalBadge approval-" + artifact.approval_status}>
139151
{artifact.approval_status}
140152
</span>

0 commit comments

Comments
 (0)