Improve conference room history labels

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-06-16 17:38:49 +00:00
1 parent bc43558c58
commit a8e4bfbdeb
5 files changed
+136 -3

No files matched your search

@@ -10,6 +10,7 @@ const ASSIGNEE_AGENT_ID = "33333333-3333-4333-8333-333333333333";
const mockIssueService = vi.hoisted(() => ({
getById: vi.fn(),
update: vi.fn(),
addComment: vi.fn(),
listComments: vi.fn(),
getComment: vi.fn(),
@@ -175,6 +176,7 @@ describe("selected-agent issue chat backend", () => {
explanation: "Allowed by test grant.",
});
mockIssueService.getById.mockResolvedValue(makeIssue());
mockIssueService.update.mockResolvedValue(makeIssue());
mockIssueService.addComment.mockResolvedValue({
id: "comment-1",
issueId: ISSUE_ID,
@@ -240,6 +242,55 @@ describe("selected-agent issue chat backend", () => {
);
});
it("titles new board chat issues from the first selected-agent message and stores the target agent", async () => {
mockIssueService.getById.mockResolvedValue(makeIssue({
originKind: "board_chat",
originId: null,
title: "New chat",
}));
const db = createDbStub(
[makeAgent({ id: CEO_AGENT_ID, name: "CEO", role: "ceo" })],
[],
);
const res = await request(await createApp(db))
.post(`/api/issues/${ISSUE_ID}/selected-agent-chat/comments`)
.send({ body: "Can you summarize the hiring plan before tomorrow?" });
expect(res.status, JSON.stringify(res.body)).toBe(201);
expect(mockIssueService.update).toHaveBeenCalledWith(
ISSUE_ID,
{
originId: CEO_AGENT_ID,
title: "Can you summarize the hiring plan before tomorrow?",
},
);
});
it("keeps a custom board chat title while refreshing the selected target agent", async () => {
mockIssueService.getById.mockResolvedValue(makeIssue({
originKind: "board_chat",
originId: CEO_AGENT_ID,
title: "Hiring plan",
}));
const db = createDbStub(
[makeAgent()],
[],
);
const res = await request(await createApp(db))
.post(`/api/issues/${ISSUE_ID}/selected-agent-chat/comments`)
.send({ body: "switch this to the CTO", targetAgentId: TARGET_AGENT_ID });
expect(res.status, JSON.stringify(res.body)).toBe(201);
expect(mockIssueService.update).toHaveBeenCalledWith(
ISSUE_ID,
{
originId: TARGET_AGENT_ID,
},
);
});
it("rejects a targetAgentId outside the issue company", async () => {
const db = createDbStub([makeAgent({ companyId: "other-company" })]);
+23
View File
@@ -753,6 +753,16 @@ function selectedAgentChatTaskKey(issueId: string, targetAgentId: string) {
return `selected-agent-chat:${issueId}:${targetAgentId}`;
}
const BOARD_CHAT_ORIGIN_KIND = "board_chat";
const BOARD_CHAT_GENERIC_TITLES = new Set(["New chat", "Board Operations"]);
function deriveSelectedAgentBoardChatTitle(message: string): string {
const singleLine = message.replace(/\s+/g, " ").trim();
if (!singleLine) return "New chat";
if (singleLine.length <= 80) return singleLine;
return `${singleLine.slice(0, 77).trimEnd()}...`;
}
function shouldImplicitlyMoveCommentedIssueToTodo(input: {
issueStatus: string | null | undefined;
assigneeAgentId: string | null | undefined;
@@ -6565,6 +6575,19 @@ export function issueRoutes(
sourceTrust,
});
if (issue.originKind === BOARD_CHAT_ORIGIN_KIND) {
const boardChatPatch: Partial<typeof issueRows.$inferInsert> = {};
if (issue.originId !== targetAgent.id) {
boardChatPatch.originId = targetAgent.id;
}
if (BOARD_CHAT_GENERIC_TITLES.has(issue.title)) {
boardChatPatch.title = deriveSelectedAgentBoardChatTitle(req.body.body);
}
if (Object.keys(boardChatPatch).length > 0) {
await svc.update(issue.id, boardChatPatch);
}
}
await issueReferencesSvc.syncComment(comment.id);
await logActivity(db, {
companyId: issue.companyId,
+4 -1
View File
@@ -315,6 +315,7 @@ export interface SelectedAgentChatProps {
showAgentSwitcher?: boolean;
currentUserId?: string | null;
emptyMessage?: string;
onMessageSent?: () => void | Promise<void>;
className?: string;
}
@@ -333,6 +334,7 @@ export function SelectedAgentChat({
showAgentSwitcher = true,
currentUserId,
emptyMessage,
onMessageSent,
className,
}: SelectedAgentChatProps) {
const queryClient = useQueryClient();
@@ -409,6 +411,7 @@ export function SelectedAgentChat({
setErrorText(null);
try {
await issuesApi.addSelectedAgentChatComment(issueId, body, { targetAgentId });
await onMessageSent?.();
invalidateRuns();
} catch (err) {
setErrorText(
@@ -420,7 +423,7 @@ export function SelectedAgentChat({
throw err;
}
},
[issueId, targetAgentId, invalidateRuns],
[issueId, targetAgentId, onMessageSent, invalidateRuns],
);
const handleStopRun = useCallback(
+25
View File
@@ -95,11 +95,19 @@ const CEO_AGENT = {
status: "active",
icon: null,
};
const CTO_AGENT = {
id: "agent-cto",
name: "Morgan",
role: "cto",
status: "active",
icon: null,
};
const BOARD_ISSUE = {
id: "issue-board",
identifier: "PAP-1",
title: "How is hiring going?",
originKind: "board_chat",
originId: "agent-ceo",
status: "in_progress",
createdAt: "2026-06-10T00:00:00.000Z",
updatedAt: "2026-06-10T00:00:00.000Z",
@@ -109,6 +117,7 @@ const LEGACY_BOARD_ISSUE = {
identifier: "PAP-0",
title: "Board Operations",
originKind: "board_chat",
originId: null,
status: "todo",
createdAt: "2026-06-09T00:00:00.000Z",
updatedAt: "2026-06-09T00:00:00.000Z",
@@ -251,6 +260,22 @@ describe("BoardChat Conference Room transport", () => {
expect(container.textContent).not.toContain("PAP-0");
});
it("shows the selected chat agent in history rows", async () => {
mockAgentsApi.list.mockResolvedValue([CEO_AGENT, CTO_AGENT]);
mockIssuesApi.list.mockResolvedValue([
{
...BOARD_ISSUE,
id: "issue-cto-chat",
originId: CTO_AGENT.id,
title: "Architecture review",
},
]);
await render();
expect(container.textContent).toContain("Architecture review");
expect(container.textContent).toContain("Morgan · CTO");
});
it("reserves mobile viewport height and bottom-nav space for the agent feed", async () => {
await render();
+33 -2
View File
@@ -24,7 +24,7 @@ import { Activity, AlertCircle, History, Loader2, MessageSquarePlus } from "luci
import { ActivityFeed } from "../components/ActivityFeed";
import { SelectedAgentChat } from "../components/SelectedAgentChat";
import { cn, relativeTime } from "../lib/utils";
import type { Issue } from "@paperclipai/shared";
import { AGENT_ROLE_LABELS, type Agent, type Issue } from "@paperclipai/shared";
import {
Sheet,
SheetContent,
@@ -51,6 +51,7 @@ const DEFAULT_CHAT_FRACTION = 2 / 3;
const BOARD_CHAT_ISSUE_TITLE = "Board Operations";
const BOARD_CHAT_ORIGIN_KIND = "board_chat";
const roleLabels = AGENT_ROLE_LABELS as Record<string, string>;
const boardChatHistoryDateFormatter = new Intl.DateTimeFormat(undefined, {
month: "short",
@@ -72,6 +73,21 @@ function boardChatHistoryLabel(issue: Pick<Issue, "title" | "createdAt" | "updat
return issue.title;
}
function boardChatHistoryAgentLabel(
issue: Pick<Issue, "originId" | "originKind">,
agentsById: Map<string, Agent>,
fallbackAgent: Agent | null,
): string | null {
const targetAgent =
issue.originKind === BOARD_CHAT_ORIGIN_KIND && issue.originId
? agentsById.get(issue.originId) ?? null
: null;
const agent = targetAgent ?? fallbackAgent;
if (!agent) return null;
const role = roleLabels[agent.role] ?? agent.role;
return `${agent.name} · ${role}`;
}
export function BoardChat() {
const { selectedCompanyId, selectedCompany } = useCompany();
const { setBreadcrumbs } = useBreadcrumbs();
@@ -176,6 +192,10 @@ export function BoardChat() {
() => agents?.find((a) => a.role === "ceo" && a.status !== "terminated"),
[agents],
);
const agentsById = useMemo(
() => new Map((agents ?? []).map((agent) => [agent.id, agent] as const)),
[agents],
);
// Conference Room conversations are backed by ordinary company-scoped
// `board_chat` issues so history survives reloads without a separate store.
@@ -290,6 +310,11 @@ export function BoardChat() {
setMintError(null);
}, []);
const refreshBoardChatHistory = useCallback(async () => {
if (!historyQueryKey) return;
await queryClient.invalidateQueries({ queryKey: historyQueryKey });
}, [historyQueryKey, queryClient]);
if (!selectedCompanyId) {
return (
<div className="flex items-center justify-center h-full">
@@ -395,6 +420,11 @@ export function BoardChat() {
{boardChatIssues.map((issue) => {
const active = issue.id === boardIssueId;
const label = boardChatHistoryLabel(issue);
const agentLabel = boardChatHistoryAgentLabel(
issue,
agentsById,
ceoAgent ?? null,
);
return (
<button
key={issue.id}
@@ -412,7 +442,7 @@ export function BoardChat() {
{label}
</span>
<span className="block truncate text-xs text-muted-foreground">
Updated {relativeTime(issue.updatedAt)}
{agentLabel ? `${agentLabel} · ` : ""}updated {relativeTime(issue.updatedAt)}
</span>
</span>
</button>
@@ -436,6 +466,7 @@ export function BoardChat() {
defaultTargetAgentId={ceoAgent?.id ?? null}
currentUserId={currentUserId}
emptyMessage={`Send ${ceoAgent?.name ?? "your CEO"} a message to start the conversation.`}
onMessageSent={refreshBoardChatHistory}
className="min-h-0 flex-1"
/>
) : mintError ? (