diff --git a/apps/hub/package.json b/apps/hub/package.json index 5ce4e18829..b57010df05 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -37,7 +37,7 @@ "@corbits/inbox": "workspace:*", "@corbits/inference-catalog": "workspace:*", "@corbits/last-30-days-research-workflow": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", "@corbits/mcp-tools": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/memory-hub": "workspace:*", diff --git a/bun.lock b/bun.lock index dc3dd7ae69..e933c940a5 100644 --- a/bun.lock +++ b/bun.lock @@ -62,7 +62,7 @@ "@corbits/inbox": "workspace:*", "@corbits/inference-catalog": "workspace:*", "@corbits/last-30-days-research-workflow": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", "@corbits/mcp-tools": "workspace:*", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/memory-hub": "workspace:*", @@ -429,7 +429,7 @@ "@corbits/connections": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/hub-api-client": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/turn-artifacts": "workspace:*", "@corbits/url-path": "workspace:*", @@ -467,6 +467,7 @@ "@corbits/icons": "workspace:*", "@corbits/inference-settings": "workspace:*", "@corbits/jimmy-agent": "workspace:*", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", "@corbits/presence": "workspace:*", "@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635", "@tanstack/react-query": "catalog:", @@ -787,7 +788,7 @@ "version": "0.0.1", "dependencies": { "@corbits/error-sink": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", "@corbits/notify": "workspace:*", "@intx/hub-api": "workspace:*", "@intx/log": "0.3.0", @@ -2159,7 +2160,7 @@ "@corbits/longevity-sim": ["@corbits/longevity-sim@workspace:packages/longevity-sim"], - "@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#fea6211", { "dependencies": { "@hono/standard-validator": "0.2.3", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "@intx/mime": "^0.2.2", "@intx/types": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-fea6211", "sha512-FfJsBGUSr7gRcPn9L0hmlAmqW8VATXP02qzColrshfJJzJHJCEICfzTi0ER+KDeAvNcvvxY7YqQ9IAsx9Pl9gg=="], + "@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#bc96404", { "dependencies": { "@hono/standard-validator": "0.2.3", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "@intx/mime": "^0.2.2", "@intx/types": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-bc96404", "sha512-NEeNP2XsjIKOUbRr5szrCDWAYPQSmMNSu33ODyLUCiXdnq12HYWYG/C+0DcAMELlY+9Ktqf543Kg/Qb22NtjWQ=="], "@corbits/manus-tools": ["@corbits/manus-tools@workspace:packages/manus-tools"], diff --git a/packages/chat-ui/package.json b/packages/chat-ui/package.json index faac02a3e3..5ff8ea2c00 100644 --- a/packages/chat-ui/package.json +++ b/packages/chat-ui/package.json @@ -34,6 +34,7 @@ "@tanstack/react-query": "catalog:", "@corbits/connections": "workspace:*", "@corbits/jimmy-agent": "workspace:*", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", "arktype": "catalog:", "@corbits/icons": "workspace:*", "react": "^19.2.0", diff --git a/packages/chat-ui/src/index.ts b/packages/chat-ui/src/index.ts index 683d5609a1..d242f8f800 100644 --- a/packages/chat-ui/src/index.ts +++ b/packages/chat-ui/src/index.ts @@ -37,6 +37,9 @@ export type { CorbitAvatarSize, } from "./avatar"; +export { threadMessagesToTimeline, roomRefFor } from "./mailbox-timeline"; +export type { TimelineItem } from "./mailbox-timeline"; + export { PinnedStrip } from "./pinned-strip"; export type { PinsStatus } from "./use-workbench-feed"; export { diff --git a/packages/chat-ui/src/mailbox-timeline.test.ts b/packages/chat-ui/src/mailbox-timeline.test.ts new file mode 100644 index 0000000000..35ebb579a8 --- /dev/null +++ b/packages/chat-ui/src/mailbox-timeline.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from "bun:test"; +import type { MailboxThreadMessage } from "@corbits/mailbox"; + +import { roomRefFor, threadMessagesToTimeline } from "./mailbox-timeline"; + +function message( + overrides: Partial & Pick, +): MailboxThreadMessage { + return { + references: [], + fromAddress: "sender@dana.localhost", + createdAt: "2026-09-17T00:00:00.000000Z", + read: true, + archived: false, + parentId: null, + body: "hello", + ...overrides, + }; +} + +describe("threadMessagesToTimeline", () => { + test("links a reply's threadId to its parent's own id via inReplyTo", () => { + const parent = message({ id: "id-1", messageId: "msg-1@mail" }); + const child = message({ + id: "id-2", + messageId: "msg-2@mail", + inReplyTo: "msg-1@mail", + body: "reply", + }); + + const items = threadMessagesToTimeline([parent, child]); + + expect(items[0]?.threadId).toBeUndefined(); + expect(items[1]?.threadId).toBe("id-1"); + }); + + test("leaves threadId absent when the parent isn't in this batch", () => { + const orphan = message({ + id: "id-3", + messageId: "msg-3@mail", + inReplyTo: "msg-missing@mail", + }); + + const [item] = threadMessagesToTimeline([orphan]); + + expect(item?.threadId).toBeUndefined(); + }); +}); + +describe("roomRefFor", () => { + test("stamps the same { kind: 'workbench', id } ref the mailbox writers use", () => { + expect(roomRefFor("tenant-1", "room-1")).toEqual({ + kind: "workbench", + id: "room-1", + }); + }); +}); diff --git a/packages/chat-ui/src/mailbox-timeline.ts b/packages/chat-ui/src/mailbox-timeline.ts new file mode 100644 index 0000000000..f664227a79 --- /dev/null +++ b/packages/chat-ui/src/mailbox-timeline.ts @@ -0,0 +1,69 @@ +// Pure adapter from a mailbox thread read (`@corbits/mailbox`'s +// `MailboxThreadMessage`) onto the timeline item shape `use-workbench-feed.ts` +// already produces (CL-8174 slice 2a). No new wire shape: a mail message +// becomes exactly the `MessageItem` the rest of chat-ui already knows how to +// render, so the timeline never has to branch on where a message came from. + +import type { MailboxRef, MailboxThreadMessage } from "@corbits/mailbox"; +import type { MessageItem } from "./api"; + +/** The timeline item shape — an alias, not a new type, so a mail-derived + * row and a chat-native row are interchangeable everywhere the timeline + * already consumes `MessageItem`. */ +export type TimelineItem = MessageItem; + +/** + * Maps one mailbox thread's messages onto timeline items. + * + * `subject`, when present, prefixes the body as its own line — mail is the + * only source that carries a subject, so it is folded into the one text + * part a `MessageItem` has room for rather than inventing a field the rest + * of chat-ui would have to learn about. + * + * `threadId` links a reply to its parent's own `id` (not its `messageId`, + * which is the RFC-2822-style id the `inReplyTo`/`references` headers + * carry) by resolving `inReplyTo` against every message's `messageId` in + * this same batch. A message whose parent isn't in this batch — the root, + * or a reply to a message this read didn't page in — keeps `threadId` + * absent rather than guessing. + */ +export function threadMessagesToTimeline( + messages: readonly MailboxThreadMessage[], +): TimelineItem[] { + const idByMessageId = new Map(); + for (const message of messages) { + idByMessageId.set(message.messageId, message.id); + } + + return messages.map((message) => { + const parentId = + message.inReplyTo !== undefined + ? idByMessageId.get(message.inReplyTo) + : undefined; + const text = + message.subject !== undefined && message.subject.length > 0 + ? `${message.subject}\n${message.body}` + : message.body; + return { + id: message.id, + createdAt: message.createdAt, + parts: [{ kind: "text", text }], + sender: { name: null, address: message.fromAddress }, + ...(parentId !== undefined ? { threadId: parentId } : {}), + }; + }); +} + +/** + * The same `{ kind: "workbench", id }` ref every mailbox writer stamps + * (`apps/hub/src/mailbox-persist.ts`'s `hubMailboxResolveRefs`, + * `packages/chat/src/mailbox-fanout.ts`'s `writeChatMailboxFanout`) — a + * workbench is a plain tenant now (CL-8083), so the room a thread hangs + * off is the same id a chat room is scoped to, and `tenantId` is carried + * here only so a caller with just a tenant in hand has a matching call + * shape; the stamped ref itself never varies by which of the two identical + * ids produced it. + */ +export function roomRefFor(_tenantId: string, roomId: string): MailboxRef { + return { kind: "workbench", id: roomId }; +} diff --git a/packages/chat/package.json b/packages/chat/package.json index b60726061e..1f539602e8 100644 --- a/packages/chat/package.json +++ b/packages/chat/package.json @@ -44,7 +44,7 @@ "@corbits/commands": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/hub-api-client": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", "@corbits/memory": "github:corbitsdev/corbits-memory#9e6f213fa2c002b531d3f6af1aa0abd737b8afe3", "@corbits/turn-artifacts": "workspace:*", "@corbits/workflows": "workspace:*", diff --git a/packages/inbox/package.json b/packages/inbox/package.json index 398c434fbe..2d596659b0 100644 --- a/packages/inbox/package.json +++ b/packages/inbox/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@corbits/error-sink": "workspace:*", - "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#fea6211e42fddfc87e8e1755c219c437e45a7d62", + "@corbits/mailbox": "github:corbitsdev/corbits-mailbox#bc964040fa42ffe4e5d5004f87edaab6486e5d60", "@corbits/notify": "workspace:*", "@intx/hub-api": "workspace:*", "@intx/log": "0.3.0",