evOo-local/apps/mobile/lib/inbox-spaces.ts
cynarAI 6ace14c3c8
Projects: one sidebar row with icon and color, coordinator overview (#190)
Each project is now a single sidebar row with its own icon and color that
opens the project's coordinator conversation. The whole row drags to
reorder. The row menu gains Customize for name, icon and color. Inside
the coordinator conversation, an overview lists the project's agent
threads that need the user, are running, or recently finished.


Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-10 03:32:40 +02:00

86 lines
2.9 KiB
TypeScript

import type { SpaceBot, SpaceGroup } from "@rakazo/contracts";
import { groupBotsForSidebar } from "@rakazo/core";
import {
adoptDeletedSpaceFallback,
type MobileBot,
type MobileGroup,
type MobileSpace,
rpc,
selectedSpaceId,
selectSpace,
} from "./api";
export type InboxSpace = Pick<
MobileSpace,
"id" | "name" | "isDefault" | "hasContent" | "canDelete" | "botSections" | "icon" | "color"
> & {
bots: (MobileBot | SpaceBot)[];
groups: (MobileGroup | SpaceGroup)[];
};
export type InboxSpaceItem =
| { type: "bot"; bot: MobileBot | SpaceBot }
| { type: "group"; group: MobileGroup | SpaceGroup }
| { type: "heading"; key: string; title: string; space?: InboxSpace };
export function canDeleteInboxSpace(
space: Pick<InboxSpace, "isDefault" | "hasContent" | "canDelete">,
) {
return !space.isDefault && !space.hasContent && space.canDelete === true;
}
export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] {
return spaces.flatMap((space): InboxSpaceItem[] => {
const chats = [
// A main bot is reached through its space's main conversation, not listed again.
...space.bots
.filter((chat) => chat.kind !== "main")
.map((chat) => ({ type: "bot" as const, bot: chat, ...chat })),
...space.groups.map((chat) => ({ type: "group" as const, group: chat, ...chat })),
];
const items: InboxSpaceItem[] = [];
if (spaces.length > 1 || !space.isDefault || chats.length === 0) {
items.push({ type: "heading", key: space.id, title: space.name, space });
}
for (const group of groupBotsForSidebar(chats, space.botSections)) {
if (group.title) {
items.push({ type: "heading", key: `${space.id}:${group.key}`, title: group.title });
}
items.push(...group.bots);
}
return items;
});
}
export async function selectInboxSpace(spaceId: string, refresh: () => Promise<void>) {
if (!(await selectSpace(spaceId).catch(() => false))) return false;
await refresh();
return true;
}
export async function retryInboxSpaceFallback(spaceId: string, refresh: () => Promise<void>) {
if (!(await adoptDeletedSpaceFallback(spaceId))) return false;
await refresh();
return true;
}
// Return the selection to retry if native storage fails after a successful deletion.
// The inbox must not refresh against the deleted selection in that case.
export async function removeInboxSpace(
spaceId: string,
refresh: () => Promise<void>,
): Promise<string | null> {
const response = await rpc<{ ok: true; activeSpaceId: string }>("spaces/remove", { spaceId });
if (selectedSpaceId() === spaceId) {
if (await selectSpace(response.activeSpaceId).catch(() => false)) {
await refresh();
return null;
}
const durable = await adoptDeletedSpaceFallback(response.activeSpaceId);
await refresh();
return durable ? null : response.activeSpaceId;
}
await refresh();
return null;
}