mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 21:07:40 +01:00
Improve conference room history labels
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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" })]);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
Reference in new issue
Block a user