import { __cf_data, computed, Default, equals, handler, NAME, pattern, type PatternFactory, SELF, Stream, toSchema, UI, type VNode, Writable, } from "commonfabric"; import AgentQueue, { type AgentQueueOutput, withAgentQueueRunLinkSchema, } from "./agent-queue.tsx"; import FabriChatManager, { type FabriChatManagerOutput, } from "../fabrichat/manager.tsx"; import FavoritesManager from "./favorites-manager.tsx"; import Self from "../self.tsx"; import SharedSpaceCollection, { type SharedSpaceCollectionOutput, } from "./shared-space-collection.tsx"; import { sharedSpaceCollectionViewOf, type SharedSpaceInterface, sharedSpaceReceiptViewOf, } from "./shared-space-interface.ts"; import { changeSharedSpaceMembership, readSharedSpaceCatalog, registerSharedSpace, removeSharedSpace, type SharedSpaceCatalog, type SharedSpaceCatalogStorage, type SharedSpaceMembershipChange, type SharedSpaceMembershipResult, type SharedSpaceRegistration, type SharedSpaceRegistrationResult, type SharedSpaceRemoval, type SharedSpaceRemovalResult, } from "./shared-space-catalog.ts"; import { type CreateProfileEvent, seedProfileName, submitProfileCreation, type TrustedDefaultProfile, type TrustedProfileList, type TrustedProfileMru, } from "./profile-create.tsx"; import ProfilePicker from "./profile-picker.tsx"; import { refusalReasonText } from "./private-inbox-refusal-text.ts"; import type { BackwardsCompatibleProfile } from "./profile-home.tsx"; import { ensurePrivateInbox, type EnsurePrivateInboxEvent, pointProfilesAtPrivateInbox, PrivateInboxAnswering, type PrivateInboxHolder, type PrivateInboxReceipt, type PrivateInboxRefusalHolder, type PrivateInboxReplacementHolder, type PrivateInboxRequest, replaceRefusedPrivateInbox, repointProfilesFromReplacedInbox, requestPrivateInbox, type RetainedPrivateInboxes, TRUSTED_PRIVATE_INBOX_REFUSAL_SURFACE, TRUSTED_REPLACE_REFUSED_INBOX_ACTION, } from "./private-inbox.tsx"; // Types from favorites-manager.tsx type Favorite = { cell: { [NAME]?: string }; // Discovery tags snapshotted from the piece's schema when favorited. tags: string[]; userTags: string[]; spaceName?: string; // Stable key the favorite entity is addressed by (the piece's identity). id?: string; }; type JournalEntry = { timestamp?: number; eventType?: string; subject?: Writable; snapshot?: { name?: string; schemaTag?: string; valueExcerpt?: string }; narrative?: string; narrativePending?: boolean; tags?: string[]; space?: string; }; // An entry of the space list. An entry with a `did` opens that space, and its // `name` is only what the entry is called: two entries may share one, and // renaming one changes nothing else. An entry written before spaces had random // identities has only a `name`, which resolves as a legacy space name until the // runtime adopts the entry (`adoptSpace`) under the DID the name resolves to. type SpaceEntry = { name: string; did?: string; }; export type HomeOutput = { /** The collection browser's named instance and session-local controls. */ socialSpaceCollection: SharedSpaceCollectionOutput; [NAME]: string; [UI]: VNode; // These defaults are part of Home's published result contract. Pattern setup // generates the output fields, so a newly added result does not need a // migration default; once a default has shipped, however, removing it changes // the contract's materialization semantics. favorites: Writable>; journal: Writable>; spaces: Writable>; defaultAppUrl: Writable>; // The profile list/mru are CFC-wrapped (WriteAuthorizedBy), so their existing // default goes OUTSIDE the wrapper — `Default, []>`, exactly as // profile-home spells externalLinks/verifiedIdentities. An empty default // carries no elements and therefore asserts no writer claims; the contract // governs every real element appended through the trusted create surface. // `defaultProfile` is the slot holding the selected profile's link under // `profile`, and no `profile` while none is selected (`DefaultProfileSlot`). // It is optional, decided by the `?` marker, because a home can hold none. // `legacyDefaultProfile` is a default held as a link at the root of the // `defaultProfile` cell, the shape a home holds one in when it was chosen // before the slot. It is the default while the slot holds none. profiles: Default; defaultProfile?: TrustedDefaultProfile; legacyDefaultProfile?: BackwardsCompatibleProfile; mru: Default; // The user's agent queue: the index of their agent runs and their // registered runner. `wish({ query: "#agent_queue" })` resolves to it, and // the `agent` builtin appends to its `entries`. agentQueue: AgentQueueOutput; // The user's chat manager: the index of the FabriChat rooms they belong to. // `wish({ query: "#chatManager" })` resolves to it. chatManager: FabriChatManagerOutput; // The user's private inbox, where others deliver offers to them: the one the // deciding profile points at, if the host vetted it, or, when no profile // advertises an inbox, one it created. The deciding profile is the first, in // the order `#profile` answers in, that points at an inbox. Home keeps the // inbox it holds while that profile points at it, and while no profile // points at an inbox. Each of their profiles that points at no inbox is // pointed here; one that points at another inbox keeps it. Absent until // `ensurePrivateInbox` runs, and while Home holds none and the advertised // inbox failed vetting. privateInbox: Writable; // The inboxes `privateInbox` held before, in the order Home gave them up, so // that what senders delivered to them stays readable. retainedPrivateInboxes: Writable>; // The host's refusal of the inbox the deciding profile points at, under // `refusal`: why the host refused it, by the host's code; the refused inbox; // and when Home first recorded it. `ensurePrivateInbox` records one only // while that profile is in Home's list and still points at the refused inbox, // which is not the one Home holds. The next ensure clears it when Home adopts // or creates an inbox, when the deciding profile points at the inbox Home // holds, or when no profile points at the refused inbox any longer; nothing // else clears it automatically between ensures, though the owner's own code // can, through `ensurePrivateInbox`. No `refusal` while there is none to // report. privateInboxRefusal: Writable< PrivateInboxRefusalHolder | Default> >; // The last refusal the owner had Home replace, under `replacement`: the // refused inbox, the refusal's code and time, and when the owner acted. Only // `replaceRefusedPrivateInbox` writes it, from the button on Home's refusal // notice. No `replacement` before the first. privateInboxReplacement: Writable; // Independent roles for product consumers. Select `commands` for writes, // `view` for collection lists, and `receipts` for offer admission evidence. sharedSpaces: SharedSpaceInterface; // Flat aliases address the same catalog and command streams. sharedSpaceCatalog: SharedSpaceCatalog; registerSharedSpace: Stream< SharedSpaceRegistration, SharedSpaceRegistrationResult >; changeSharedSpaceMembership: Stream< SharedSpaceMembershipChange, SharedSpaceMembershipResult >; // Only for an application undoing its own import of shared spaces: removes // one entry no offer receipt names, at the revision the caller observed (see // `removeSharedSpace` for what the caller must do). Home renders no control // for it, and nothing a person invokes calls it: archive is how a person puts // a shared space away. removeSharedSpace: Stream; createProfile: Stream; // Gives Home the private inbox the deciding profile advertises: it adopts the // one the host vetted and names, with that profile, when the profile is in // Home's list and still points at it, or creates one when Home holds none // and no profile points at an inbox, and points every profile that points at // no inbox at Home's. It records the host's refusal of the deciding // profile's inbox in `privateInboxRefusal`. // The host sends it the first time a runtime worker brings up Home, and // again at that worker's next bring-up if the ensure failed, so Home adopts // again only then. `requestPrivateInbox` sends it an event naming nothing, // which adopts nothing. ensurePrivateInbox: Stream; // For a client, such as a loom daemon, that would otherwise create an inbox // of its own: has Home ensure its inbox, as `ensurePrivateInbox` does for an // event naming nothing, and links the handling's receipt to Home's answer // for the profile the request names, which reads `ensured` once that profile // points at Home's inbox, or a final status saying why it never will. requestPrivateInbox: Stream; // For the owner, from the button on Home's refusal notice: replaces the // refused inbox with the one Home holds, or with one it creates when it holds // none, and points the profiles that point at the refused inbox at Home's. // Refused unless the renderer marks the event as a click on that button. replaceRefusedPrivateInbox: Stream; addFavorite: Stream<{ piece: Writable<{ [NAME]?: string }>; tags?: string[]; spaceName?: string; id?: string; }>; removeFavorite: Stream<{ piece?: Writable; id?: string }>; addJournalEntry: Stream<{ entry: JournalEntry }>; addSpace: Stream< { did?: string; name?: string; detail?: { message: string } } >; removeSpace: Stream<{ did?: string; name?: string }>; adoptSpace: Stream<{ name: string; did: string }>; renameSpace: Stream<{ did: string; name: string }>; }; // Handler to add a favorite const addFavorite = handler< { piece: Writable<{ [NAME]?: string }>; tags?: string[]; spaceName?: string; id?: string; }, { favorites: Writable } >(({ piece, tags, spaceName, id }, { favorites }) => { // The favorite is addressed by the piece's identity (the client-supplied id), // so favoriting the same piece from two sessions resolves to one membership // entry and favorites of distinct pieces merge, without reading the whole // list. if (!id) return; const entry = favorites.elementById(id); // Only seed the entity on a fresh favorite; a re-favorite keeps the existing // userTags. Discovery tags are derived by the client (which can see the // piece's schema) and passed in; the handler just stores them. if (!entry.get()) { entry.set({ cell: piece, tags: tags ?? [], userTags: [], spaceName, id, }); } favorites.addUnique(entry); }); // Handler to remove a favorite const removeFavorite = handler< { piece?: Writable; id?: string }, { favorites: Writable } >(({ piece, id }, { favorites }) => { // A favorite added through the keyed path has an entity at elementById(id). // Drop its membership by identity (concurrent unfavorites of distinct pieces // merge) and clear the entity, since it outlives its link — a later // re-favorite reads it back to decide whether to seed fresh. if (id) { const entry: Writable = favorites.elementById(id); if (entry.get()) { favorites.removeByValue(favorites.elementById(id)); entry.set(undefined); return; } } // A favorite added before keyed addressing has no such entity; remove it by // matching its piece cell. This rewrites the whole list, but keyed entries // keep their addressing. if (!piece) return; favorites.set(favorites.get().filter((f) => f && !equals(f.cell, piece))); }); // Handler to add a journal entry (kept for schema compatibility) const addJournalEntry = handler< { entry: JournalEntry }, { journal: Writable } >(({ entry }, { journal }) => { journal.push(entry); }); // Handler to add a space to the managed list. The space already exists: the // runtime creates it and sends its DID here. The entry is addressed by the DID, // so two sessions adding one space resolve to one entry, and adds of distinct // spaces merge, without reading the whole list. An event with no label, or an // empty one, keeps the label an existing entry has. An event carrying only a // typed name (`detail.message`) records a name-only entry, which opens the // legacy space the name resolves to and which the runtime later adopts under // that DID. const addSpaceHandler = handler< { did?: string; name?: string; detail?: { message: string } }, { spaces: Writable } >(({ did, name, detail }, { spaces }) => { if (did) { const entry = spaces.elementById(did); entry.set({ name: name || entry.get()?.name || "", did }); spaces.addUnique(entry); return; } const legacyName = detail?.message?.trim(); if (!legacyName) return; const entry = spaces.elementById(legacyName); entry.set({ name: legacyName }); spaces.addUnique(entry); }); // Handler to remove a space from the managed list. The per-row button binds the // entry's DID, or a legacy entry's name, as state; the exported `removeSpace` // stream passes one in the event. Removing an entry changes nothing about the // space it named. const removeSpaceHandler = handler< { did?: string; name?: string }, { did?: string; name?: string; spaces: Writable } >((event, state) => { // removeByValue matches by the deterministic link, so concurrent removes of // distinct spaces merge instead of clobbering through a whole-list set. const key = event.did ?? event.name ?? state.did ?? state.name; if (!key) return; state.spaces.removeByValue(state.spaces.elementById(key)); }); // Handler that replaces a legacy entry, addressed by its name, with an entry // addressed by the DID the name resolves to and called by the same name. const adoptSpaceHandler = handler< { name: string; did: string }, { spaces: Writable } >(({ name, did }, { spaces }) => { if (!name || !did) return; const entry = spaces.elementById(did); entry.set({ name, did }); spaces.addUnique(entry); spaces.removeByValue(spaces.elementById(name)); }); // Handler that changes what an entry is called, and nothing else. const renameSpaceHandler = handler< { did: string; name: string }, { spaces: Writable } >(({ did, name }, { spaces }) => { if (!did) return; const entry = spaces.elementById(did); entry.set({ name: name ?? "", did }); spaces.addUnique(entry); }); const homeArgumentSchema = toSchema>(); const homeResultSchema = __cf_data( withAgentQueueRunLinkSchema(toSchema()), ); const Home = pattern( (input: Record & { [SELF]: HomeOutput }): HomeOutput => { const home = input[SELF]; // OWN the data cells (.for for id stability) const favorites = new Writable([]).for("favorites"); const journal = new Writable([]).for("journal"); const spaces = new Writable([]).for("spaces"); const catalog = new Writable({ entries: {}, offers: {}, }).for("sharedSpaceCatalog"); const defaultAppUrl = new Writable("").for("defaultAppUrl"); // NOTE(CT-1628): the `as any` casts around the profile cells below are // required because the CFC wrapper types (TrustedProfile*) don't yet compose // with Writable/the pattern factory output type. Tracked for a proper type // fix. // // Multi-profile model: a user has many profiles, each in its own `inSpace` // space. `profiles` is the durable list (appended on create). // `defaultProfile` holds, under `profile`, the one `#profile` resolves to // in headless mode and orders first in the picker; `mru` is the // recency-ordered list driving the rest of the ordering. The default's cell // carries its trusted type, so its write contract labels the document // `setDefaultProfile` writes. const profiles = new Writable([]).for( "profiles", ); const defaultProfile = new Writable({}).for( "defaultProfileSlot", ); // A default chosen before the slot: a link at the root of this cell. It // stays the default until one is chosen in the slot, and nothing writes // it, since a handle to a cell whose root holds a link denotes the linked // profile rather than the cell. const legacyDefaultProfile = new Writable< BackwardsCompatibleProfile | undefined >(undefined).for("defaultProfile"); const mru = new Writable([]).for("mru"); // Home's private inbox, which a profile is pointed at when it is created // and when the host ensures the inbox. const privateInbox = new Writable({}).for( "privateInbox", ); // The inboxes Home held before, where `ensurePrivateInbox` moves the one // Home holds when it adopts another. const retainedPrivateInboxes = new Writable([]) .for("retainedPrivateInboxes"); // Where `ensurePrivateInbox` records the host's refusal of the inbox the // deciding profile points at. const privateInboxRefusal = new Writable({}) .for("privateInboxRefusal"); // The last refusal the owner had Home replace, which only the notice's // button writes. const privateInboxReplacement = new Writable( {}, ).for("privateInboxReplacement"); // The profiles requests for Home's inbox have named, by entity, which // Home goes on answering for. const privateInboxRequests = new Writable>({}).for( "privateInboxRequests", ); // The refusal notice reads the refusal's code and time, and never its // `inbox`: the stored link carries the label the refused inbox's owner // gives its offers. The notice renders after everything else, for the // reason the chats panel does, and the screen's header slot shows it // above the tabs. Only its text is computed: its markup, button included, // is static, and `refused` decides whether it shows. const refusalReason = privateInboxRefusal.key("refusal").key("reason"); const refusedAt = privateInboxRefusal.key("refusal").key("refusedAt"); const refused = computed(() => typeof refusedAt.get() === "number"); const refusalNoticeText = computed(() => { const stored = refusalReason.get(); const reason = typeof stored === "string" ? stored : ""; const at = refusedAt.get(); return { explanation: refusalReasonText(reason) ?? "", code: reason || "none given", // The sandbox formats a date given no zone in UTC and one given a // zone in that zone, while a date's local getters read the host's // zone, so the zone is given here and named in the text. noticed: typeof at === "number" ? new Date(at).toLocaleString("en-US", { timeZone: "UTC", timeZoneName: "short", }) : "", }; }); // Untrusted-write regression surface: this stream is exported so tests can // verify that sending it from outside the trusted create surface does NOT // create a profile. The actual create UI lives in the profile picker below. const createProfileStream = submitProfileCreation({ profiles: profiles as any, seedName: seedProfileName({ privateInbox }), }); // The home Profile tab IS the profile picker: it lists profiles natively, // sets the default, stamps MRU on selection, and creates more inline. const profilePicker = ProfilePicker({ profiles: profiles as any, defaultProfile: defaultProfile as any, legacyDefaultProfile: legacyDefaultProfile as any, offersSetDefault: true, mru: mru as any, privateInbox, }); // Child components const favoritesComponent = FavoritesManager({}); const agentQueue = AgentQueue({}); const chatManager = FabriChatManager({ sharedSpaceCatalog: catalog }); const ensurePrivateInboxStream = ensurePrivateInbox({ privateInbox, retainedPrivateInboxes, privateInboxRefusal, profiles: profiles as any, pointProfiles: pointProfilesAtPrivateInbox({ privateInbox, profiles: profiles as any, }), }); const replaceRefusedInboxStream = replaceRefusedPrivateInbox({ privateInbox, privateInboxRefusal, privateInboxReplacement, repointProfiles: repointProfilesFromReplacedInbox({ privateInbox, privateInboxReplacement, profiles: profiles as any, }), }); // Private self-model — the "real you" tier (values, neurotype, meaning Q&A), // home-local and never shared. Distinct from the outward profile/personas in // the Profile tab. Owns its own durable cell (seeded via Default<>). const selfComponent = Self({}); const activeTab = new Writable("spaces").for("activeTab"); // Home's answers to requests for its inbox, which `requestPrivateInbox` // links a request's receipt to. const privateInboxAnswering = PrivateInboxAnswering({ privateInbox, profiles: profiles as any, requested: privateInboxRequests, }); const sharedSpaces = { view: computed(() => sharedSpaceCollectionViewOf(catalog.get())), receipts: computed(() => sharedSpaceReceiptViewOf(catalog.get())), commands: { register: home.registerSharedSpace, changeMembership: home.changeSharedSpaceMembership, }, imports: { remove: home.removeSharedSpace }, }; // The builder numbers unnamed roots in input/result walk order, including // the UI. Naming this spot preserves every existing positional cause. const socialSpaceCollection = SharedSpaceCollection({ catalog, view: sharedSpaces.view, }); return { [NAME]: `Home`, [UI]: (

homespace

Spaces Favorites Profile Self Agent runs Chats Social spaces {agentQueue} {favoritesComponent} {selfComponent}

Profile

{profilePicker}

My Spaces

{spaces.map((space) => (
✕
))} {computed(() => spaces.get().length === 0 ? (

No spaces yet. Create one below.

) : null )}

Create Space

Type a label and press enter to create a new space. Click the link to open it.

Settings

Pattern URL for new spaces. Leave empty for system default.
{chatManager} {socialSpaceCollection}
{refused ? (

Shares may not reach you

One of your profiles points others at an inbox Home can't use, so what they share with you may not arrive. {" "} {refusalNoticeText.explanation} Reason:{" "} {refusalNoticeText.code}. First noticed {" "} {refusalNoticeText.noticed}. Using a new inbox points the profiles that point at the refused one at Home's own inbox instead. A loom daemon running as an identity other than yours then stops receiving loom shares.
Use a new inbox
) : null}
) as VNode, // Exported data socialSpaceCollection, favorites, journal, spaces, defaultAppUrl, profiles: profiles as any, defaultProfile: defaultProfile as any, legacyDefaultProfile: legacyDefaultProfile as any, mru: mru as any, agentQueue, chatManager, privateInbox, retainedPrivateInboxes, privateInboxRefusal, privateInboxReplacement, sharedSpaces, sharedSpaceCatalog: computed(() => readSharedSpaceCatalog(catalog)), // Exported handlers registerSharedSpace: registerSharedSpace({ catalog }), changeSharedSpaceMembership: changeSharedSpaceMembership({ catalog }), removeSharedSpace: removeSharedSpace({ catalog }), addFavorite: addFavorite({ favorites }), removeFavorite: removeFavorite({ favorites }), addJournalEntry: addJournalEntry({ journal }), addSpace: addSpaceHandler({ spaces }), removeSpace: removeSpaceHandler({ spaces }), adoptSpace: adoptSpaceHandler({ spaces }), renameSpace: renameSpaceHandler({ spaces }), createProfile: createProfileStream, ensurePrivateInbox: ensurePrivateInboxStream, requestPrivateInbox: requestPrivateInbox({ profiles: profiles as any, requested: privateInboxRequests, answers: privateInboxAnswering.answers, ensure: ensurePrivateInboxStream, }), replaceRefusedPrivateInbox: replaceRefusedInboxStream, }; }, homeArgumentSchema, homeResultSchema, ) as PatternFactory, HomeOutput>; export default Home;