fix(lens): show the first user message as the run input (#44958)

* fix(traces): use the first user message for the run input preview

* test(traces): cover first user message as the input preview

* test(traces): check fixture previews against the first user message

* fix(lens): show the whole input preview on one line in the runs table

* test(lens): cover multi-line input previews in the runs table
This commit is contained in:
ishaan-berri 2026-10-06 16:30:52 -07:00 • committed by GitHub
parent 9a3f000c9b
commit d8bc2b78e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 45 additions and 7 deletions

View file

@ -422,11 +422,10 @@ pub(super) fn event_payload(events: &[(bool, Value)], output: bool) -> Option<St
(!values.is_empty()).then(|| canonical(&encode(&values)))
}
/// The latest user message with text.
/// The first user message with text.
pub(super) fn preview(messages: &[Message]) -> String {
messages
.iter()
.rev()
.find(|message| message.role == "user" && !message.content.is_empty())
.map_or("", |message| message.content.as_str())
.chars()
@ -434,7 +433,7 @@ pub(super) fn preview(messages: &[Message]) -> String {
.collect()
}
/// The latest user message when `input` is a conversation, else the input itself.
/// The first user message when `input` is a conversation, else the input itself.
pub(super) fn input_preview(input: &str) -> String {
match serde_json::from_str::<Value>(input)
.ok()
@ -597,7 +596,7 @@ mod tests {
use serde_json::Value;
#[rstest]
#[case::latest_user(r#"{"messages":[{"role":"user","content":"first"},{"role":"assistant","content":"reply"},{"role":"user","content":"last"}]}"#, Some("last"))]
#[case::first_user(r#"{"messages":[{"role":"user","content":"first"},{"role":"assistant","content":"reply"},{"role":"user","content":"last"}]}"#, Some("first"))]
#[case::malformed("not-json", None)]
#[case::missing("{}", None)]
#[case::not_messages(r#"{"messages":[{"role":"user"}]}"#, None)]

View file

@ -502,6 +502,25 @@ fn existing_formats_win_over_new_formats(span: Span, #[case] kind: &str) {
assert_eq!(decoded.normalized.observation_type, ObservationType::Llm);
}
#[rstest]
fn input_preview_is_the_first_user_message(span: Span) {
let conversation = json!([
{"role": "system", "content": "sys"},
{"role": "user", "content": "initial question"},
{"role": "assistant", "content": "answer"},
{"role": "user", "content": "follow up"},
])
.to_string();
let decoded = decode(
span,
"custom",
&[("gen_ai.input.messages", &conversation)],
vec![],
)
.unwrap();
assert_eq!(decoded.normalized.input_preview, "initial question");
}
#[rstest]
#[case::messages(&[("gen_ai.input.messages", r#"[{"role":"user","content":"modern"}]"#)], "modern")]
#[case::indexed(&[("gen_ai.prompt.0.role", "user"), ("gen_ai.prompt.0.content", "indexed")], "indexed")]

View file

@ -82,7 +82,7 @@ fn assert_invariants(span: &DecodedSpan) {
}
assert!(normalized.input_preview.chars().count() <= 240);
if let Ok(Value::Array(messages)) = serde_json::from_str(&normalized.input) {
let user = messages.iter().rev().find_map(|message| {
let user = messages.iter().find_map(|message| {
(message.get("role")?.as_str()? == "user")
.then(|| {
message

View file

@ -165,6 +165,26 @@ describe("AgentTracesTable cost cell", () => {
});
});
describe("AgentTracesTable input cell", () => {
it("shows the whole preview on one line instead of only its first line", () => {
const template = (traceList as TracePage).data[0] as TraceSummary;
renderWithProviders(
inList(
<AgentTracesTable
traces={[{ ...template, input_preview: "CURRENT USER REQUEST:\n\n- add feedback to Lens" }]}
findings={new Map()}
isLoading={false}
error={null}
hasMore={false}
onLoadMore={vi.fn()}
onSetUpTracing={vi.fn()}
/>,
),
);
expect(screen.getByText("CURRENT USER REQUEST: - add feedback to Lens")).toBeInTheDocument();
});
});
describe("AgentTracesTable column picker", () => {
const runs = (traceList as TracePage).data as TraceSummary[];
const renderRuns = () =>

View file

@ -69,7 +69,7 @@ function CostCell({ run }: { run: TraceSummary }) {
);
}
const firstLine = (text: string): string => text.split("\n")[0] ?? text;
const singleLine = (text: string): string => text.replace(/\s+/g, " ").trim();
const runKey = (run: TraceSummary): string => run.trace_ref || run.trace_id;
const PREFETCH_MARGIN = "0px 0px 480px 0px";
@ -96,7 +96,7 @@ function InputCell({ run }: { run: TraceSummary }) {
return (
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-foreground">
{firstLine(previewText(run.input_preview)) || traceDisplayName(run)}
{singleLine(previewText(run.input_preview)) || traceDisplayName(run)}
</span>
{run.resolution_limited && (
<span