Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/prime/src/prime_lab_app/agent_cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
73 changes: 71 additions & 2 deletions packages/prime/src/prime_lab_app/agent_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@
_ASSISTANT_STREAM_KEY = "__assistant_stream__"
_ASSISTANT_CHUNK_SEEN_KEY = "__assistant_chunk_seen__"
_ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
_INTERNAL_SKILL_LINE_PREFIXES = (
"using ",
"loading ",
"loaded ",
"applying ",
"activated ",
"selected ",
"running ",
"i'm using ",
"i'm loading ",
"i'll use ",
"i will use ",
"i\u2019m using ",
"i\u2019m loading ",
"i\u2019ll use ",
)


@dataclass(frozen=True)
Expand Down Expand Up @@ -732,7 +748,11 @@ def _handle_session_update(self, params: dict[str, Any]) -> None:
def _record_acp_tool_event(self, title: str, status: str, text: str) -> None:
title = title.strip()
status = status.strip()
text = text.strip()
text = _clean_agent_output_text(text.strip())
if _is_internal_skill_disclosure_line(title):
if not text:
return
title = "Agent update"
if not title and not text:
return
if text and _is_lab_widget_tool_result_text(text):
Expand Down Expand Up @@ -813,9 +833,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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return skips streaming assistant finalization

Low Severity

When _dynamic_tool_chat_content returns None (i.e., status == "tool"), _record_dynamic_tool_call now returns before reaching _finish_latest_streaming_assistant_locked(fallback=""). Previously, this finalization was always called, ensuring any active streaming assistant message was properly completed (or removed if empty) before appending a system message. Now, if there happens to be an active streaming assistant message when a "tool" status event arrives, it will remain in the "streaming" state until some other event finalizes it, potentially causing a lingering streaming indicator in the UI.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1924dd4. Configure here.

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:
Expand Down Expand Up @@ -1192,11 +1215,57 @@ def _clean_agent_output_text(text: str) -> str:
if not text:
return ""
cleaned = _ANSI_RE.sub("", text)
cleaned = _strip_internal_skill_disclosures(cleaned)
if cleaned != text and not cleaned.strip():
return ""
return cleaned


def _strip_internal_skill_disclosures(text: str) -> str:
lines = text.splitlines(keepends=True)
if not lines:
return "" if _is_internal_skill_disclosure_line(text) else text
return "".join(
raw_line for raw_line in lines if not _is_internal_skill_disclosure_line(raw_line)
)


def _is_internal_skill_disclosure_line(line: str) -> bool:
normalized = " ".join(line.strip().lower().split())
if "skill" not in normalized:
return False
return normalized.startswith(_INTERNAL_SKILL_LINE_PREFIXES)


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 _first_nonempty_line(content) or "Action ready"
return content


def _first_nonempty_line(text: str) -> str:
for line in text.splitlines():
stripped = line.strip()
if stripped:
return stripped
return ""


def _is_lab_widget_tool_result_text(text: str) -> bool:
try:
payload = json.loads(text)
Expand Down
3 changes: 3 additions & 0 deletions packages/prime/src/prime_lab_app/agent_widget_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions packages/prime/src/prime_lab_app/agent_widget_titles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion packages/prime/src/prime_lab_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 6 additions & 2 deletions packages/prime/src/prime_lab_app/chat_parts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion packages/prime/src/prime_lab_app/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -1292,7 +1292,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=(
Expand Down
2 changes: 1 addition & 1 deletion packages/prime/src/prime_lab_app/details.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 61 additions & 6 deletions packages/prime/src/prime_lab_app/setup_screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import re
from collections.abc import Callable
from pathlib import Path

Expand Down Expand Up @@ -185,7 +186,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"),
Expand Down Expand Up @@ -263,7 +264,10 @@ def _run_sync_worker(self, workspace: Path, agent: str) -> None:
self.app.call_from_thread(self._finish_sync, result)

def _append_sync_output(self, text: str) -> None:
self._output = (self._output + text)[-50000:]
visible_text = _user_visible_sync_output(text)
if not visible_text:
return
self._output = (self._output + visible_text)[-50000:]
self.query_one("#sync-output", Static).update(Text(self._output))

def _finish_sync(self, result: LabSyncResult) -> None:
Expand Down Expand Up @@ -435,7 +439,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)
Expand Down Expand Up @@ -469,8 +473,59 @@ def _doctor_result_table(result: LabDoctorResult) -> Table:
}.get(check.status, "dim")
table.add_row(
Text(check.status, style=status_style),
check.name,
check.message,
check.remediation,
_user_visible_lab_asset_text(check.name),
_user_visible_lab_asset_text(check.message),
_user_visible_lab_asset_text(check.remediation),
)
return table


def _user_visible_sync_output(text: str) -> str:
lines: list[str] = []
previous_visible_line = ""
for raw_line in text.splitlines(keepends=True):
line = raw_line.rstrip("\r\n")
ending = raw_line[len(line) :]
visible_line = _user_visible_sync_line(line)
if visible_line and visible_line != previous_visible_line:
lines.append(f"{visible_line}{ending}")
previous_visible_line = visible_line
return "".join(lines)


def _user_visible_sync_line(line: str) -> str:
normalized = line.strip().lower()
if "skill" not in normalized:
return line
if normalized.startswith("prepared "):
return "Prepared local Lab assets"
if normalized.startswith("skipped coding-agent"):
return "Skipped coding-agent local assets (--no-agent)"
if normalized.startswith("skipped ") and "user-owned" in normalized:
return "Skipped user-owned local Lab asset"
if normalized.startswith("warning: removed stale managed"):
return "Warning: removed stale managed Lab asset"
if normalized.startswith("sync failed:"):
return "Sync failed: Lab asset refresh failed"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve sync failure cause when redacting skill lines

When a sync error message contains skill (for example exceptions like Skill 'x' is missing SKILL.md. or duplicate-skill errors), this branch rewrites the full Sync failed: ... line to the generic Sync failed: Lab asset refresh failed, which removes the actionable failure reason from the TUI. run_lab_sync_service already emits the precise exception text, so this redaction makes real sync failures hard to diagnose and forces users to rerun outside the screen to understand what broke.

Useful? React with 👍 / 👎.

return ""


def _user_visible_lab_asset_text(text: str) -> str:
rendered = str(text)
rendered = re.sub(
r"Missing .*/\.prime/skills/\.prime-managed\.json",
"Missing local Lab asset cache",
rendered,
)
replacements = {
"Global Lab skill cache": "Global Lab asset cache",
"Workspace Lab skills": "Workspace Lab assets",
"managed skill(s)": "managed asset(s)",
"managed skill link(s)": "managed asset link(s)",
"No managed Lab skills are installed.": "No managed Lab assets are installed.",
" skills": " assets",
" skill": " asset",
}
for old, new in replacements.items():
rendered = rendered.replace(old, new)
return rendered
Loading
Loading