diff --git a/packages/prime/src/prime_cli/lab_setup.py b/packages/prime/src/prime_cli/lab_setup.py index 9d3a94450..deac1155c 100644 --- a/packages/prime/src/prime_cli/lab_setup.py +++ b/packages/prime/src/prime_cli/lab_setup.py @@ -15,7 +15,7 @@ from dataclasses import dataclass from importlib import metadata from pathlib import Path -from typing import Any +from typing import Any, Literal from urllib.error import HTTPError, URLError from urllib.request import urlopen @@ -67,6 +67,17 @@ _REPO_TREE_CACHE: dict[tuple[str, str, int], tuple[RepoTreeEntry, ...]] = {} Emit = Callable[[str], None] +LabSyncProgressKind = Literal[ + "started", + "lab_assets_prepared", + "agent_assets_prepared", + "agent_assets_skipped", + "templates_refreshed", + "guidance_refreshed", + "completed", + "failed", +] +LabSyncProgressEmit = Callable[["LabSyncProgressEvent"], None] Runner = Callable[[Sequence[str], Path, Emit], int] @@ -113,6 +124,14 @@ class LabSyncResult: workspace: Path +@dataclass(frozen=True) +class LabSyncProgressEvent: + """One typed Lab sync progress milestone for renderers.""" + + kind: LabSyncProgressKind + workspace: Path + + @dataclass(frozen=True) class LabDoctorOptions: """Options for checking a Lab workspace.""" @@ -316,14 +335,21 @@ def run_lab_sync_service( *, workspace: Path, emit: Emit | None = None, + emit_progress: LabSyncProgressEmit | None = None, ) -> LabSyncResult: """Refresh Lab skills and local agent guidance.""" workspace = workspace.expanduser().resolve() emit = emit or (lambda _text: None) try: - _run_lab_sync_steps(options, workspace=workspace, emit=emit) + _run_lab_sync_steps( + options, + workspace=workspace, + emit=emit, + emit_progress=emit_progress, + ) except Exception as exc: + _emit_sync_progress(emit_progress, "failed", workspace) emit(f"Sync failed: {exc}\n") return LabSyncResult(exit_code=1, workspace=workspace) return LabSyncResult(exit_code=0, workspace=workspace) @@ -381,11 +407,13 @@ def _run_lab_sync_steps( *, workspace: Path, emit: Emit, + emit_progress: LabSyncProgressEmit | None = None, ) -> None: workspace.mkdir(parents=True, exist_ok=True) (workspace / "configs").mkdir(exist_ok=True) (workspace / "environments").mkdir(exist_ok=True) emit(f"Syncing Lab assets in {workspace}\n") + _emit_sync_progress(emit_progress, "started", workspace) agents = _resolve_sync_agents(workspace, options.agents, no_agent=options.no_agent) guidance_agents = agents if not guidance_agents and options.no_agent: @@ -393,11 +421,13 @@ def _run_lab_sync_steps( managed_skill_names = _sync_prime_skills(emit) _prepare_workspace_skill_dir(workspace, managed_skill_names, emit) + _emit_sync_progress(emit_progress, "lab_assets_prepared", workspace) if agents: _prepare_agent_skill_dirs(workspace, agents, managed_skill_names, emit) _report_missing_agent_requirements(agents, emit) _prepare_agent_native_surfaces(workspace, agents, emit) _sync_lab_metadata(workspace, agents, setup_source="prime lab sync") + _emit_sync_progress(emit_progress, "agent_assets_prepared", workspace) else: reason = ( "--no-agent" @@ -405,13 +435,26 @@ def _run_lab_sync_steps( else "no configured agent; pass --agent to configure one" ) emit(f"Skipped coding-agent skill roots ({reason})\n") + _emit_sync_progress(emit_progress, "agent_assets_skipped", workspace) _sync_config_templates(workspace, emit) + _emit_sync_progress(emit_progress, "templates_refreshed", workspace) if not options.skip_docs: _sync_workspace_guidance(workspace, guidance_agents, emit, force=True) _write_lab_docs_index(workspace, guidance_agents) + _emit_sync_progress(emit_progress, "guidance_refreshed", workspace) emit("Lab sync completed\n") + _emit_sync_progress(emit_progress, "completed", workspace) + + +def _emit_sync_progress( + emit_progress: LabSyncProgressEmit | None, + kind: LabSyncProgressKind, + workspace: Path, +) -> None: + if emit_progress is not None: + emit_progress(LabSyncProgressEvent(kind=kind, workspace=workspace)) def _sync_workspace_guidance( @@ -854,14 +897,14 @@ def _managed_skill_manifest_check() -> LabDoctorCheck: skills = manifest.get("skills") if isinstance(skills, dict) and skills: return LabDoctorCheck( - name="Global Lab skill cache", + name="Global Lab asset cache", status="PASS", - message=f"{len(skills)} managed skill(s) installed.", + message=f"{len(skills)} managed asset(s) installed.", ) return LabDoctorCheck( - name="Global Lab skill cache", + name="Global Lab asset cache", status="WARN", - message=f"Missing {_global_prime_skills_dir() / PRIME_SKILLS_MANIFEST}", + message="Missing local Lab asset cache.", remediation="Run prime lab sync.", ) @@ -886,9 +929,9 @@ def _workspace_managed_skills_check(workspace: Path) -> LabDoctorCheck: skill_names = _managed_skill_names_from_manifest() if not skill_names: return LabDoctorCheck( - name="Workspace Lab skills", + name="Workspace Lab assets", status="WARN", - message="No managed Lab skills are installed.", + message="No managed Lab assets are installed.", remediation="Run prime lab sync.", ) skills_dir = workspace / WORKSPACE_SKILLS_DIR @@ -899,14 +942,14 @@ def _workspace_managed_skills_check(workspace: Path) -> LabDoctorCheck: ] if not missing: return LabDoctorCheck( - name="Workspace Lab skills", + name="Workspace Lab assets", status="PASS", - message=f"{len(skill_names)} managed skill link(s) are present.", + message=f"{len(skill_names)} managed asset link(s) are present.", ) return LabDoctorCheck( - name="Workspace Lab skills", + name="Workspace Lab assets", status="WARN", - message="Missing " + ", ".join(missing[:5]), + message="Missing managed Lab assets: " + ", ".join(missing[:5]), remediation="Run prime lab sync.", ) @@ -915,9 +958,9 @@ def _agent_managed_skills_check(label: str, agent_skill_dir: Path, agent: str) - skill_names = _managed_skill_names_from_manifest() if not skill_names: return LabDoctorCheck( - name=f"{label} skills", + name=f"{label} assets", status="WARN", - message="No managed Lab skills are installed.", + message="No managed Lab assets are installed.", remediation="Run prime lab sync.", ) missing = [ @@ -928,14 +971,14 @@ def _agent_managed_skills_check(label: str, agent_skill_dir: Path, agent: str) - ] if not missing: return LabDoctorCheck( - name=f"{label} skills", + name=f"{label} assets", status="PASS", - message=f"{len(skill_names)} managed skill link(s) are present.", + message=f"{len(skill_names)} managed asset link(s) are present.", ) return LabDoctorCheck( - name=f"{label} skills", + name=f"{label} assets", status="WARN", - message="Missing " + ", ".join(missing[:5]), + message="Missing managed Lab assets: " + ", ".join(missing[:5]), remediation=f"Run prime lab sync --agent {agent}.", ) @@ -1692,6 +1735,8 @@ def _print_lab_doctor_result(result: LabDoctorResult, console: Console) -> None: "LabSetupOptions", "LabSetupResult", "LabSyncOptions", + "LabSyncProgressEvent", + "LabSyncProgressKind", "LabSyncResult", "SUPPORTED_AGENTS", "parse_lab_doctor_args", diff --git a/packages/prime/src/prime_lab_app/agent_cards.py b/packages/prime/src/prime_lab_app/agent_cards.py index 297e39a35..c21958f4d 100644 --- a/packages/prime/src/prime_lab_app/agent_cards.py +++ b/packages/prime/src/prime_lab_app/agent_cards.py @@ -29,6 +29,7 @@ build_agent_widget_model, widget_payload, ) +from .agent_widget_titles import config_picker_summary from .config_screen import ConfigBuildResult from .launch_runner import ConfigLaunchRunner, extract_training_log_follow_command from .palette import PRIMARY, STATUS_ERROR, STATUS_WARNING, SUCCESS @@ -502,10 +503,14 @@ def _widget_card_heading(model: AgentWidgetModel) -> Group: def _widget_card_body(model: AgentWidgetModel) -> Group: payload = widget_payload(model.action) description = str(payload.get("description") or "").strip() + kind = str(payload.get("kind") or "").strip() + config_kind = str(payload.get("config_kind") or "").strip() table = Table.grid(padding=(0, 2)) table.add_column(style="bold dim", no_wrap=True) table.add_column() - if config_path := str(payload.get("config_path") or "").strip(): + if kind in {"config_editor", "run_launcher"} and config_kind: + table.add_row("Pickers", config_picker_summary(config_kind)) + elif config_path := str(payload.get("config_path") or "").strip(): table.add_row("Path", _short_path(config_path)) if description: table.add_row("Summary", description) diff --git a/packages/prime/src/prime_lab_app/agent_runtime.py b/packages/prime/src/prime_lab_app/agent_runtime.py index 00ff315af..4676a41e7 100644 --- a/packages/prime/src/prime_lab_app/agent_runtime.py +++ b/packages/prime/src/prime_lab_app/agent_runtime.py @@ -726,27 +726,27 @@ def _handle_session_update(self, params: dict[str, Any]) -> None: self._append_streaming_assistant_text(event.text) return if event.kind in {"tool_call", "tool_update"}: - self._record_acp_tool_event(event.title, event.status, event.text) + self._record_acp_tool_event(event.title, event.tool_kind, event.status, event.text) return - def _record_acp_tool_event(self, title: str, status: str, text: str) -> None: + def _record_acp_tool_event( + self, + title: str, + tool_kind: str, + status: str, + text: str, + ) -> None: title = title.strip() + tool_kind = tool_kind.strip() status = status.strip() - text = text.strip() - if not title and not text: + text = _clean_agent_output_text(text.strip()) + if not text: return if text and _is_lab_widget_tool_result_text(text): return label = title or "Tool" - content = label if not text else f"{label}\n{text}" + content = f"{label}\n{text}" with self._lock: - if ( - not text - and _is_lab_widget_tool_event_title(label) - and self._messages - and self._messages[-1].status == "widget" - ): - return if ( self._messages and self._messages[-1].role == "system" @@ -757,7 +757,7 @@ def _record_acp_tool_event(self, title: str, status: str, text: str) -> None: "system", content, "tool", - {"title": label, "tool_status": status}, + {"title": label, "tool_kind": tool_kind, "tool_status": status}, ) else: self._messages.append( @@ -765,7 +765,7 @@ def _record_acp_tool_event(self, title: str, status: str, text: str) -> None: "system", content, "tool", - {"title": label, "tool_status": status}, + {"title": label, "tool_kind": tool_kind, "tool_status": status}, ) ) self._emit_messages_locked() @@ -813,9 +813,12 @@ def _record_dynamic_tool_call( content: str, action: dict[str, Any] | None = None, ) -> None: + display_content = _dynamic_tool_chat_content(status, content, action) + if display_content is None: + return with self._lock: self._finish_latest_streaming_assistant_locked(fallback="") - self._messages.append(AgentChatMessage("system", content, status, action or {})) + self._messages.append(AgentChatMessage("system", display_content, status, action or {})) self._emit_messages_locked() def _record_codex_turn(self, result: dict[str, Any]) -> None: @@ -1191,10 +1194,28 @@ def _is_chunk_message(value: Any) -> bool: def _clean_agent_output_text(text: str) -> str: if not text: return "" - cleaned = _ANSI_RE.sub("", text) - if cleaned != text and not cleaned.strip(): - return "" - return cleaned + return _ANSI_RE.sub("", text) + + +def _dynamic_tool_chat_content( + status: str, + content: str, + action: dict[str, Any] | None, +) -> str | None: + if status == "tool": + return None + if status == "widget": + if isinstance(action, dict): + title = str(action.get("title") or "").strip() + if title: + return title + payload = action.get("payload") + if isinstance(payload, dict): + payload_title = str(payload.get("title") or "").strip() + if payload_title: + return payload_title + return "Action ready" + return content def _is_lab_widget_tool_result_text(text: str) -> bool: @@ -1233,11 +1254,6 @@ def _contains_lab_widget_tool_result(value: Any) -> bool: return False -def _is_lab_widget_tool_event_title(value: str) -> bool: - normalized = value.strip().lower().replace("-", "_") - return normalized.startswith(("prime_lab_", "mcp_prime_lab_")) - - def _merge_stream_text(existing: str, delta: str) -> str: """Append streamed text while ignoring duplicate final snapshots.""" diff --git a/packages/prime/src/prime_lab_app/agent_widget_model.py b/packages/prime/src/prime_lab_app/agent_widget_model.py index c53020e89..297c91991 100644 --- a/packages/prime/src/prime_lab_app/agent_widget_model.py +++ b/packages/prime/src/prime_lab_app/agent_widget_model.py @@ -327,6 +327,9 @@ def _widget_field_specs(context: dict[str, Any] | None) -> tuple[AgentWidgetFiel value = str(model_options[0][1]) widget = "select" options = model_options + if config_kind == "eval" and name == "envs" and value: + widget = "select" + options = ((value, value),) fields.append( AgentWidgetFieldSpec( name=name, diff --git a/packages/prime/src/prime_lab_app/agent_widget_titles.py b/packages/prime/src/prime_lab_app/agent_widget_titles.py index 89c1b1ba2..742634cae 100644 --- a/packages/prime/src/prime_lab_app/agent_widget_titles.py +++ b/packages/prime/src/prime_lab_app/agent_widget_titles.py @@ -10,3 +10,13 @@ def clean_widget_title(value: str) -> str: if lowered.startswith(prefix): return title[len(prefix) :].strip() or title return title + + +def config_picker_summary(config_kind: str) -> str: + if config_kind == "eval": + return "Environment, model, examples, rollouts, tokens, concurrency" + if config_kind == "rl": + return "Environment, model, steps, rollouts, batch, tokens" + if config_kind == "gepa": + return "Environment, model" + return "Environment, model" diff --git a/packages/prime/src/prime_lab_app/app.py b/packages/prime/src/prime_lab_app/app.py index 760957ccc..5a1ee4782 100644 --- a/packages/prime/src/prime_lab_app/app.py +++ b/packages/prime/src/prime_lab_app/app.py @@ -2169,7 +2169,7 @@ def _with_workspace_agent_choice(item: LabItem, workspace: Path, agent: str) -> key=item.key, section=item.section, title=item.title, - subtitle=f"{agent} · refresh templates, skills, docs, and local guidance", + subtitle=f"{agent} · refresh Lab assets and local guidance", status=item.status, status_style=item.status_style, metadata=_replace_metadata( diff --git a/packages/prime/src/prime_lab_app/chat_parts.py b/packages/prime/src/prime_lab_app/chat_parts.py index ce50d7581..f81d9b5e3 100644 --- a/packages/prime/src/prime_lab_app/chat_parts.py +++ b/packages/prime/src/prime_lab_app/chat_parts.py @@ -14,7 +14,7 @@ from rich.text import Text from .agent_runtime import AgentChatMessage, AgentConnectionState -from .agent_widget_titles import clean_widget_title +from .agent_widget_titles import clean_widget_title, config_picker_summary from .palette import PRIMARY, STATUS_ERROR, STATUS_SUCCESS, STATUS_WARNING, SUCCESS ReferenceKind = Literal["environment", "config", "run", "eval", "file"] @@ -161,11 +161,15 @@ def _render_widget_turn(message: AgentChatMessage) -> Panel: action = payload if isinstance(payload, dict) else message.metadata title = clean_widget_title(str(action.get("title") or "Action")) description = str(action.get("description") or "").strip() + kind = str(action.get("kind") or "").strip() + config_kind = str(action.get("config_kind") or "").strip() body = Table.grid(padding=(0, 2)) body.add_column(style="bold dim", no_wrap=True) body.add_column() - if config_path := str(action.get("config_path") or "").strip(): + if kind in {"config_editor", "run_launcher"} and config_kind: + body.add_row("Pickers", config_picker_summary(config_kind)) + elif config_path := str(action.get("config_path") or "").strip(): body.add_row("Path", config_path) if description: body.add_row("Summary", description) diff --git a/packages/prime/src/prime_lab_app/data.py b/packages/prime/src/prime_lab_app/data.py index 564c37a1d..40dfe33a1 100644 --- a/packages/prime/src/prime_lab_app/data.py +++ b/packages/prime/src/prime_lab_app/data.py @@ -1308,7 +1308,7 @@ def _workspace_agent_sync_item(workspace: Path, lab_metadata: dict[str, Any]) -> key=f"workspace:agent-sync:{_workspace_cache_key(workspace)}", section="workspace", title="Sync Lab assets", - subtitle=f"{agent} · refresh templates, skills, docs, and local guidance", + subtitle=f"{agent} · refresh Lab assets and local guidance", status="sync", status_style=STATUS_INFO, metadata=( diff --git a/packages/prime/src/prime_lab_app/details.py b/packages/prime/src/prime_lab_app/details.py index fab384130..ce32a05b3 100644 --- a/packages/prime/src/prime_lab_app/details.py +++ b/packages/prime/src/prime_lab_app/details.py @@ -331,7 +331,7 @@ def _workspace_detail_chunks(item: LabItem) -> list[Any]: [ Text(""), Text("Lab Asset Sync", style="bold dim"), - Text("Open this row to refresh templates, skills, docs, and agent guidance."), + Text("Open this row to refresh Lab assets and local guidance."), ] ) return chunks diff --git a/packages/prime/src/prime_lab_app/setup_screens.py b/packages/prime/src/prime_lab_app/setup_screens.py index 3c7b93396..55163757c 100644 --- a/packages/prime/src/prime_lab_app/setup_screens.py +++ b/packages/prime/src/prime_lab_app/setup_screens.py @@ -11,6 +11,7 @@ LabSetupOptions, LabSetupResult, LabSyncOptions, + LabSyncProgressEvent, LabSyncResult, run_lab_doctor_service, run_lab_setup_service, @@ -33,6 +34,16 @@ from .shell import lab_header SetupCompleteAction = Callable[[], None] +_SYNC_PROGRESS_TEXT = { + "started": "Syncing Lab assets", + "lab_assets_prepared": "Prepared local Lab assets", + "agent_assets_prepared": "Prepared agent surfaces", + "agent_assets_skipped": "Skipped agent surfaces", + "templates_refreshed": "Refreshed Lab templates", + "guidance_refreshed": "Updated local guidance", + "completed": "Lab sync completed", + "failed": "Lab sync failed", +} class SetupScreen(Screen[None]): @@ -185,7 +196,7 @@ def _setup_pressed(self, _event: Button.Pressed) -> None: class AgentSyncScreen(Screen[None]): - """Refresh Lab templates, skills, docs, and local agent guidance.""" + """Refresh Lab assets and local agent guidance.""" BINDINGS = [ Binding("escape", "back", "Back", key_display="Esc"), @@ -258,12 +269,18 @@ def _run_sync_worker(self, workspace: Path, agent: str) -> None: result = run_lab_sync_service( LabSyncOptions(agents=(agent,)), workspace=workspace, - emit=lambda text: self.app.call_from_thread(self._append_sync_output, text), + emit_progress=lambda event: self.app.call_from_thread( + self._append_sync_progress, + event, + ), ) self.app.call_from_thread(self._finish_sync, result) - def _append_sync_output(self, text: str) -> None: - self._output = (self._output + text)[-50000:] + def _append_sync_progress(self, event: LabSyncProgressEvent) -> None: + visible_text = _sync_progress_text(event) + if not visible_text or self._output.splitlines()[-1:] == [visible_text]: + return + self._output = (self._output + visible_text + "\n")[-50000:] self.query_one("#sync-output", Static).update(Text(self._output)) def _finish_sync(self, result: LabSyncResult) -> None: @@ -435,7 +452,7 @@ def _agent_sync_body(item: LabItem) -> Group: table.add_row("Agent", str(item.raw.get("agent") or "codex")) table.add_row("Command", str(item.raw.get("command") or "prime lab sync")) note = Text( - "Refresh Prime-owned templates, skills, docs, and agent guidance for this workspace.", + "Refresh Prime-owned Lab assets and local guidance for this workspace.", style="dim", ) return Group(Text("Lab asset sync", style="bold"), table, Text(""), note) @@ -474,3 +491,7 @@ def _doctor_result_table(result: LabDoctorResult) -> Table: check.remediation, ) return table + + +def _sync_progress_text(event: LabSyncProgressEvent) -> str: + return _SYNC_PROGRESS_TEXT[event.kind] diff --git a/packages/prime/tests/test_lab_view.py b/packages/prime/tests/test_lab_view.py index 44b54daf8..3b577e5aa 100644 --- a/packages/prime/tests/test_lab_view.py +++ b/packages/prime/tests/test_lab_view.py @@ -25,10 +25,14 @@ from prime_cli.commands.rl import RLConfig as HostedRLConfig from prime_cli.lab_mcp import _serve_lab_mcp_stdio, lab_mcp_tool_definitions from prime_cli.lab_setup import ( + LabDoctorCheck, LabDoctorOptions, + LabDoctorResult, LabSetupOptions, LabSetupResult, LabSyncOptions, + LabSyncProgressEvent, + LabSyncProgressKind, parse_lab_doctor_args, parse_lab_setup_args, parse_lab_sync_args, @@ -164,7 +168,15 @@ from prime_lab_app.readme import readme_links as _readme_links from prime_lab_app.readme import readme_markdown as _readme_markdown from prime_lab_app.rows import item_badges_text -from prime_lab_app.setup_screens import AgentSyncScreen, DoctorScreen, SetupScreen, _setup_body +from prime_lab_app.setup_screens import ( + AgentSyncScreen, + DoctorScreen, + SetupScreen, + _agent_sync_body, + _doctor_result_table, + _setup_body, + _sync_progress_text, +) from prime_lab_app.shell import ( compact_path, configured_workspace_agent, @@ -2186,6 +2198,7 @@ def fake_download(_url: str, dest: Path, _emit: Any, *, force: bool = False) -> lambda command: "/bin/pi" if command == "pi" else None, ) emitted: list[str] = [] + progress: list[LabSyncProgressEvent] = [] assert parse_lab_sync_args(["--agent", "pi"]).agents == ("pi",) assert parse_lab_sync_args([]).agents == () @@ -2194,9 +2207,18 @@ def fake_download(_url: str, dest: Path, _emit: Any, *, force: bool = False) -> LabSyncOptions(agents=("pi",)), workspace=tmp_path, emit=emitted.append, + emit_progress=progress.append, ) assert result.exit_code == 0 + assert [event.kind for event in progress] == [ + "started", + "lab_assets_prepared", + "agent_assets_prepared", + "templates_refreshed", + "guidance_refreshed", + "completed", + ] assert (tmp_path / ".prime" / "skills" / "create-environments" / "SKILL.md").is_file() assert (tmp_path / ".pi" / "skills" / "create-environments").exists() assert not any("pi-acp" in line for line in emitted) @@ -2206,6 +2228,79 @@ def fake_download(_url: str, dest: Path, _emit: Any, *, force: bool = False) -> assert downloads +def test_agent_sync_screen_renders_asset_copy(tmp_path: Path) -> None: + item = LabItem( + key="workspace:sync", + section="workspace", + title="Sync Lab assets", + subtitle="codex", + status="sync", + status_style="", + metadata=(), + raw={ + "type": "agent_sync", + "workspace": str(tmp_path), + "agent": "codex", + "command": "prime lab sync --agent codex", + }, + ) + + rendered = _render_renderable(_agent_sync_body(item)) + + assert "skill" not in rendered.lower() + assert "Lab assets" in rendered + + +def test_agent_sync_progress_renders_typed_events(tmp_path: Path) -> None: + kinds: tuple[LabSyncProgressKind, ...] = ( + "started", + "lab_assets_prepared", + "agent_assets_prepared", + "templates_refreshed", + "guidance_refreshed", + "completed", + ) + rendered = [ + _sync_progress_text(LabSyncProgressEvent(kind=kind, workspace=tmp_path)) for kind in kinds + ] + + assert rendered == [ + "Syncing Lab assets", + "Prepared local Lab assets", + "Prepared agent surfaces", + "Refreshed Lab templates", + "Updated local guidance", + "Lab sync completed", + ] + + +def test_doctor_screen_renders_source_check_copy(tmp_path: Path) -> None: + result = LabDoctorResult( + exit_code=1, + workspace=tmp_path, + checks=( + LabDoctorCheck( + name="Global Lab asset cache", + status="WARN", + message="Missing local Lab asset cache.", + remediation="Run prime lab sync.", + ), + LabDoctorCheck( + name="Codex assets", + status="PASS", + message="1 managed asset link(s) are present.", + ), + ), + ) + + rendered = _render_renderable(_doctor_result_table(result)) + + assert "skill" not in rendered.lower() + assert "Global Lab asset cache" in rendered + assert "Missing local Lab asset cache" in rendered + assert "Codex assets" in rendered + + def test_prime_lab_sync_service_preserves_workspace_agent( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, @@ -2634,32 +2729,25 @@ def test_agent_runtime_filters_wrapped_lab_widget_tool_results() -> None: assert _is_lab_widget_tool_result_text(wrapped) is True -def test_agent_runtime_suppresses_empty_lab_tool_update_after_widget() -> None: - messages: tuple[Any, ...] = () +def test_agent_runtime_ignores_empty_acp_tool_event() -> None: + runtime = AgentRuntime() - def on_messages(value: Any) -> None: - nonlocal messages - messages = value + runtime._record_acp_tool_event("Lab choose", "mcp", "completed", "") + + assert runtime.messages() == () - runtime = AgentRuntime(on_messages=on_messages) - runtime._messages = [ - AgentChatMessage( - "system", - "Lab tool diagnostic", - "widget", - {"tool": "choose", "kind": "choice_picker"}, - ) - ] - runtime._record_acp_tool_event("prime_lab_choose", "completed", "") +def test_agent_runtime_records_text_acp_tool_event() -> None: + runtime = AgentRuntime() + + runtime._record_acp_tool_event("Read config", "read", "completed", "ok") - assert len(messages) == 0 assert runtime.messages() == ( AgentChatMessage( "system", - "Lab tool diagnostic", - "widget", - {"tool": "choose", "kind": "choice_picker"}, + "Read config\nok", + "tool", + {"title": "Read config", "tool_kind": "read", "tool_status": "completed"}, ), ) @@ -3884,7 +3972,9 @@ def on_messages(value: Any) -> None: assert tool_responses[-1]["success"] is True assert _wait_for(lambda: messages and messages[-1].status == "widget") latest_message = _last_message(messages) - assert "Pick a config" in latest_message.content + assert latest_message.content == "Pick a config" + assert "widget-1" not in latest_message.content + assert "Candidates" not in latest_message.content assert actions[-1]["type"] == "widget_requested" assert actions[-1]["tool"] == "choose" assert actions[-1]["kind"] == "choice_picker" @@ -3892,6 +3982,14 @@ def on_messages(value: Any) -> None: runtime.stop() +def test_agent_runtime_hides_codex_non_widget_tool_chatter() -> None: + runtime = AgentRuntime() + + runtime._record_dynamic_tool_call("tool", "Environment search\n2 result(s)") + + assert runtime.messages() == () + + def test_agent_chat_transcript_renders_assistant_markdown() -> None: transcript = _chat_transcript( ( @@ -3952,7 +4050,9 @@ def test_agent_chat_transcript_renders_lab_widget_card() -> None: assert "alphabet-sort" in rendered assert "Eval: alphabet-sort" not in rendered assert "Run launcher" not in rendered - assert "configs/eval/alphabet-sort.toml" in rendered + assert "configs/eval/alphabet-sort.toml" not in rendered + assert "Pickers" in rendered + assert "Environment, model" in rendered def test_agent_chat_parts_parse_lab_references() -> None: @@ -4131,6 +4231,21 @@ def test_agent_stream_delta_ignores_lab_widget_ack_payload() -> None: assert text == "" +def test_agent_stream_delta_preserves_assistant_content() -> None: + text = _extract_stream_delta( + { + "type": "assistant", + "message": { + "role": "assistant", + "content": "Using skill create-environments for local setup.\nReady.", + }, + }, + {}, + ) + + assert text == "Using skill create-environments for local setup.\nReady." + + def test_agent_stream_delta_ignores_final_snapshot_after_streamed_chunks() -> None: seen_messages: dict[str, str] = {} @@ -5032,13 +5147,13 @@ async def test_prime_lab_app_chat_mounts_lab_widget_cards(tmp_path: Path) -> Non input_values = { input_widget.name: input_widget.value for input_widget in cards[0].query(ClearableInput) } - assert input_values["envs"] == "primeintellect/alphabet-sort" + select_values = {select.name: select.value for select in cards[0].query(Select)} + assert select_values["envs"] == "primeintellect/alphabet-sort" assert input_values["num_examples"] == "50" assert input_values["rollouts_per_example"] == "3" assert "max_concurrent" in input_values assert input_values["max_concurrent"] == "auto" assert input_values["model"] == "" - assert not list(cards[0].query(Select)) button_labels = {str(button.label) for button in app.screen.query(Button)} assert "Launch" in button_labels assert "Stop" in button_labels @@ -5806,8 +5921,8 @@ async def test_prime_lab_app_chat_widget_prefills_local_env_and_endpoint_model( input_widget.name: input_widget.value for input_widget in card.query(ClearableInput) } selects = {select.name: select.value for select in card.query(Select)} - assert fields["envs"] == "reverse-text" - assert "envs" not in selects + assert "envs" not in fields + assert selects["envs"] == "reverse-text" assert selects["model"] == "local/reverse-model" @@ -5875,13 +5990,13 @@ async def test_prime_lab_app_chat_widget_prefills_existing_eval_config( fields = { input_widget.name: input_widget.value for input_widget in card.query(ClearableInput) } - model_select = card.query_one(Select) - assert fields["envs"] == "reverse-text" + selects = {select.name: select.value for select in card.query(Select)} + assert selects["envs"] == "reverse-text" assert fields["num_examples"] == "12" assert fields["rollouts_per_example"] == "5" assert fields["max_tokens"] == "2048" assert fields["max_concurrent"] == "3" - assert model_select.value == "existing/model" + assert selects["model"] == "existing/model" @pytest.mark.asyncio