From 6ace14c3c8bf45493bfc991a9c5aa0c80995ceb3 Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Sat, 10 Oct 2026 03:32:40 +0200 Subject: [PATCH] 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 --- apps/api/src/router.ts | 30 ++++ apps/mobile/app/index.tsx | 22 ++- apps/mobile/lib/inbox-spaces.ts | 2 +- apps/web/e2e/empty-project-state.spec.ts | 7 +- apps/web/e2e/helpers.ts | 5 + apps/web/e2e/project-coordinator.spec.ts | 77 ++++++++ apps/web/e2e/project-main-agent.spec.ts | 19 +- apps/web/e2e/spaces.spec.ts | 48 ++--- apps/web/src/components/SortableGroup.tsx | 65 +------ apps/web/src/components/project-icon.tsx | 167 ++++++++++++++++++ apps/web/src/lib/run-activity.ts | 59 +++++++ apps/web/src/locales/de/messages.po | 25 ++- apps/web/src/locales/en/messages.po | 25 ++- apps/web/src/locales/es/messages.po | 25 ++- apps/web/src/locales/hi/messages.po | 25 ++- apps/web/src/locales/ko/messages.po | 25 ++- apps/web/src/locales/pt-BR/messages.po | 25 ++- apps/web/src/locales/ru/messages.po | 25 ++- apps/web/src/locales/tr/messages.po | 25 ++- apps/web/src/locales/zh-CN/messages.po | 25 ++- apps/web/src/pages/ActivityList.tsx | 42 +---- apps/web/src/pages/Shell.tsx | 134 ++++++++++++-- .../src/pages/shell/project-look-dialog.tsx | 125 +++++++++++++ apps/web/src/pages/shell/project-overview.tsx | 75 ++++++++ apps/web/src/pages/shell/project-row.tsx | 95 ++++++++++ apps/web/src/pages/shell/roster-row-style.ts | 7 +- .../src/pages/shell/sidebar-group-header.tsx | 19 +- .../src/pages/shell/use-heute-action-count.ts | 52 ++---- apps/web/src/pages/shell/use-sidebar-model.ts | 2 +- packages/contracts/src/domain.ts | 5 + packages/contracts/src/rpc.ts | 11 ++ packages/core/src/index.ts | 2 + packages/core/src/project-look.test.ts | 26 +++ packages/core/src/project-look.ts | 106 +++++++++++ packages/core/src/project-threads.test.ts | 52 ++++++ packages/core/src/project-threads.ts | 49 +++++ .../migration.sql | 3 + packages/db/prisma/schema.prisma | 3 + packages/db/src/main-agent.postgres.test.ts | 29 ++- packages/db/src/spaces.ts | 29 +++ 40 files changed, 1346 insertions(+), 246 deletions(-) create mode 100644 apps/web/e2e/project-coordinator.spec.ts create mode 100644 apps/web/src/components/project-icon.tsx create mode 100644 apps/web/src/lib/run-activity.ts create mode 100644 apps/web/src/pages/shell/project-look-dialog.tsx create mode 100644 apps/web/src/pages/shell/project-overview.tsx create mode 100644 apps/web/src/pages/shell/project-row.tsx create mode 100644 packages/core/src/project-look.test.ts create mode 100644 packages/core/src/project-look.ts create mode 100644 packages/core/src/project-threads.test.ts create mode 100644 packages/core/src/project-threads.ts create mode 100644 packages/db/prisma/migrations/20261010090000_space_icon_color/migration.sql diff --git a/apps/api/src/router.ts b/apps/api/src/router.ts index 30f89a4..97b7f3c 100644 --- a/apps/api/src/router.ts +++ b/apps/api/src/router.ts @@ -116,6 +116,8 @@ import { expandSkillReferencesInPrompt, hasMixedOneShotSchedule, isOneShotRoutineCrons, + isProjectColor, + isProjectIcon, isValidAvatarValue, nextCronDateAcrossStrict, } from "@rakazo/core"; @@ -168,6 +170,7 @@ import { selectSpaceModelPreference, selectSpaceVoicePreference, updateNotificationSettings, + updateSpaceForOwner, wireProjectWorkspace, } from "@rakazo/db"; import { getLogger } from "@rakazo/logging"; @@ -755,6 +758,7 @@ export function createRouter(deps: RouterDeps) { isDefault: false, hasContent: false, canDelete: true, + canEdit: true, workspace, bots: bots.map(spaceBotDto), groups: groups.map(spaceGroupDto), @@ -776,6 +780,27 @@ export function createRouter(deps: RouterDeps) { spaceId: conversation.spaceId, }; }), + update: authed.spaces.update.handler(async ({ context, input }) => { + if (input.icon !== undefined && !isProjectIcon(input.icon)) { + throw new ORPCError("BAD_REQUEST", { message: "Unknown project icon" }); + } + if (input.color !== undefined && !isProjectColor(input.color)) { + throw new ORPCError("BAD_REQUEST", { message: "Unknown project color" }); + } + try { + await updateSpaceForOwner(deps.prisma, { userId: context.actor.userId, ...input }); + } catch (error) { + if (error instanceof SpaceNotFoundError) { + throw new ORPCError("NOT_FOUND", { message: error.message }); + } + if (error instanceof IsolationError) throw new ORPCError("FORBIDDEN"); + if (error instanceof InvalidSpaceNameError) { + throw new ORPCError("BAD_REQUEST", { message: error.message }); + } + throw error; + } + return { ok: true as const }; + }), remove: authed.spaces.remove.handler(async ({ context, input }) => { let claimId: string | null = null; let claimActive = false; @@ -5444,6 +5469,8 @@ async function spaceNavigationDto( space: { select: { name: true, + icon: true, + color: true, isDefault: true, deletingAt: true, workspacePath: true, @@ -5524,6 +5551,8 @@ async function spaceNavigationDto( return { id: membership.spaceId, name: membership.space.name, + icon: membership.space.icon, + color: membership.space.color, isDefault: membership.space.isDefault, hasContent: spacesWithContent.has(membership.spaceId), canDelete: @@ -5532,6 +5561,7 @@ async function spaceNavigationDto( memberships.length > 1 && !spacesWithContent.has(membership.spaceId) && (membership.space.deletingAt === null || membership.space.deletingAt < staleClaimBefore), + canEdit: membership.role === "owner", // A linked folder is on the owner's host; other members see neither its path nor remote. workspace: actor.isDeploymentOwner ? projectWorkspaceDto(membership.space) : null, bots: spaceBots.map(spaceBotDto), diff --git a/apps/mobile/app/index.tsx b/apps/mobile/app/index.tsx index 91c69f1..fcb84a0 100644 --- a/apps/mobile/app/index.tsx +++ b/apps/mobile/app/index.tsx @@ -6,7 +6,7 @@ import { type SpaceBot, type SpaceGroup, } from "@rakazo/contracts"; -import { runChatTitle } from "@rakazo/core"; +import { projectLook, runChatTitle } from "@rakazo/core"; import { botColors } from "@rakazo/ui-tokens"; import { Redirect, useFocusEffect, useRouter } from "expo-router"; import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -599,6 +599,7 @@ export default function Home() { }} style={({ pressed }) => [styles.spaceSelect, pressed && styles.rowPressed]} > + {item.title} {canDeleteInboxSpace(item.space) ? ( @@ -907,6 +908,16 @@ function ConversationRow({ ); } +function ProjectTile({ space }: { space: Pick }) { + const styles = useThemedStyles(createHomeStyles); + const { icon, color } = projectLook(space); + return ( + + + + ); +} + function CircleButton({ children, onPress, @@ -1192,6 +1203,15 @@ function createHomeStyles() { spaceSelect: { flex: 1, minHeight: 44, + flexDirection: "row", + alignItems: "center", + gap: 10, + }, + projectTile: { + width: 28, + height: 28, + borderRadius: 8, + alignItems: "center", justifyContent: "center", }, spaceTitle: { diff --git a/apps/mobile/lib/inbox-spaces.ts b/apps/mobile/lib/inbox-spaces.ts index 3ee0bc3..532c081 100644 --- a/apps/mobile/lib/inbox-spaces.ts +++ b/apps/mobile/lib/inbox-spaces.ts @@ -12,7 +12,7 @@ import { export type InboxSpace = Pick< MobileSpace, - "id" | "name" | "isDefault" | "hasContent" | "canDelete" | "botSections" + "id" | "name" | "isDefault" | "hasContent" | "canDelete" | "botSections" | "icon" | "color" > & { bots: (MobileBot | SpaceBot)[]; groups: (MobileGroup | SpaceGroup)[]; diff --git a/apps/web/e2e/empty-project-state.spec.ts b/apps/web/e2e/empty-project-state.spec.ts index 821a391..3e06de0 100644 --- a/apps/web/e2e/empty-project-state.spec.ts +++ b/apps/web/e2e/empty-project-state.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { completeOnboarding, routePatchRpc, rpc, signup } from "./helpers"; +import { completeOnboarding, projectRow, routePatchRpc, rpc, signup } from "./helpers"; test("empty project space offers Start before the main conversation exists", async ({ page }) => { const stamp = Date.now(); @@ -40,8 +40,7 @@ test("empty project space offers Start before the main conversation exists", asy await expect(mainStart).toBeVisible({ timeout: 30_000 }); await expect(mainStart).toContainText(/Start|Starten/i); + // The sidebar lists the project as one row; opening it starts the main conversation. const projekte = page.locator(".sidebar-projekte"); - const sidebarStart = projekte.getByTestId(`project-start-${side.id}`); - await expect(sidebarStart).toBeVisible({ timeout: 30_000 }); - await expect(sidebarStart).toContainText(/Start|Starten/i); + await expect(projectRow(projekte, "Empty project")).toBeVisible({ timeout: 30_000 }); }); diff --git a/apps/web/e2e/helpers.ts b/apps/web/e2e/helpers.ts index 9913e10..4769201 100644 --- a/apps/web/e2e/helpers.ts +++ b/apps/web/e2e/helpers.ts @@ -12,6 +12,11 @@ export function unterhaltungenOnly(page: Page) { return page.locator(".sidebar-unterhaltungen").locator(":scope > *:not(.sidebar-projekte)"); } +/** A project's row in the sidebar; it opens the project's main conversation. */ +export function projectRow(scope: Page | Locator, name: string) { + return scope.getByTestId(/^project-row-/).filter({ hasText: name }); +} + export function isRealSandboxProvider(provider = process.env.SANDBOX_PROVIDER) { return provider === "e2b" || provider === "daytona" || provider === "box"; } diff --git a/apps/web/e2e/project-coordinator.spec.ts b/apps/web/e2e/project-coordinator.spec.ts new file mode 100644 index 0000000..1a720f8 --- /dev/null +++ b/apps/web/e2e/project-coordinator.spec.ts @@ -0,0 +1,77 @@ +import { expect, test } from "@playwright/test"; +import { + captureScreenshot, + completeOnboarding, + projectRow, + rpc, + signup, + waitForChatComposer, +} from "./helpers"; + +test("a project is one sidebar row with its own look, and its coordinator lists the delegated threads", async ({ + page, +}, testInfo) => { + const stamp = Date.now(); + await signup(page, `project-coordinator-${stamp}@rakazo.test`, "password12", "Coordinator"); + await completeOnboarding(page); + + const projekte = page.locator(".sidebar-projekte"); + await projekte.getByTestId("create-project-trigger").click(); + const dialog = page.getByRole("dialog", { name: "New project" }); + await dialog.getByLabel("Name").fill("Garden"); + await dialog.getByRole("button", { name: "Create project", exact: true }).click(); + await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 }); + await waitForChatComposer(page); + + const row = projectRow(projekte, "Garden"); + await expect(row).toBeVisible({ timeout: 30_000 }); + await expect(row).toHaveAttribute("aria-current", "page"); + await expect(row.locator("[data-project-icon]")).toHaveAttribute("data-project-icon", "folder"); + + // Name, icon and color change from the row's menu. + await row.click({ button: "right" }); + await page.getByRole("menuitem", { name: "Customize" }).click(); + const look = page.getByTestId("project-look-dialog"); + await expect(look).toBeVisible(); + await look.getByLabel("Name").fill("Garten"); + await look.getByRole("radio", { name: "green" }).click(); + await look.getByTestId("project-icon-leaf").click(); + await captureScreenshot(page, testInfo, "project-look"); + await look.getByRole("button", { name: "Save", exact: true }).click(); + await expect(look).toBeHidden(); + + const renamed = projectRow(projekte, "Garten"); + await expect(renamed.locator("[data-project-icon]")).toHaveAttribute("data-project-icon", "leaf"); + await expect(renamed.locator("[data-project-icon]")).toHaveAttribute( + "data-project-color", + "green", + ); + await page.reload({ waitUntil: "domcontentloaded" }); + await expect(renamed.locator("[data-project-icon]")).toHaveAttribute( + "data-project-icon", + "leaf", + { timeout: 30_000 }, + ); + + // The coordinator hands work to a specialist; the finished thread shows in its overview. + await waitForChatComposer(page); + await rpc(page, "bots/create", { + name: "Gardener", + title: "", + description: "", + instructions: "", + notifyOnFinish: true, + }); + const composer = page.getByRole("combobox", { name: "Message" }); + await composer.fill("delegate to the bot named Gardener: plan the spring beds"); + await composer.press("Enter"); + const card = page.getByTestId("transcript").getByTestId("delegated-task-card"); + await expect(card).toHaveAttribute("data-status", "done", { timeout: 60_000 }); + + const overview = page.getByTestId("project-overview"); + const done = overview.getByRole("button", { name: /Done/ }); + await expect(done).toBeVisible({ timeout: 30_000 }); + await done.click(); + await expect(overview.getByText("Gardener").first()).toBeVisible(); + await captureScreenshot(page, testInfo, "project-overview"); +}); diff --git a/apps/web/e2e/project-main-agent.spec.ts b/apps/web/e2e/project-main-agent.spec.ts index 7d0fedd..5d7ca20 100644 --- a/apps/web/e2e/project-main-agent.spec.ts +++ b/apps/web/e2e/project-main-agent.spec.ts @@ -1,9 +1,12 @@ -import { expect, type Locator, test } from "@playwright/test"; -import { captureScreenshot, completeOnboarding, rpc, signup, waitForChatComposer } from "./helpers"; - -function projectSpace(projekte: Locator, name: string) { - return projekte.locator('[data-sidebar-group^="project-space:"]').filter({ hasText: name }); -} +import { expect, test } from "@playwright/test"; +import { + captureScreenshot, + completeOnboarding, + projectRow, + rpc, + signup, + waitForChatComposer, +} from "./helpers"; test("a new project opens straight into the conversation with its own main agent", async ({ page, @@ -26,9 +29,7 @@ test("a new project opens straight into the conversation with its own main agent await waitForChatComposer(page); // The project is one conversation, led by a main agent of its own. - const projectEntry = projectSpace(projekte, "Launch").getByRole("button", { - name: "Chief, main conversation", - }); + const projectEntry = projectRow(projekte, "Launch"); await expect(projectEntry).toBeVisible({ timeout: 30_000 }); await expect(projekte.getByRole("button", { name: "Start", exact: true })).toHaveCount(0); const groupId = new URL(page.url()).pathname.split("/").at(-1)!; diff --git a/apps/web/e2e/spaces.spec.ts b/apps/web/e2e/spaces.spec.ts index e540978..628a1e2 100644 --- a/apps/web/e2e/spaces.spec.ts +++ b/apps/web/e2e/spaces.spec.ts @@ -1,21 +1,15 @@ -import { expect, type Locator, test } from "@playwright/test"; +import { expect, test } from "@playwright/test"; import { captureScreenshot, completeOnboarding, openNewSpace, + projectRow, routePatchRpc, rpc, signup, unterhaltungenOnly, } from "./helpers"; -function projectSpace(projekte: Locator, name: string) { - return projekte.locator('[data-sidebar-group^="project-space:"]').filter({ - hasText: name, - exact: true, - }); -} - test("Projects + opens new project dialog instead of group form", async ({ page }, testInfo) => { const stamp = Date.now(); await signup(page, `proj-create-${stamp}@rakazo.test`, "password12", "Project Create"); @@ -42,11 +36,7 @@ test("Projects + opens new project dialog instead of group form", async ({ page await expect(unterhaltungenOnly(page).getByText("Customer project", { exact: true })).toHaveCount( 0, ); - await expect( - projectSpace(projekte, "Customer project").getByRole("button", { - name: "Chief, main conversation", - }), - ).toBeVisible(); + await expect(projectRow(projekte, "Customer project")).toBeVisible(); await expect(page.getByTestId("project-start-main")).toHaveCount(0); await captureScreenshot(page, testInfo, "new-project-main-conversation"); }); @@ -118,14 +108,14 @@ test("spaces stay invisible by default and chat creation requires approval", asy ); await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); - const supportSpace = projekte.locator('[data-sidebar-group^="project-space:"]').filter({ - hasText: "Customer support", - }); - const supportSpaceGroup = await supportSpace.getAttribute("data-sidebar-group"); - const supportSpaceId = supportSpaceGroup?.match(/^project-space:([^:]+)/)?.[1]; + const supportSpace = projectRow(projekte, "Customer support"); + const supportSpaceId = (await supportSpace.getAttribute("data-testid"))?.replace( + /^project-row-/, + "", + ); expect(supportSpaceId).toBeTruthy(); - // A project created from chat comes with its own main agent, reached through its conversation. - await supportSpace.getByRole("button", { name: "Chief, main conversation" }).click(); + // A project created from chat comes with its own main agent, reached through its row. + await supportSpace.click(); await page.waitForURL(/\/app\/g\/[^/]+$/); await expect .poll(() => page.evaluate(() => window.localStorage.getItem("rakazo:space-id"))) @@ -134,7 +124,7 @@ test("spaces stay invisible by default and chat creation requires approval", asy // The home main conversation stays the first entry while another space is open. await expect(projekte.getByText("Customer support", { exact: true })).toBeVisible(); await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); - await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(1); + await expect(projekte.getByRole("button", { name: /^Chief/ })).toHaveCount(0); await expect(projekte.getByRole("button", { name: "Start", exact: true })).toHaveCount(0); await captureScreenshot(page, testInfo, "spaces-sidebar"); @@ -168,9 +158,7 @@ test("a new space opens its main conversation and can be deleted from its menu", const projekte = page.locator(".sidebar-projekte"); await expect(projekte.getByText("Temporary", { exact: true })).toBeVisible(); await expect(unterhaltungenOnly(page).getByRole("button", { name: /^Chief/ })).toHaveCount(1); - await expect( - projectSpace(projekte, "Temporary").getByRole("button", { name: "Chief, main conversation" }), - ).toBeVisible(); + await expect(projectRow(projekte, "Temporary")).toBeVisible(); await expect(projekte.getByRole("button", { name: "Start", exact: true })).toHaveCount(0); expect(await rpc>(page, "bots/list", {})).toEqual([ expect.objectContaining({ kind: "main" }), @@ -178,9 +166,8 @@ test("a new space opens its main conversation and can be deleted from its menu", await captureScreenshot(page, testInfo, "new-space-main-conversation"); // Deletion is discoverable through the visible Space actions menu. - await projectSpace(projekte, "Temporary") - .getByRole("button", { name: "Actions for Temporary" }) - .click(); + await projectRow(projekte, "Temporary").hover(); + await projekte.getByRole("button", { name: "Actions for Temporary" }).click(); await captureScreenshot(page, testInfo, "space-actions-menu"); await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible(); await page.getByRole("menuitem", { name: "Delete space" }).click(); @@ -242,11 +229,10 @@ test("deleting the last bot in a space stays in the app after first use", async await expect(page).not.toHaveURL(/\/onboarding/); // The project's main conversation stays, so the project opens there. await page.waitForURL(/\/app\/g\/[^/]+$/, { timeout: 30_000 }); - await expect( - projectSpace(projekte, "Side").getByRole("button", { name: "Chief, main conversation" }), - ).toBeVisible(); + await expect(projectRow(projekte, "Side")).toBeVisible(); - await projectSpace(projekte, "Side").getByRole("button", { name: "Actions for Side" }).click(); + await projectRow(projekte, "Side").hover(); + await projekte.getByRole("button", { name: "Actions for Side" }).click(); await expect(page.getByRole("menuitem", { name: "Delete space" })).toBeVisible(); await page.getByRole("menuitem", { name: "Delete space" }).click(); await page diff --git a/apps/web/src/components/SortableGroup.tsx b/apps/web/src/components/SortableGroup.tsx index 97ece43..2d44424 100644 --- a/apps/web/src/components/SortableGroup.tsx +++ b/apps/web/src/components/SortableGroup.tsx @@ -1,62 +1,18 @@ /** - * Sortable wrapper for a project (space) sidebar group card. - * Drag listeners attach only to the grip handle via `useSortableGroupHandle`. + * Sortable wrapper for a project in the sidebar. The whole row drags: a mouse after a short + * move, a finger after a short hold, so a plain click or scroll still works. */ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { useLingui } from "@lingui/react/macro"; -import { createContext, memo, useContext, useMemo } from "react"; +import { memo, useMemo } from "react"; interface SortableGroupProps { group: { spaceId: string; key: string; title?: string | null }; children: React.ReactNode; } -type SortableGroupHandle = { - setActivatorNodeRef: (element: HTMLElement | null) => void; - listeners: ReturnType["listeners"]; - attributes: ReturnType["attributes"]; -}; - -const SortableGroupHandleContext = createContext(null); - -export function useSortableGroupHandle(): SortableGroupHandle | null { - return useContext(SortableGroupHandleContext); -} - -export const ProjectDragGrip = memo(function ProjectDragGrip({ - className, - children, -}: { - className?: string; - children: React.ReactNode; -}) { - const { t } = useLingui(); - const handle = useSortableGroupHandle(); - return ( - - ); -}); - export const SortableGroup = memo(function SortableGroup({ group, children }: SortableGroupProps) { - const { - attributes, - listeners, - setNodeRef, - setActivatorNodeRef, - transform, - transition, - isDragging, - } = useSortable({ + const { listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: group.spaceId, }); @@ -70,16 +26,9 @@ export const SortableGroup = memo(function SortableGroup({ group, children }: So [transform, transition, isDragging], ); - const handle = useMemo( - () => ({ setActivatorNodeRef, listeners, attributes }), - [setActivatorNodeRef, listeners, attributes], - ); - return ( - -
- {children} -
-
+
+ {children} +
); }); diff --git a/apps/web/src/components/project-icon.tsx b/apps/web/src/components/project-icon.tsx new file mode 100644 index 0000000..dee2f95 --- /dev/null +++ b/apps/web/src/components/project-icon.tsx @@ -0,0 +1,167 @@ +import type { ProjectIconId } from "@rakazo/core"; +import { projectLook } from "@rakazo/core"; +import type { LucideIcon } from "lucide-react"; +import { + Banknote, + Bike, + BookOpen, + Box, + Briefcase, + BriefcaseMedical, + Calendar, + Camera, + Car, + ChartColumn, + Clock, + Cloud, + Code, + Coffee, + Dumbbell, + FileText, + Film, + Flag, + Flame, + FlaskConical, + Folder, + Gamepad2, + Gift, + Globe, + GraduationCap, + Hammer, + Heart, + House, + Image, + Infinity as InfinityIcon, + KeyRound, + Layers, + Leaf, + Library, + Lightbulb, + Lock, + Mail, + Map as MapIcon, + MapPin, + MessageCircle, + Mic, + Monitor, + Moon, + Music, + Newspaper, + Paintbrush, + PawPrint, + Pencil, + Plane, + Server, + Settings, + Shield, + ShoppingBag, + ShoppingCart, + Smartphone, + Sparkles, + SquareTerminal, + Star, + Sun, + Target, + TrendingUp, + Trophy, + User, + Users, + Utensils, + Wrench, + Zap, +} from "lucide-react"; + +export const PROJECT_ICON_COMPONENTS: Record = { + folder: Folder, + briefcase: Briefcase, + house: House, + heart: Heart, + star: Star, + book: BookOpen, + code: Code, + terminal: SquareTerminal, + globe: Globe, + camera: Camera, + film: Film, + music: Music, + mic: Mic, + image: Image, + paintbrush: Paintbrush, + pencil: Pencil, + cart: ShoppingCart, + bag: ShoppingBag, + money: Banknote, + chart: ChartColumn, + trending: TrendingUp, + calendar: Calendar, + clock: Clock, + mail: Mail, + chat: MessageCircle, + people: Users, + person: User, + school: GraduationCap, + flask: FlaskConical, + bulb: Lightbulb, + bolt: Zap, + flame: Flame, + leaf: Leaf, + paw: PawPrint, + airplane: Plane, + car: Car, + bicycle: Bike, + map: MapIcon, + pin: MapPin, + fitness: Dumbbell, + medical: BriefcaseMedical, + food: Utensils, + coffee: Coffee, + game: Gamepad2, + gift: Gift, + trophy: Trophy, + flag: Flag, + key: KeyRound, + lock: Lock, + shield: Shield, + gear: Settings, + hammer: Hammer, + tools: Wrench, + cloud: Cloud, + sun: Sun, + moon: Moon, + box: Box, + layers: Layers, + server: Server, + phone: Smartphone, + desktop: Monitor, + news: Newspaper, + document: FileText, + library: Library, + sparkles: Sparkles, + target: Target, + infinity: InfinityIcon, +}; + +/** A project's icon on its color, as a rounded tile. */ +export function ProjectIcon({ + icon, + color, + size = 32, +}: { + icon?: string | null; + color?: string | null; + size?: number; +}) { + const look = projectLook({ icon, color }); + const Glyph = PROJECT_ICON_COMPONENTS[look.icon.id]; + return ( + + ); +} diff --git a/apps/web/src/lib/run-activity.ts b/apps/web/src/lib/run-activity.ts new file mode 100644 index 0000000..eeb58cb --- /dev/null +++ b/apps/web/src/lib/run-activity.ts @@ -0,0 +1,59 @@ +import type { RunActivityRow } from "@rakazo/contracts"; +import { useSyncExternalStore } from "react"; +import { rpc } from "./rpc"; + +export type RunActivity = { + active: RunActivityRow[]; + recent: RunActivityRow[]; + loaded: boolean; +}; + +const POLL_MS = 15_000; +let snapshot: RunActivity = { active: [], recent: [], loaded: false }; +const listeners = new Set<() => void>(); +let timer: number | undefined; +let polling = false; + +async function poll() { + try { + const [active, recent] = await Promise.all([ + rpc.runs.list({ filter: "active" }), + rpc.runs.list({ filter: "recent" }), + ]); + snapshot = { active: active.runs, recent: recent.runs, loaded: true }; + } catch { + // Keep the last good snapshot on transient failures. + if (!snapshot.loaded) snapshot = { ...snapshot, loaded: true }; + } + for (const listener of listeners) listener(); + // Schedule the next poll after this one settles, and only while someone is listening. + timer = listeners.size > 0 ? window.setTimeout(() => void poll(), POLL_MS) : undefined; + polling = timer !== undefined; +} + +function subscribe(listener: () => void) { + listeners.add(listener); + if (!polling) { + polling = true; + void poll(); + } + return () => { + listeners.delete(listener); + if (listeners.size === 0 && timer !== undefined) { + window.clearTimeout(timer); + timer = undefined; + polling = false; + } + }; +} + +/** The user's active and recently finished runs in every space, polled once for all readers. */ +export function useRunActivity(): RunActivity { + return useSyncExternalStore(subscribe, () => snapshot); +} + +/** Ask for a fresh snapshot now, e.g. after the user answered a waiting thread. */ +export function refreshRunActivity() { + if (timer !== undefined) window.clearTimeout(timer); + void poll(); +} diff --git a/apps/web/src/locales/de/messages.po b/apps/web/src/locales/de/messages.po index 46985b7..5680578 100644 --- a/apps/web/src/locales/de/messages.po +++ b/apps/web/src/locales/de/messages.po @@ -352,6 +352,10 @@ msgstr "{name} ergänzt: {line}" msgid "{name} stopped" msgstr "{name} wurde gestoppt" +#: src/pages/shell/project-row.tsx +msgid "{name}, {needsYou} need you" +msgstr "{name}, {needsYou} brauchen dich" + #: src/pages/shell/main-conversation-row.tsx msgid "{name}, main conversation" msgstr "{name}, Hauptunterhaltung" @@ -461,6 +465,10 @@ msgstr "Aktionsbestätigungen" msgid "Actions for {0}" msgstr "Aktionen für {0}" +#: src/pages/shell/project-row.tsx +msgid "Actions for {name}" +msgstr "Aktionen für {name}" + #: src/pages/Shell.tsx msgid "Actions for space" msgstr "Aktionen für Space" @@ -1067,6 +1075,7 @@ msgstr "Server nicht erreichbar." #: src/pages/PluginsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Cancel" msgstr "Abbrechen" @@ -1376,6 +1385,7 @@ msgid "Collapse {0}" msgstr "{0} einklappen" #: src/components/OoAtelier.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Color" msgstr "Farbe" @@ -1829,6 +1839,7 @@ msgstr "Routine konnte nicht ausgeführt werden" #: src/pages/KnowledgeSection.tsx #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Could not save" msgstr "Speichern fehlgeschlagen" @@ -2056,6 +2067,7 @@ msgid "Customer support" msgstr "Kundensupport" #: src/components/onboarding/OnboardingOoDesigner.tsx +#: src/pages/Shell.tsx msgid "Customize" msgstr "Anpassen" @@ -2280,6 +2292,7 @@ msgstr "Kein Stress — wir sind jetzt da." #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Done" msgstr "Fertig" @@ -2325,10 +2338,6 @@ msgstr "Skill-Entwurf" msgid "Drafting the spec…" msgstr "Entwerfe die Spezifikation…" -#: src/components/SortableGroup.tsx -msgid "Drag to reorder" -msgstr "Ziehen zum Umsortieren" - #: src/components/OoAtelier.tsx msgid "Drag, drop, or paste an image" msgstr "Bild ziehen, ablegen oder einfügen" @@ -2897,6 +2906,10 @@ msgstr "Ich bin fertig" msgid "I'm listening — what's the priority?" msgstr "Ich höre zu — was hat Priorität?" +#: src/pages/shell/project-look-dialog.tsx +msgid "Icon" +msgstr "Icon" + #: src/components/UserProfileSettings.tsx msgid "Identity" msgstr "Identität" @@ -3483,6 +3496,7 @@ msgstr "In Ordner verschieben" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Name" msgstr "Name" @@ -3520,6 +3534,7 @@ msgstr "Braucht dich" #: src/pages/ActivityList.tsx #: src/pages/HeuteBrief.tsx #: src/pages/shell/fullscreen-computer-overlay.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/transcript.tsx msgid "Needs you" msgstr "Braucht dich" @@ -4483,6 +4498,7 @@ msgstr "läuft" #: src/components/ThreadCard.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Running" msgstr "Läuft" @@ -4518,6 +4534,7 @@ msgstr "Läuft, wenn dieser Bot eine verifizierte Nachricht von diesem Anbieter #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Save" msgstr "Speichern" diff --git a/apps/web/src/locales/en/messages.po b/apps/web/src/locales/en/messages.po index 0236ddf..3b434df 100644 --- a/apps/web/src/locales/en/messages.po +++ b/apps/web/src/locales/en/messages.po @@ -352,6 +352,10 @@ msgstr "{name} riffed: {line}" msgid "{name} stopped" msgstr "{name} stopped" +#: src/pages/shell/project-row.tsx +msgid "{name}, {needsYou} need you" +msgstr "{name}, {needsYou} need you" + #: src/pages/shell/main-conversation-row.tsx msgid "{name}, main conversation" msgstr "{name}, main conversation" @@ -461,6 +465,10 @@ msgstr "Action confirmations" msgid "Actions for {0}" msgstr "Actions for {0}" +#: src/pages/shell/project-row.tsx +msgid "Actions for {name}" +msgstr "Actions for {name}" + #: src/pages/Shell.tsx msgid "Actions for space" msgstr "Actions for space" @@ -1067,6 +1075,7 @@ msgstr "Can't reach the server." #: src/pages/PluginsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Cancel" msgstr "Cancel" @@ -1376,6 +1385,7 @@ msgid "Collapse {0}" msgstr "Collapse {0}" #: src/components/OoAtelier.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Color" msgstr "Color" @@ -1829,6 +1839,7 @@ msgstr "Could not run routine" #: src/pages/KnowledgeSection.tsx #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Could not save" msgstr "Could not save" @@ -2056,6 +2067,7 @@ msgid "Customer support" msgstr "Customer support" #: src/components/onboarding/OnboardingOoDesigner.tsx +#: src/pages/Shell.tsx msgid "Customize" msgstr "Customize" @@ -2280,6 +2292,7 @@ msgstr "Don't stress — we're here now." #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Done" msgstr "Done" @@ -2325,10 +2338,6 @@ msgstr "Draft skill" msgid "Drafting the spec…" msgstr "Drafting the spec…" -#: src/components/SortableGroup.tsx -msgid "Drag to reorder" -msgstr "Drag to reorder" - #: src/components/OoAtelier.tsx msgid "Drag, drop, or paste an image" msgstr "Drag, drop, or paste an image" @@ -2897,6 +2906,10 @@ msgstr "I’m done" msgid "I'm listening — what's the priority?" msgstr "I'm listening — what's the priority?" +#: src/pages/shell/project-look-dialog.tsx +msgid "Icon" +msgstr "Icon" + #: src/components/UserProfileSettings.tsx msgid "Identity" msgstr "Identity" @@ -3483,6 +3496,7 @@ msgstr "Move to folder" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Name" msgstr "Name" @@ -3520,6 +3534,7 @@ msgstr "Needs you" #: src/pages/ActivityList.tsx #: src/pages/HeuteBrief.tsx #: src/pages/shell/fullscreen-computer-overlay.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/transcript.tsx msgid "Needs you" msgstr "Needs you" @@ -4483,6 +4498,7 @@ msgstr "running" #: src/components/ThreadCard.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Running" msgstr "Running" @@ -4518,6 +4534,7 @@ msgstr "Runs when this bot receives a verified message from this provider." #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Save" msgstr "Save" diff --git a/apps/web/src/locales/es/messages.po b/apps/web/src/locales/es/messages.po index 0288d11..c09f376 100644 --- a/apps/web/src/locales/es/messages.po +++ b/apps/web/src/locales/es/messages.po @@ -352,6 +352,10 @@ msgstr "" msgid "{name} stopped" msgstr "{name} se detuvo" +#: src/pages/shell/project-row.tsx +msgid "{name}, {needsYou} need you" +msgstr "" + #: src/pages/shell/main-conversation-row.tsx msgid "{name}, main conversation" msgstr "" @@ -461,6 +465,10 @@ msgstr "Confirmaciones de acción" msgid "Actions for {0}" msgstr "Acciones para {0}" +#: src/pages/shell/project-row.tsx +msgid "Actions for {name}" +msgstr "" + #: src/pages/Shell.tsx msgid "Actions for space" msgstr "" @@ -1067,6 +1075,7 @@ msgstr "No se puede alcanzar el servidor." #: src/pages/PluginsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Cancel" msgstr "Cancelar" @@ -1376,6 +1385,7 @@ msgid "Collapse {0}" msgstr "Contraer {0}" #: src/components/OoAtelier.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Color" msgstr "Color" @@ -1829,6 +1839,7 @@ msgstr "No se pudo ejecutar la rutina" #: src/pages/KnowledgeSection.tsx #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Could not save" msgstr "No se pudo guardar" @@ -2056,6 +2067,7 @@ msgid "Customer support" msgstr "Atención al cliente" #: src/components/onboarding/OnboardingOoDesigner.tsx +#: src/pages/Shell.tsx msgid "Customize" msgstr "" @@ -2280,6 +2292,7 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Done" msgstr "Listo" @@ -2325,10 +2338,6 @@ msgstr "Habilidad en borrador" msgid "Drafting the spec…" msgstr "" -#: src/components/SortableGroup.tsx -msgid "Drag to reorder" -msgstr "" - #: src/components/OoAtelier.tsx msgid "Drag, drop, or paste an image" msgstr "Arrastra, suelta o pega una imagen" @@ -2897,6 +2906,10 @@ msgstr "Listo" msgid "I'm listening — what's the priority?" msgstr "" +#: src/pages/shell/project-look-dialog.tsx +msgid "Icon" +msgstr "" + #: src/components/UserProfileSettings.tsx msgid "Identity" msgstr "" @@ -3483,6 +3496,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Name" msgstr "Nombre" @@ -3520,6 +3534,7 @@ msgstr "" #: src/pages/ActivityList.tsx #: src/pages/HeuteBrief.tsx #: src/pages/shell/fullscreen-computer-overlay.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/transcript.tsx msgid "Needs you" msgstr "" @@ -4483,6 +4498,7 @@ msgstr "en ejecución" #: src/components/ThreadCard.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Running" msgstr "En ejecución" @@ -4518,6 +4534,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Save" msgstr "Guardar" diff --git a/apps/web/src/locales/hi/messages.po b/apps/web/src/locales/hi/messages.po index 4ab9c48..00ec7b7 100644 --- a/apps/web/src/locales/hi/messages.po +++ b/apps/web/src/locales/hi/messages.po @@ -352,6 +352,10 @@ msgstr "" msgid "{name} stopped" msgstr "{name} रोक दिया गया" +#: src/pages/shell/project-row.tsx +msgid "{name}, {needsYou} need you" +msgstr "" + #: src/pages/shell/main-conversation-row.tsx msgid "{name}, main conversation" msgstr "" @@ -461,6 +465,10 @@ msgstr "कार्रवाई की पुष्टि" msgid "Actions for {0}" msgstr "{0} के लिए कार्रवाइयां" +#: src/pages/shell/project-row.tsx +msgid "Actions for {name}" +msgstr "" + #: src/pages/Shell.tsx msgid "Actions for space" msgstr "" @@ -1067,6 +1075,7 @@ msgstr "सर्वर तक नहीं पहुंचा जा सका #: src/pages/PluginsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Cancel" msgstr "रद्द करें" @@ -1376,6 +1385,7 @@ msgid "Collapse {0}" msgstr "" #: src/components/OoAtelier.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Color" msgstr "रंग" @@ -1829,6 +1839,7 @@ msgstr "" #: src/pages/KnowledgeSection.tsx #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Could not save" msgstr "सहेजा नहीं जा सका" @@ -2056,6 +2067,7 @@ msgid "Customer support" msgstr "" #: src/components/onboarding/OnboardingOoDesigner.tsx +#: src/pages/Shell.tsx msgid "Customize" msgstr "" @@ -2280,6 +2292,7 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Done" msgstr "पूर्ण" @@ -2325,10 +2338,6 @@ msgstr "ड्राफ़्ट स्किल" msgid "Drafting the spec…" msgstr "" -#: src/components/SortableGroup.tsx -msgid "Drag to reorder" -msgstr "" - #: src/components/OoAtelier.tsx msgid "Drag, drop, or paste an image" msgstr "छवि खींचें, छोड़ें या पेस्ट करें" @@ -2897,6 +2906,10 @@ msgstr "मेरा काम हो गया" msgid "I'm listening — what's the priority?" msgstr "" +#: src/pages/shell/project-look-dialog.tsx +msgid "Icon" +msgstr "" + #: src/components/UserProfileSettings.tsx msgid "Identity" msgstr "" @@ -3483,6 +3496,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Name" msgstr "नाम" @@ -3520,6 +3534,7 @@ msgstr "" #: src/pages/ActivityList.tsx #: src/pages/HeuteBrief.tsx #: src/pages/shell/fullscreen-computer-overlay.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/transcript.tsx msgid "Needs you" msgstr "" @@ -4483,6 +4498,7 @@ msgstr "चल रहा है" #: src/components/ThreadCard.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Running" msgstr "चल रहा है" @@ -4518,6 +4534,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Save" msgstr "सहेजें" diff --git a/apps/web/src/locales/ko/messages.po b/apps/web/src/locales/ko/messages.po index ca2b666..7d3d41e 100644 --- a/apps/web/src/locales/ko/messages.po +++ b/apps/web/src/locales/ko/messages.po @@ -352,6 +352,10 @@ msgstr "" msgid "{name} stopped" msgstr "{name} 중지됨" +#: src/pages/shell/project-row.tsx +msgid "{name}, {needsYou} need you" +msgstr "" + #: src/pages/shell/main-conversation-row.tsx msgid "{name}, main conversation" msgstr "" @@ -461,6 +465,10 @@ msgstr "작업 실행 전 확인" msgid "Actions for {0}" msgstr "{0} 작업 메뉴" +#: src/pages/shell/project-row.tsx +msgid "Actions for {name}" +msgstr "" + #: src/pages/Shell.tsx msgid "Actions for space" msgstr "" @@ -1067,6 +1075,7 @@ msgstr "서버에 연결할 수 없습니다." #: src/pages/PluginsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Cancel" msgstr "취소" @@ -1376,6 +1385,7 @@ msgid "Collapse {0}" msgstr "{0} 접기" #: src/components/OoAtelier.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Color" msgstr "색상" @@ -1829,6 +1839,7 @@ msgstr "루틴을 실행할 수 없습니다" #: src/pages/KnowledgeSection.tsx #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Could not save" msgstr "저장하지 못했습니다." @@ -2056,6 +2067,7 @@ msgid "Customer support" msgstr "고객 지원" #: src/components/onboarding/OnboardingOoDesigner.tsx +#: src/pages/Shell.tsx msgid "Customize" msgstr "" @@ -2280,6 +2292,7 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Done" msgstr "완료" @@ -2325,10 +2338,6 @@ msgstr "작업 기술 초안" msgid "Drafting the spec…" msgstr "" -#: src/components/SortableGroup.tsx -msgid "Drag to reorder" -msgstr "" - #: src/components/OoAtelier.tsx msgid "Drag, drop, or paste an image" msgstr "이미지를 끌어다 놓거나 붙여넣으세요" @@ -2897,6 +2906,10 @@ msgstr "조작 끝내기" msgid "I'm listening — what's the priority?" msgstr "" +#: src/pages/shell/project-look-dialog.tsx +msgid "Icon" +msgstr "" + #: src/components/UserProfileSettings.tsx msgid "Identity" msgstr "" @@ -3483,6 +3496,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Name" msgstr "이름" @@ -3520,6 +3534,7 @@ msgstr "" #: src/pages/ActivityList.tsx #: src/pages/HeuteBrief.tsx #: src/pages/shell/fullscreen-computer-overlay.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/transcript.tsx msgid "Needs you" msgstr "" @@ -4483,6 +4498,7 @@ msgstr "실행 중" #: src/components/ThreadCard.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Running" msgstr "실행 중" @@ -4518,6 +4534,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Save" msgstr "저장" diff --git a/apps/web/src/locales/pt-BR/messages.po b/apps/web/src/locales/pt-BR/messages.po index 8350ce9..dcccea2 100644 --- a/apps/web/src/locales/pt-BR/messages.po +++ b/apps/web/src/locales/pt-BR/messages.po @@ -352,6 +352,10 @@ msgstr "" msgid "{name} stopped" msgstr "{name} foi interrompido" +#: src/pages/shell/project-row.tsx +msgid "{name}, {needsYou} need you" +msgstr "" + #: src/pages/shell/main-conversation-row.tsx msgid "{name}, main conversation" msgstr "" @@ -461,6 +465,10 @@ msgstr "Confirmações de ação" msgid "Actions for {0}" msgstr "Ações para {0}" +#: src/pages/shell/project-row.tsx +msgid "Actions for {name}" +msgstr "" + #: src/pages/Shell.tsx msgid "Actions for space" msgstr "" @@ -1067,6 +1075,7 @@ msgstr "Não é possível acessar o servidor." #: src/pages/PluginsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Cancel" msgstr "Cancelar" @@ -1376,6 +1385,7 @@ msgid "Collapse {0}" msgstr "" #: src/components/OoAtelier.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Color" msgstr "Cor" @@ -1829,6 +1839,7 @@ msgstr "" #: src/pages/KnowledgeSection.tsx #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Could not save" msgstr "Não foi possível salvar" @@ -2056,6 +2067,7 @@ msgid "Customer support" msgstr "" #: src/components/onboarding/OnboardingOoDesigner.tsx +#: src/pages/Shell.tsx msgid "Customize" msgstr "" @@ -2280,6 +2292,7 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Done" msgstr "Concluído" @@ -2325,10 +2338,6 @@ msgstr "Habilidade de rascunho" msgid "Drafting the spec…" msgstr "" -#: src/components/SortableGroup.tsx -msgid "Drag to reorder" -msgstr "" - #: src/components/OoAtelier.tsx msgid "Drag, drop, or paste an image" msgstr "Arraste, solte ou cole uma imagem" @@ -2897,6 +2906,10 @@ msgstr "Eu terminei." msgid "I'm listening — what's the priority?" msgstr "" +#: src/pages/shell/project-look-dialog.tsx +msgid "Icon" +msgstr "" + #: src/components/UserProfileSettings.tsx msgid "Identity" msgstr "" @@ -3483,6 +3496,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Name" msgstr "Nome" @@ -3520,6 +3534,7 @@ msgstr "" #: src/pages/ActivityList.tsx #: src/pages/HeuteBrief.tsx #: src/pages/shell/fullscreen-computer-overlay.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/transcript.tsx msgid "Needs you" msgstr "" @@ -4483,6 +4498,7 @@ msgstr "em execução" #: src/components/ThreadCard.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Running" msgstr "Em execução" @@ -4518,6 +4534,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Save" msgstr "Salvar" diff --git a/apps/web/src/locales/ru/messages.po b/apps/web/src/locales/ru/messages.po index 695c2e0..12bfba2 100644 --- a/apps/web/src/locales/ru/messages.po +++ b/apps/web/src/locales/ru/messages.po @@ -352,6 +352,10 @@ msgstr "" msgid "{name} stopped" msgstr "{name}: остановлено" +#: src/pages/shell/project-row.tsx +msgid "{name}, {needsYou} need you" +msgstr "" + #: src/pages/shell/main-conversation-row.tsx msgid "{name}, main conversation" msgstr "" @@ -461,6 +465,10 @@ msgstr "Подтверждения действий" msgid "Actions for {0}" msgstr "Действия для {0}" +#: src/pages/shell/project-row.tsx +msgid "Actions for {name}" +msgstr "" + #: src/pages/Shell.tsx msgid "Actions for space" msgstr "Действия для пространства" @@ -1067,6 +1075,7 @@ msgstr "Не удаётся связаться с сервером." #: src/pages/PluginsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Cancel" msgstr "Отмена" @@ -1376,6 +1385,7 @@ msgid "Collapse {0}" msgstr "Свернуть {0}" #: src/components/OoAtelier.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Color" msgstr "Цвет" @@ -1829,6 +1839,7 @@ msgstr "Не удалось запустить задачу" #: src/pages/KnowledgeSection.tsx #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Could not save" msgstr "Не удалось сохранить" @@ -2056,6 +2067,7 @@ msgid "Customer support" msgstr "Поддержка клиентов" #: src/components/onboarding/OnboardingOoDesigner.tsx +#: src/pages/Shell.tsx msgid "Customize" msgstr "" @@ -2280,6 +2292,7 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Done" msgstr "Готово" @@ -2325,10 +2338,6 @@ msgstr "Черновик навыка" msgid "Drafting the spec…" msgstr "" -#: src/components/SortableGroup.tsx -msgid "Drag to reorder" -msgstr "" - #: src/components/OoAtelier.tsx msgid "Drag, drop, or paste an image" msgstr "Перетащите или вставьте изображение" @@ -2897,6 +2906,10 @@ msgstr "Готово" msgid "I'm listening — what's the priority?" msgstr "" +#: src/pages/shell/project-look-dialog.tsx +msgid "Icon" +msgstr "" + #: src/components/UserProfileSettings.tsx msgid "Identity" msgstr "" @@ -3483,6 +3496,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Name" msgstr "Имя" @@ -3520,6 +3534,7 @@ msgstr "Требуется ваше участие" #: src/pages/ActivityList.tsx #: src/pages/HeuteBrief.tsx #: src/pages/shell/fullscreen-computer-overlay.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/transcript.tsx msgid "Needs you" msgstr "Требуется ваше участие" @@ -4483,6 +4498,7 @@ msgstr "выполняется" #: src/components/ThreadCard.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Running" msgstr "Выполняется" @@ -4518,6 +4534,7 @@ msgstr "Запускается, когда этот бот получает пр #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Save" msgstr "Сохранить" diff --git a/apps/web/src/locales/tr/messages.po b/apps/web/src/locales/tr/messages.po index efa9ae6..8cd0d65 100644 --- a/apps/web/src/locales/tr/messages.po +++ b/apps/web/src/locales/tr/messages.po @@ -352,6 +352,10 @@ msgstr "" msgid "{name} stopped" msgstr "{name} durduruldu" +#: src/pages/shell/project-row.tsx +msgid "{name}, {needsYou} need you" +msgstr "" + #: src/pages/shell/main-conversation-row.tsx msgid "{name}, main conversation" msgstr "" @@ -461,6 +465,10 @@ msgstr "İşlem onayları" msgid "Actions for {0}" msgstr "{0} için işlemler" +#: src/pages/shell/project-row.tsx +msgid "Actions for {name}" +msgstr "" + #: src/pages/Shell.tsx msgid "Actions for space" msgstr "" @@ -1067,6 +1075,7 @@ msgstr "Sunucuya ulaşılamıyor." #: src/pages/PluginsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Cancel" msgstr "İptal" @@ -1376,6 +1385,7 @@ msgid "Collapse {0}" msgstr "" #: src/components/OoAtelier.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Color" msgstr "Renk" @@ -1829,6 +1839,7 @@ msgstr "" #: src/pages/KnowledgeSection.tsx #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Could not save" msgstr "Kaydedilemedi" @@ -2056,6 +2067,7 @@ msgid "Customer support" msgstr "" #: src/components/onboarding/OnboardingOoDesigner.tsx +#: src/pages/Shell.tsx msgid "Customize" msgstr "" @@ -2280,6 +2292,7 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Done" msgstr "Bitti" @@ -2325,10 +2338,6 @@ msgstr "Taslak beceri" msgid "Drafting the spec…" msgstr "" -#: src/components/SortableGroup.tsx -msgid "Drag to reorder" -msgstr "" - #: src/components/OoAtelier.tsx msgid "Drag, drop, or paste an image" msgstr "Görüntüyü sürükleyip bırakın veya yapıştırın" @@ -2897,6 +2906,10 @@ msgstr "Bitirdim" msgid "I'm listening — what's the priority?" msgstr "" +#: src/pages/shell/project-look-dialog.tsx +msgid "Icon" +msgstr "" + #: src/components/UserProfileSettings.tsx msgid "Identity" msgstr "" @@ -3483,6 +3496,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Name" msgstr "Ad" @@ -3520,6 +3534,7 @@ msgstr "" #: src/pages/ActivityList.tsx #: src/pages/HeuteBrief.tsx #: src/pages/shell/fullscreen-computer-overlay.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/transcript.tsx msgid "Needs you" msgstr "" @@ -4483,6 +4498,7 @@ msgstr "çalışıyor" #: src/components/ThreadCard.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Running" msgstr "Çalışıyor" @@ -4518,6 +4534,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Save" msgstr "Kaydet" diff --git a/apps/web/src/locales/zh-CN/messages.po b/apps/web/src/locales/zh-CN/messages.po index c47f86f..7d442f0 100644 --- a/apps/web/src/locales/zh-CN/messages.po +++ b/apps/web/src/locales/zh-CN/messages.po @@ -352,6 +352,10 @@ msgstr "" msgid "{name} stopped" msgstr "{name} 已停止" +#: src/pages/shell/project-row.tsx +msgid "{name}, {needsYou} need you" +msgstr "" + #: src/pages/shell/main-conversation-row.tsx msgid "{name}, main conversation" msgstr "" @@ -461,6 +465,10 @@ msgstr "操作确认" msgid "Actions for {0}" msgstr "{0} 的操作" +#: src/pages/shell/project-row.tsx +msgid "Actions for {name}" +msgstr "" + #: src/pages/Shell.tsx msgid "Actions for space" msgstr "" @@ -1067,6 +1075,7 @@ msgstr "无法连接服务器。" #: src/pages/PluginsOverlay.tsx #: src/pages/PrivacySettingsOverlay.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Cancel" msgstr "取消" @@ -1376,6 +1385,7 @@ msgid "Collapse {0}" msgstr "折叠 {0}" #: src/components/OoAtelier.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Color" msgstr "颜色" @@ -1829,6 +1839,7 @@ msgstr "无法运行例行任务" #: src/pages/KnowledgeSection.tsx #: src/pages/ModelTierSettings.tsx #: src/pages/shell/bot-panel.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Could not save" msgstr "无法保存" @@ -2056,6 +2067,7 @@ msgid "Customer support" msgstr "客户支持" #: src/components/onboarding/OnboardingOoDesigner.tsx +#: src/pages/Shell.tsx msgid "Customize" msgstr "" @@ -2280,6 +2292,7 @@ msgstr "" #: src/components/ThreadCard.tsx #: src/pages/HeuteBrief.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Done" msgstr "完成" @@ -2325,10 +2338,6 @@ msgstr "技能草稿" msgid "Drafting the spec…" msgstr "" -#: src/components/SortableGroup.tsx -msgid "Drag to reorder" -msgstr "" - #: src/components/OoAtelier.tsx msgid "Drag, drop, or paste an image" msgstr "拖放或粘贴图片" @@ -2897,6 +2906,10 @@ msgstr "我做完了" msgid "I'm listening — what's the priority?" msgstr "" +#: src/pages/shell/project-look-dialog.tsx +msgid "Icon" +msgstr "" + #: src/components/UserProfileSettings.tsx msgid "Identity" msgstr "" @@ -3483,6 +3496,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Name" msgstr "名称" @@ -3520,6 +3534,7 @@ msgstr "" #: src/pages/ActivityList.tsx #: src/pages/HeuteBrief.tsx #: src/pages/shell/fullscreen-computer-overlay.tsx +#: src/pages/shell/project-overview.tsx #: src/pages/shell/transcript.tsx msgid "Needs you" msgstr "" @@ -4483,6 +4498,7 @@ msgstr "运行中" #: src/components/ThreadCard.tsx #: src/pages/ProjectSessionTimeline.tsx +#: src/pages/shell/project-overview.tsx msgid "Running" msgstr "运行中" @@ -4518,6 +4534,7 @@ msgstr "" #: src/pages/RoutineEditor.tsx #: src/pages/shell/bot-panel.tsx #: src/pages/shell/dialogs.tsx +#: src/pages/shell/project-look-dialog.tsx msgid "Save" msgstr "保存" diff --git a/apps/web/src/pages/ActivityList.tsx b/apps/web/src/pages/ActivityList.tsx index 0cb4e5b..e4b0b49 100644 --- a/apps/web/src/pages/ActivityList.tsx +++ b/apps/web/src/pages/ActivityList.tsx @@ -3,9 +3,8 @@ import { t } from "@lingui/core/macro"; import { Trans, useLingui } from "@lingui/react/macro"; import type { RunActivityRow } from "@rakazo/contracts"; import { runChatTitle } from "@rakazo/core"; -import { useEffect, useState } from "react"; import { isNeedsYouStatus, runActivityLabel } from "../lib/agent-activity"; -import { rpc } from "../lib/rpc"; +import { useRunActivity } from "../lib/run-activity"; function statusTone(status: RunActivityRow["status"]): string { if (status === "failed") return "text-destructive"; @@ -20,41 +19,8 @@ type ActivityListProps = { }; export function ActivityList({ onOpenRun }: ActivityListProps) { - const [activeRuns, setActiveRuns] = useState([]); - const [recentRuns, setRecentRuns] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - let cancelled = false; - let timer: number | undefined; - - const tick = async () => { - try { - const [active, recent] = await Promise.all([ - rpc.runs.list({ filter: "active" }), - rpc.runs.list({ filter: "recent" }), - ]); - if (cancelled) return; - setActiveRuns(active.runs); - setRecentRuns(recent.runs); - } catch { - // Keep the last good snapshot on transient RPC failures. - if (cancelled) return; - } finally { - if (!cancelled) { - setLoading(false); - // Schedule the next poll after the previous settles — no overlap. - timer = window.setTimeout(() => void tick(), 15_000); - } - } - }; - - void tick(); - return () => { - cancelled = true; - if (timer !== undefined) window.clearTimeout(timer); - }; - }, []); + const { active: activeRuns, recent: recentRuns, loaded } = useRunActivity(); + const loading = !loaded; if (loading) { return ( @@ -92,7 +58,7 @@ export function ActivityList({ onOpenRun }: ActivityListProps) { ); } -function ActivityRow({ run, onOpen }: { run: RunActivityRow; onOpen: () => void }) { +export function ActivityRow({ run, onOpen }: { run: RunActivityRow; onOpen: () => void }) { const { t } = useLingui(); const title = runChatTitle(run); const label = statusLabel(run.status); diff --git a/apps/web/src/pages/Shell.tsx b/apps/web/src/pages/Shell.tsx index 38c3bd6..a4e36fe 100644 --- a/apps/web/src/pages/Shell.tsx +++ b/apps/web/src/pages/Shell.tsx @@ -40,6 +40,7 @@ import { matchVoiceByName, type PaneState, parseVoiceChatCommand, + projectThreads, recordScreenPaneChoice, reorderBotTo, resolveComposerSendPlan, @@ -266,6 +267,10 @@ function voiceStatusRefreshTimeout(): Promise { export type NavPillar = "heute" | "team" | "projekte"; import { SortableGroup } from "../components/SortableGroup"; +import { useRunActivity } from "../lib/run-activity"; +import { ProjectLookDialog } from "./shell/project-look-dialog"; +import { ProjectOverview } from "./shell/project-overview"; +import { ProjectRow } from "./shell/project-row"; export function ShellPage() { const { t } = useLingui(); @@ -512,6 +517,7 @@ export function ShellPage() { const [deleteTarget, setDeleteTarget] = useState(null); const [deleteGroupTarget, setDeleteGroupTarget] = useState(null); const [deleteSpaceTarget, setDeleteSpaceTarget] = useState(null); + const [lookTarget, setLookTarget] = useState(null); const [spaceMenu, setSpaceMenu] = useState<{ id: string; position: ContextMenuPosition; @@ -1824,6 +1830,7 @@ export function ShellPage() { }, [voiceEntry, showProjectTimeline, callOpen, voiceCallBot, voiceStatus?.ready]); const heuteActionCount = useHeuteActionCount(groups, botsRef); + const runActivity = useRunActivity(); useShellSounds({ bots, transcriptRunning, @@ -3069,6 +3076,64 @@ export function ShellPage() { strategy={verticalListSortingStrategy} > {projectSidebarGroups.map((group) => { + const projectSpace = group.key.startsWith("project-space:") + ? spaces.find((space) => space.id === group.spaceId) + : undefined; + if (projectSpace) { + const threads = projectThreads(runActivity, { + spaceId: projectSpace.id, + }); + const projectSelected = + activeNavPillar === "projekte" && + bootstrapMe?.spaceId === projectSpace.id; + // Other chats in the project show only while it is open. + const projectChats = projectSelected + ? group.bots.filter((item) => item.chat.kind !== "main") + : []; + return ( + + item.chat.unread)} + needsYou={threads.needsYou.length} + running={threads.running.length > 0} + onOpen={() => { + setMobileSidebarOpen(false); + void startProject(projectSpace.id); + }} + onMenu={(menu, anchor) => { + spaceMenuAnchor.current = anchor; + setSpaceMenu(menu); + }} + /> + {projectChats.length > 0 ? ( +
+ {projectChats.map((item) => ( + { + setActiveNavPillar("projekte"); + openSpaceChat(item.chat.spaceId, `/app/g/${item.chat.id}`); + }} + activeSpaceId={bootstrapMe?.spaceId} + menuAnchor={botMenuAnchor} + onMenu={setBotMenu} + /> + ))} +
+ ) : null} +
+ ); + } const collapsed = Boolean(group.title) && collapsedSidebarSections.has(group.key); return ( @@ -3414,6 +3479,25 @@ export function ShellPage() { enrichMember={enrichStageMember} /> ) : null} + {!callOpen && activeGroup?.kind === "main" ? ( + + openSpaceChat( + run.spaceId, + run.groupId ? `/app/g/${run.groupId}` : `/app/${run.botId}`, + ) + } + /> + ) : null} - { - const target = spaces.find((space) => space.id === spaceMenu.id); - if (target) setDeleteSpaceTarget(target); - setSpaceMenu(null); - }} - > - - {t`Delete space`} - + {spaces.some( + (space) => space.id === spaceMenu.id && space.canEdit && !space.isDefault, + ) ? ( + { + const target = spaces.find((space) => space.id === spaceMenu.id); + if (target) setLookTarget(target); + setSpaceMenu(null); + }} + > + + {t`Customize`} + + ) : null} + {spaces.find((space) => space.id === spaceMenu.id)?.canDelete ? ( + { + const target = spaces.find((space) => space.id === spaceMenu.id); + if (target) setDeleteSpaceTarget(target); + setSpaceMenu(null); + }} + > + + {t`Delete space`} + + ) : null} ) : null} + {lookTarget ? ( + setLookTarget(null)} + onSave={async (look) => { + await rpc.spaces.update({ spaceId: lookTarget.id, ...look }); + setLookTarget(null); + await refreshBots(); + }} + /> + ) : null} + {deleteTarget ? ( void; + onSave: (look: ProjectLook) => Promise; +}) { + const { t } = useLingui(); + const nameId = useId(); + const start = projectLook(initial); + const [name, setName] = useState(initial.name); + const [icon, setIcon] = useState(start.icon.id); + const [color, setColor] = useState(start.color.id); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const save = () => { + const trimmed = name.trim(); + if (!trimmed || saving) return; + setSaving(true); + setError(null); + void onSave({ name: trimmed, icon, color }).catch((reason: unknown) => { + setError(reason instanceof Error ? reason.message : t`Could not save`); + setSaving(false); + }); + }; + + return ( + { + if (!open && !saving) onCancel(); + }} + > + + + + + {name.trim() || initial.name} + + + +
+ {PROJECT_COLORS.map((option) => ( +
+
+ {PROJECT_ICONS.map((option) => { + const Glyph = PROJECT_ICON_COMPONENTS[option.id]; + return ( + + ); + })} +
+ {error ?

{error}

: null} + + + + +
+
+ ); +} diff --git a/apps/web/src/pages/shell/project-overview.tsx b/apps/web/src/pages/shell/project-overview.tsx new file mode 100644 index 0000000..356cd4f --- /dev/null +++ b/apps/web/src/pages/shell/project-overview.tsx @@ -0,0 +1,75 @@ +import { Trans } from "@lingui/react/macro"; +import type { RunActivityRow } from "@rakazo/contracts"; +import { projectThreads } from "@rakazo/core"; +import type { ReactNode } from "react"; +import { useState } from "react"; +import { useRunActivity } from "../../lib/run-activity"; +import { ActivityRow } from "../ActivityList"; + +type Section = "needsYou" | "running" | "done"; + +/** + * The coordinator's view of its project: threads that wait on the user, threads its agents are + * working on, and recently finished ones. Empty groups stay hidden. + */ +export function ProjectOverview({ + spaceId, + coordinator, + onOpenRun, +}: { + spaceId: string; + coordinator?: { threadId: string; botId?: string }; + onOpenRun: (run: RunActivityRow) => void; +}) { + const activity = useRunActivity(); + const threads = projectThreads(activity, { spaceId, coordinator }); + // null until the user picks; "none" once they closed every group. + const [open, setOpen] = useState
(null); + const sections: { id: Section; runs: RunActivityRow[]; label: ReactNode; tone: string }[] = [ + { + id: "needsYou", + runs: threads.needsYou, + label: Needs you, + tone: "text-warning", + }, + { + id: "running", + runs: threads.running, + label: Running, + tone: "text-foreground", + }, + { id: "done", runs: threads.done, label: Done, tone: "text-muted-foreground" }, + ]; + const visible = sections.filter((section) => section.runs.length > 0); + if (visible.length === 0) return null; + // Waiting threads are what the user came for, so they start open. + const shown = open ?? (threads.needsYou.length > 0 ? "needsYou" : null); + const current = visible.find((section) => section.id === shown); + return ( +
+
+ {visible.map((section) => ( + + ))} +
+ {current ? ( +
+ {current.runs.map((run) => ( + onOpenRun(run)} /> + ))} +
+ ) : null} +
+ ); +} diff --git a/apps/web/src/pages/shell/project-row.tsx b/apps/web/src/pages/shell/project-row.tsx new file mode 100644 index 0000000..cec0c14 --- /dev/null +++ b/apps/web/src/pages/shell/project-row.tsx @@ -0,0 +1,95 @@ +import { useLingui } from "@lingui/react/macro"; +import { Button } from "@rakazo/ui-web"; +import { MoreHorizontal } from "lucide-react"; +import { ProjectIcon } from "../../components/project-icon"; +import { rosterNameClass, rosterRowSurfaceClass } from "./roster-row-style"; +import type { SpaceMenuRequest } from "./sidebar-group-header"; + +/** One project in the sidebar: its icon and name; it opens the project's coordinator. */ +export function ProjectRow({ + spaceId, + name, + icon, + color, + hasMenu, + selected, + unread, + needsYou, + running, + onOpen, + onMenu, +}: { + spaceId: string; + name: string; + icon?: string | null; + color?: string | null; + hasMenu: boolean; + selected: boolean; + unread: boolean; + needsYou: number; + running: boolean; + onOpen: () => void; + onMenu: (menu: SpaceMenuRequest, anchor: HTMLElement) => void; +}) { + const { t } = useLingui(); + return ( +
+ + {hasMenu ? ( + // Pointer devices reveal the menu on hover; touch opens it with a long press. + + ) : null} +
+ ); +} diff --git a/apps/web/src/pages/shell/roster-row-style.ts b/apps/web/src/pages/shell/roster-row-style.ts index 5e72d8d..6431647 100644 --- a/apps/web/src/pages/shell/roster-row-style.ts +++ b/apps/web/src/pages/shell/roster-row-style.ts @@ -1,6 +1,11 @@ /** One look for sidebar conversation rows: the main conversation entry and the bot roster. */ export function rosterRowClass(selected: boolean): string { - return `flex w-full items-center gap-3 rounded-2xl border px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${ + return `flex w-full items-center gap-3 px-3 py-3 text-start cursor-pointer min-h-[52px] ${rosterRowSurfaceClass(selected)}`; +} + +/** The row's card: border, fill and shadow, for rows that lay out their own content. */ +export function rosterRowSurfaceClass(selected: boolean): string { + return `rounded-2xl border mb-[2px] transition-all duration-150 ${ selected ? "border-sidebar-border bg-sidebar-accent shadow-xs dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)]" : "border-transparent hover:bg-sidebar-accent/50" diff --git a/apps/web/src/pages/shell/sidebar-group-header.tsx b/apps/web/src/pages/shell/sidebar-group-header.tsx index 1ba7b55..4d6d42f 100644 --- a/apps/web/src/pages/shell/sidebar-group-header.tsx +++ b/apps/web/src/pages/shell/sidebar-group-header.tsx @@ -1,9 +1,8 @@ import { useLingui } from "@lingui/react/macro"; import type { BotSection, Space } from "@rakazo/contracts"; import { Button } from "@rakazo/ui-web"; -import { ChevronDown, Folder, GripVertical, Lock, MoreHorizontal } from "lucide-react"; +import { ChevronDown, Folder, Lock, MoreHorizontal } from "lucide-react"; import type { RefObject } from "react"; -import { ProjectDragGrip } from "../../components/SortableGroup"; import type { ContextMenuPosition } from "../BotContextMenu"; export type SidebarGroupHeaderGroup = { @@ -26,7 +25,7 @@ export type SectionMenuRequest = { }; /** - * Collapsible title of a sidebar group. Project groups add a drag grip, show a lock for other + * Collapsible title of a sidebar group. Project groups show a lock for other * spaces and select an empty space instead of opening it. */ export function SidebarGroupHeader({ @@ -59,20 +58,10 @@ export function SidebarGroupHeader({ const { t } = useLingui(); const project = variant === "project"; return ( -
- {project ? ( - // Drag grip — positioned to the left of the title text - - - - ) : null} +