Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions packages/chat/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ export {
createDrizzleRoomMessageStore,
createInMemoryRoomMessageStore,
postRoomMessage,
previewOf,
} from "./room-messages";
export type {
RoomMessage,
Expand Down
78 changes: 78 additions & 0 deletions packages/chat/src/room-messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,84 @@ describe("listActivity", () => {
expect(activity[WORKBENCH]?.preview).not.toMatch(/model key/i);
expect(activity[WORKBENCH]?.preview).not.toMatch(/ref /i);
});

// CL-6795: bench-list preview settles on the latest person-facing text —
// never a blank title-only row while readable messages exist, and never a
// stale greeting preferred over a newer human message.
test("a newer user message previews over a stale agent greeting", async () => {
const roomMessages = createInMemoryRoomMessageStore();
const publisher = recordingPublisher();
await postRoomMessage(
{ roomMessages, publish: publisher.publish },
{
tenantId: TENANT,
workbenchId: WORKBENCH,
sender: { name: "Myra", address: "run_myra@acme.example" },
runId: "run_myra",
parts: [
{ kind: "text", text: "Hi — I'm Myra. What are we working on?" },
],
},
);
await Bun.sleep(2);
const user = await postRoomMessage(
{ roomMessages, publish: publisher.publish },
{
tenantId: TENANT,
workbenchId: WORKBENCH,
sender: { name: null, address: "prn_ada@acme.example" },
parts: [{ kind: "text", text: "draft the agenda for Monday" }],
},
);

const activity = await roomMessages.listActivity({
tenantId: TENANT,
workbenches: [{ workbenchId: WORKBENCH }],
});

expect(activity[WORKBENCH]?.lastActivityAt).toBe(user.createdAt);
expect(activity[WORKBENCH]?.preview).toBe("draft the agenda for Monday");
expect(activity[WORKBENCH]?.preview).not.toMatch(/I'm Myra/i);
});

test("a join notice after readable text keeps the prior preview, never blanks", async () => {
const roomMessages = createInMemoryRoomMessageStore();
const publisher = recordingPublisher();
await postRoomMessage(
{ roomMessages, publish: publisher.publish },
{
tenantId: TENANT,
workbenchId: WORKBENCH,
sender: { name: null, address: "prn_ada@acme.example" },
parts: [{ kind: "text", text: "let's pull Scout in" }],
},
);
await Bun.sleep(2);
const joined = await postRoomMessage(
{ roomMessages, publish: publisher.publish },
{
tenantId: TENANT,
workbenchId: WORKBENCH,
sender: { name: null, address: "run_scout@acme.example" },
runId: "run_scout",
parts: [
{
kind: "event",
event: "workbench.agent-joined",
data: { address: "run_scout@acme.example" },
},
],
},
);

const activity = await roomMessages.listActivity({
tenantId: TENANT,
workbenches: [{ workbenchId: WORKBENCH }],
});

expect(activity[WORKBENCH]?.lastActivityAt).toBe(joined.createdAt);
expect(activity[WORKBENCH]?.preview).toBe("let's pull Scout in");
});
});

describe("previewOf", () => {
Expand Down
13 changes: 10 additions & 3 deletions packages/chat/src/room-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,9 @@ function isFailurePreviewParts(parts: readonly Part[]): boolean {
* Pick bench-list preview text from newest-first messages: skip failed
* turns and classified failure paragraphs, keep the last good human/agent
* text, and fall back to the short consumer notice when nothing else
* qualifies (CL-6735). Does not skip ordinary user/agent replies
* (CL-6795 is separate).
* qualifies (CL-6735). Event / attachment-only rows contribute no text and
* are walked past so a join notice never blanks a prior readable preview
* (CL-6795). Ordinary user/agent replies are never skipped.
*/
function activityPreviewFromNewestFirst(
newestFirst: readonly RoomMessage[],
Expand All @@ -199,6 +200,12 @@ function activityPreviewFromNewestFirst(
return "";
}

/** True when listActivity must walk back past the newest row for a preview
* (CL-6735 failures; CL-6795 empty event/attachment-only newest). */
function needsPreviewLookback(parts: readonly Part[]): boolean {
return isFailurePreviewParts(parts) || previewOf(parts).length === 0;
}

function summaryOf(
newest: RoomMessage,
unreadCount: number,
Expand Down Expand Up @@ -430,7 +437,7 @@ export function createDrizzleRoomMessageStore(
const newest = toRoomMessage(row as MessageRow);
const unreadCount = unreadByWorkbenchId.get(newest.workbenchId) ?? 0;
let newestFirstForPreview: readonly RoomMessage[] = [newest];
if (isFailurePreviewParts(newest.parts)) {
if (needsPreviewLookback(newest.parts)) {
const recentRows = await db
.select()
.from(workbenchMessages)
Expand Down
40 changes: 40 additions & 0 deletions packages/chat/test/room-activity.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,44 @@ describeIfDb("createDrizzleRoomMessageStore: listActivity", () => {
await sql.end();
}
});

// CL-6795: a join/event newest row must not blank the list preview while
// earlier readable text still exists.
test("a join notice after readable text keeps the prior preview", async () => {
const sql = postgres(scratchUrl, { max: 5, onnotice: () => undefined });
try {
const store = createDrizzleRoomMessageStore(drizzle(sql));
await store.insertMessage({
id: "msg_join_keep_1",
tenantId: TENANT,
workbenchId: "run_join_keep",
sender: { name: null, address: "prn_alice@acme.example" },
parts: [{ kind: "text", text: "let's pull Scout in" }],
});
const joined = await store.insertMessage({
id: "msg_join_keep_2",
tenantId: TENANT,
workbenchId: "run_join_keep",
sender: { name: null, address: "run_scout@acme.example" },
runId: "run_scout",
parts: [
{
kind: "event",
event: "workbench.agent-joined",
data: { address: "run_scout@acme.example" },
},
],
});

const activity = await store.listActivity({
tenantId: TENANT,
workbenches: [{ workbenchId: "run_join_keep" }],
});

expect(activity["run_join_keep"]?.lastActivityAt).toBe(joined.createdAt);
expect(activity["run_join_keep"]?.preview).toBe("let's pull Scout in");
} finally {
await sql.end();
}
});
});
Loading