feat(web): remove redundant live progress card and add adaptive project vs 1:1 agent overview sidebar

This commit is contained in:
cynarAI 2026-10-18 22:07:53 +02:00
parent f6053dd2a8
commit be657f6e3c
4 changed files with 529 additions and 258 deletions

View File

@ -286,7 +286,7 @@ 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 { AgentOverview, ProjectOverview } from "./shell/project-overview";
import { ProjectRow } from "./shell/project-row";
export function ShellPage() {
@ -3188,8 +3188,186 @@ export function ShellPage() {
</div>
</button>
</div>
<div className="sidebar-projekte">
{/* PROJEKTE SECTION */}
<div className="flex items-center justify-between pt-4 pb-1 px-2.5">
<div>
<div className="rk-shell-section-label">{t`Projects`}</div>
<p className="mt-0.5 px-0.5 text-ui-caption rk-sidebar-muted">
{t`Threads by topic · folders`}
</p>
</div>
<div className="flex items-center gap-1.5">
<button
type="button"
aria-label={t`Create a project`}
data-testid="create-project-trigger"
onClick={() => openNewSpaceDialog("project")}
className="rk-touch-target rk-sidebar-muted hover:text-foreground/90 transition-colors"
>
<Plus strokeWidth={2.5} className="size-icon-md" />
</button>
</div>
</div>
{projectSidebarGroups.length === 0 ? (
<div className="p-4 text-center text-ui-label rk-sidebar-muted">
<p>{t`No projects yet.`}</p>
<Button
variant="outline"
size="sm"
className="mt-2 text-ui-label"
onClick={() => openNewSpaceDialog("project")}
>
<Plus className="mr-1 size-icon-sm" />
{t`Create a project`}
</Button>
</div>
) : null}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleProjectDragEnd}
>
<SortableContext
items={projectSidebarGroups.filter((g) => g.title).map((g) => g.spaceId)}
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 (
<SortableGroup key={`proj-${group.key}`} group={group}>
<ProjectRow
spaceId={projectSpace.id}
name={projectSpace.name}
icon={projectSpace.icon}
color={projectSpace.color}
members={(projectSpace.bots ?? []).map((b) => ({
botId: b.id,
name:
b.name === "Chief" || b.name === "Projektkoordinator"
? "Projektmanager"
: b.name,
color: b.color || FALLBACK_BOT_COLOR,
status: b.status,
}))}
hasMenu={
projectSpace.canEdit === true || projectSpace.canDelete === true
}
selected={projectSelected}
unread={group.bots.some((item) => 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 ? (
<div className="pl-6">
{projectChats.map((item) => (
<ProjectRosterRow
key={`${item.kind}:${item.chat.id}`}
item={item}
selected={inGroup && activeGroup?.id === item.chat.id}
onOpen={() => {
setActiveNavPillar("projekte");
openSpaceChat(item.chat.spaceId, `/app/g/${item.chat.id}`);
}}
activeSpaceId={bootstrapMe?.spaceId}
menuAnchor={botMenuAnchor}
onMenu={setBotMenu}
/>
))}
</div>
) : null}
</SortableGroup>
);
}
const collapsed =
Boolean(group.title) && collapsedSidebarSections.has(group.key);
return (
<SortableGroup key={`proj-${group.key}`} group={group}>
<div data-sidebar-group={group.key}>
{group.title ? (
<SidebarGroupHeader
variant="project"
group={group}
collapsed={collapsed}
activeSpaceId={bootstrapMe?.spaceId}
botSections={botSections}
spaces={spaces}
onOpenEmptySpace={(spaceId) => void startProject(spaceId)}
onToggle={toggleSidebarSection}
sectionMenuAnchor={sectionMenuAnchor}
onSectionMenu={setSectionMenu}
spaceMenuAnchor={spaceMenuAnchor}
onSpaceMenu={setSpaceMenu}
/>
) : null}
{!collapsed && group.startSpaceId ? (
<div className="px-3 pb-2">
<Button
variant="outline"
size="sm"
className="w-full text-ui-label"
data-testid={`project-start-${group.spaceId}`}
onClick={() => void startProject(group.spaceId)}
>
<Plus className="mr-1 size-icon-sm" />
{t`Start`}
</Button>
</div>
) : null}
{!collapsed &&
group.bots.map((item) => (
<ProjectRosterRow
key={`${item.kind}:${item.chat.id}`}
item={item}
selected={
item.kind === "bot"
? !inGroup && active?.id === item.chat.id
: inGroup && activeGroup?.id === item.chat.id
}
onOpen={() => {
setActiveNavPillar("projekte");
openSpaceChat(
item.chat.spaceId,
item.kind === "bot"
? `/app/${item.chat.id}`
: `/app/g/${item.chat.id}`,
);
}}
activeSpaceId={bootstrapMe?.spaceId}
menuAnchor={botMenuAnchor}
onMenu={setBotMenu}
/>
))}
</div>
</SortableGroup>
);
})}
</SortableContext>
</DndContext>
</div>
<div className="sidebar-unterhaltungen">
<div className="flex items-center justify-between pt-4 pb-2 px-2.5">
<div className="flex items-center justify-between pt-4 pb-2 mt-3 border-t border-sidebar-border px-2.5">
<div className="rk-shell-section-label">{t`Talk to your Oos`}</div>
<div className="flex items-center">
<Popover open={conversationPickerOpen} onOpenChange={setConversationPickerOpen}>
@ -3319,184 +3497,6 @@ export function ShellPage() {
</div>
);
})}
<div className="sidebar-projekte">
{/* PROJEKTE SECTION */}
<div className="flex items-center justify-between pt-4 pb-1 mt-3 border-t border-sidebar-border px-2.5">
<div>
<div className="rk-shell-section-label">{t`Projects`}</div>
<p className="mt-0.5 px-0.5 text-ui-caption rk-sidebar-muted">
{t`Threads by topic · folders`}
</p>
</div>
<div className="flex items-center gap-1.5">
<button
type="button"
aria-label={t`Create a project`}
data-testid="create-project-trigger"
onClick={() => openNewSpaceDialog("project")}
className="rk-touch-target rk-sidebar-muted hover:text-foreground/90 transition-colors"
>
<Plus strokeWidth={2.5} className="size-icon-md" />
</button>
</div>
</div>
{projectSidebarGroups.length === 0 ? (
<div className="p-4 text-center text-ui-label rk-sidebar-muted">
<p>{t`No projects yet.`}</p>
<Button
variant="outline"
size="sm"
className="mt-2 text-ui-label"
onClick={() => openNewSpaceDialog("project")}
>
<Plus className="mr-1 size-icon-sm" />
{t`Create a project`}
</Button>
</div>
) : null}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleProjectDragEnd}
>
<SortableContext
items={projectSidebarGroups.filter((g) => g.title).map((g) => g.spaceId)}
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 (
<SortableGroup key={`proj-${group.key}`} group={group}>
<ProjectRow
spaceId={projectSpace.id}
name={projectSpace.name}
icon={projectSpace.icon}
color={projectSpace.color}
members={(projectSpace.bots ?? []).map((b) => ({
botId: b.id,
name:
b.name === "Chief" || b.name === "Projektkoordinator"
? "Projektmanager"
: b.name,
color: b.color || FALLBACK_BOT_COLOR,
status: b.status,
}))}
hasMenu={
projectSpace.canEdit === true || projectSpace.canDelete === true
}
selected={projectSelected}
unread={group.bots.some((item) => 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 ? (
<div className="pl-6">
{projectChats.map((item) => (
<ProjectRosterRow
key={`${item.kind}:${item.chat.id}`}
item={item}
selected={inGroup && activeGroup?.id === item.chat.id}
onOpen={() => {
setActiveNavPillar("projekte");
openSpaceChat(item.chat.spaceId, `/app/g/${item.chat.id}`);
}}
activeSpaceId={bootstrapMe?.spaceId}
menuAnchor={botMenuAnchor}
onMenu={setBotMenu}
/>
))}
</div>
) : null}
</SortableGroup>
);
}
const collapsed =
Boolean(group.title) && collapsedSidebarSections.has(group.key);
return (
<SortableGroup key={`proj-${group.key}`} group={group}>
<div data-sidebar-group={group.key}>
{group.title ? (
<SidebarGroupHeader
variant="project"
group={group}
collapsed={collapsed}
activeSpaceId={bootstrapMe?.spaceId}
botSections={botSections}
spaces={spaces}
onOpenEmptySpace={(spaceId) => void startProject(spaceId)}
onToggle={toggleSidebarSection}
sectionMenuAnchor={sectionMenuAnchor}
onSectionMenu={setSectionMenu}
spaceMenuAnchor={spaceMenuAnchor}
onSpaceMenu={setSpaceMenu}
/>
) : null}
{!collapsed && group.startSpaceId ? (
<div className="px-3 pb-2">
<Button
variant="outline"
size="sm"
className="w-full text-ui-label"
data-testid={`project-start-${group.spaceId}`}
onClick={() => void startProject(group.spaceId)}
>
<Plus className="mr-1 size-icon-sm" />
{t`Start`}
</Button>
</div>
) : null}
{!collapsed &&
group.bots.map((item) => (
<ProjectRosterRow
key={`${item.kind}:${item.chat.id}`}
item={item}
selected={
item.kind === "bot"
? !inGroup && active?.id === item.chat.id
: inGroup && activeGroup?.id === item.chat.id
}
onOpen={() => {
setActiveNavPillar("projekte");
openSpaceChat(
item.chat.spaceId,
item.kind === "bot"
? `/app/${item.chat.id}`
: `/app/g/${item.chat.id}`,
);
}}
activeSpaceId={bootstrapMe?.spaceId}
menuAnchor={botMenuAnchor}
onMenu={setBotMenu}
/>
))}
</div>
</SortableGroup>
);
})}
</SortableContext>
</DndContext>
</div>
</div>
</>
)}
@ -4183,17 +4183,17 @@ export function ShellPage() {
setCanvasTab(tab);
if (tab === "browser") {
setPanel("browser");
} else if (tab === "sandbox") {
setPanel("computer");
} else {
} else if (inProject && !dualSurface.isMobile && tab !== "sandbox") {
setPanel(null);
} else {
setPanel("computer");
}
}}
inProject={inProject}
/>
) : null}
{inProject && !panel && effectiveTab === "overview" && activeSpace ? (
{inProject && panel !== "settings" && effectiveTab === "overview" && activeSpace ? (
<div className="rk-scroll h-full w-full overflow-y-auto px-4 py-4">
<ProjectOverview
spaceId={activeSpace.id}
@ -4212,12 +4212,59 @@ export function ShellPage() {
run.groupId ? `/app/g/${run.groupId}` : `/app/${run.botId}`,
)
}
onOpenBot={openBot}
onOpenNewInquiry={() => setNewInquiryOpen(true)}
onEditProject={() => {
setLookTarget(activeSpace);
}}
/>
</div>
) : !inProject &&
panel !== "settings" &&
effectiveTab === "overview" &&
(active ?? coordinatorBot) ? (
<div className="rk-scroll h-full w-full overflow-y-auto px-4 py-4">
<AgentOverview
bot={(active ?? coordinatorBot)!}
running={transcriptRunning}
routines={activeRoutines}
runningRoutineId={
snapshot?.run && isActive(snapshot.run.status)
? (snapshot.run.routineId ?? null)
: null
}
togglingRoutineId={routineActiveToggleId}
onToggleRoutineActive={(routine, next) =>
void setRoutineActive(routine, next)
}
onOpenRoutine={(routine) => {
setRoutineDraft(draftFromRoutine(routine));
setRoutineWebhookSecret(null);
setEditingRoutine(routine);
setRoutineError(null);
setPanel("routine");
}}
onAddRoutine={
active
? () => {
setRoutineDraft(emptyRoutineDraft());
setRoutineWebhookSecret(null);
setEditingRoutine(null);
setRoutineError(null);
setPanel("routine");
}
: undefined
}
onStopRun={() => void stopRun()}
onOpenRun={(run) =>
openSpaceChat(
run.spaceId,
run.groupId ? `/app/g/${run.groupId}` : `/app/${run.botId}`,
)
}
onOpenSettings={() => setPanel("settings")}
/>
</div>
) : panel === "browser" || (effectiveTab === "browser" && panel !== "settings") ? (
<div className="flex-1 min-h-0 flex flex-col">
<InAppBrowser

View File

@ -1,25 +1,29 @@
import { Trans, useLingui } from "@lingui/react/macro";
import type { RunActivityRow, Space } from "@rakazo/contracts";
import type { Bot, Routine, RunActivityRow, Space } from "@rakazo/contracts";
import { projectThreads } from "@rakazo/core";
import { Button } from "@rakazo/ui-web";
import { FolderOpen, GitBranch, Pencil, Plus, Target } from "lucide-react";
import { BotAvatar, Button, cn } from "@rakazo/ui-web";
import { Clock, FolderOpen, GitBranch, Pencil, Plus, Settings, Target } from "lucide-react";
import type { ReactNode } from "react";
import { useState } from "react";
import { ProjectIcon } from "../../components/project-icon";
import { isNeedsYouStatus } from "../../lib/agent-activity";
import { useRunActivity } from "../../lib/run-activity";
import { ActivityRow } from "../ActivityList";
import { RoutineListRow } from "../RoutineEditor";
import { FALLBACK_BOT_COLOR } from "./shell-types";
type Section = "needsYou" | "running" | "done";
/**
* The coordinator's view of its project: project identity, mission goal, context badges,
* sub-thread creation launcher, and delegated task activity.
* project specialist roster, sub-thread creation launcher, and delegated task activity.
*/
export function ProjectOverview({
spaceId,
space,
coordinator,
onOpenRun,
onOpenBot,
onOpenNewInquiry,
onEditProject,
}: {
@ -27,6 +31,7 @@ export function ProjectOverview({
space?: Space;
coordinator?: { threadId: string; botId?: string };
onOpenRun: (run: RunActivityRow) => void;
onOpenBot?: (botId: string) => void;
onOpenNewInquiry?: () => void;
onEditProject?: () => void;
}) {
@ -56,13 +61,16 @@ export function ProjectOverview({
// If not a project space and no runs, keep hidden
if (!isProjectSpace && 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);
// Waiting or actively running threads start open automatically so progress is visible right away.
const shown =
open ??
(threads.needsYou.length > 0 ? "needsYou" : threads.running.length > 0 ? "running" : null);
const current = visible.find((section) => section.id === shown);
const folderPath = space?.workspace?.path;
const gitRemote = space?.workspace?.git?.remoteUrl;
const goal = space?.goal;
const projectBots = space?.bots ?? [];
return (
<div
@ -110,6 +118,17 @@ export function ProjectOverview({
{goal}
</div>
</div>
) : onEditProject ? (
<button
type="button"
onClick={onEditProject}
className="flex w-full items-center gap-2 rounded-lg border border-dashed border-border/80 bg-background/40 px-3 py-2 text-start text-ui-body-sm text-muted-foreground hover:border-border hover:bg-accent/30 hover:text-foreground transition-colors cursor-pointer"
>
<Target size={15} strokeWidth={1.8} className="shrink-0" />
<span>
<Trans>Add goal</Trans>
</span>
</button>
) : null}
{folderPath || gitRemote ? (
@ -139,6 +158,55 @@ export function ProjectOverview({
) : null}
</div>
) : null}
{projectBots.length > 0 ? (
<div className="border-t border-border/50 pt-2.5">
<div className="mb-1.5 text-ui-label font-medium text-muted-foreground">
<Trans>Team</Trans>
</div>
<div className="flex flex-wrap gap-1.5">
{projectBots.map((member) => {
const displayName =
member.name === "Chief" || member.name === "Projektkoordinator"
? "Projektmanager"
: member.name;
const isWorking = Boolean(
member.status &&
member.status !== "idle" &&
member.status !== "ready" &&
member.status !== "done",
);
return (
<button
key={member.id}
type="button"
disabled={!onOpenBot}
onClick={() => onOpenBot?.(member.id)}
className={cn(
"inline-flex items-center gap-1.5 rounded-full border border-border/70 bg-background/70 px-2.5 py-1 text-ui-label text-foreground transition-colors",
onOpenBot && "hover:bg-accent cursor-pointer",
)}
>
<BotAvatar
color={member.color || FALLBACK_BOT_COLOR}
identity={member.id}
size={16}
moment={isWorking ? "working" : "idle"}
/>
<span className="truncate max-w-[130px] font-medium">{displayName}</span>
<span
aria-hidden="true"
className={cn(
"size-1.5 rounded-full",
isWorking ? "bg-warning animate-pulse" : "bg-success",
)}
/>
</button>
);
})}
</div>
</div>
) : null}
</div>
) : null}
@ -168,7 +236,205 @@ export function ProjectOverview({
</div>
) : null}
</div>
) : isProjectSpace ? (
<div className="rounded-xl border border-border/60 bg-card/30 px-3.5 py-3 text-ui-body-sm text-muted-foreground">
<Trans>No runs yet</Trans>
</div>
) : null}
</div>
);
}
/**
* 1:1 Agent overview in the side panel: agent identity & status, active and recent
* task runs for this specific agent, and its scheduled routines.
*/
export function AgentOverview({
bot,
running = false,
routines = [],
runningRoutineId,
togglingRoutineId,
onToggleRoutineActive,
onOpenRoutine,
onAddRoutine,
onStopRun,
onOpenRun,
onOpenSettings,
}: {
bot: Bot;
running?: boolean;
routines?: readonly Routine[];
runningRoutineId?: string | null;
togglingRoutineId?: string | null;
onToggleRoutineActive?: (routine: Routine, active: boolean) => void;
onOpenRoutine?: (routine: Routine) => void;
onAddRoutine?: () => void;
onStopRun?: () => void;
onOpenRun: (run: RunActivityRow) => void;
onOpenSettings?: () => void;
}) {
const { t } = useLingui();
const { active: allActiveRuns, recent: allRecentRuns } = useRunActivity();
const botActiveRuns = allActiveRuns.filter((run) => run.botId === bot.id);
const botRecentRuns = allRecentRuns.filter((run) => run.botId === bot.id);
const needsYouCount = botActiveRuns.filter((run) => isNeedsYouStatus(run.status)).length;
const isWorking = running || botActiveRuns.length > 0;
return (
<div data-testid="agent-overview" className="mx-auto w-full max-w-3xl px-4 pt-3 pb-4 space-y-4">
{/* Agent Identity & Status Card */}
<div className="rounded-xl border border-border bg-card/60 p-3.5 backdrop-blur-xs shadow-2xs space-y-2.5">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3 min-w-0">
<div className="relative shrink-0">
<BotAvatar
color={bot.color || FALLBACK_BOT_COLOR}
identity={bot.id}
size={34}
moment={isWorking ? "working" : "idle"}
/>
<span
aria-hidden="true"
className={cn(
"absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full ring-2 ring-background",
needsYouCount > 0
? "bg-warning"
: isWorking
? "bg-warning animate-pulse"
: "bg-success",
)}
/>
</div>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-semibold text-ui-title-sm truncate text-foreground">
{bot.name}
</span>
{bot.title ? (
<span className="rounded-full border border-border/60 bg-muted/50 px-2 py-0.5 text-ui-label font-medium text-muted-foreground">
{bot.title}
</span>
) : null}
</div>
<div className="mt-0.5 flex items-center gap-1.5 text-ui-label text-muted-foreground">
{needsYouCount > 0 ? (
<span className="text-warning font-medium">
<Trans>Needs you</Trans>
</span>
) : isWorking ? (
<span className="text-foreground font-medium">
<Trans>Working</Trans>
</span>
) : (
<span>
<Trans>Ready</Trans>
</span>
)}
</div>
</div>
</div>
{onOpenSettings ? (
<Button
variant="ghost"
size="icon-sm"
aria-label={t`Show settings`}
onClick={onOpenSettings}
className="size-7 text-muted-foreground hover:text-foreground shrink-0"
>
<Settings size={15} strokeWidth={1.8} />
</Button>
) : null}
</div>
{bot.description ? (
<p className="text-ui-body-sm text-muted-foreground leading-relaxed line-clamp-3">
{bot.description}
</p>
) : null}
</div>
{/* Agent Runs & Tasks */}
<div className="space-y-2">
<div className="flex items-center justify-between px-1">
<span className="text-ui-label font-semibold uppercase tracking-wider text-muted-foreground">
<Trans>Run history</Trans>
</span>
</div>
{botActiveRuns.length > 0 ? (
<div className="rounded-xl border border-border/70 bg-card/40 p-1.5 space-y-1">
<div className="px-2 pt-1 text-ui-label font-medium text-warning">
<Trans>Now</Trans>
</div>
{botActiveRuns.map((run) => (
<ActivityRow key={run.runId} run={run} onOpen={() => onOpenRun(run)} />
))}
</div>
) : null}
{botRecentRuns.length > 0 ? (
<div className="rounded-xl border border-border/60 bg-card/30 p-1.5 space-y-1">
{botActiveRuns.length > 0 ? (
<div className="px-2 pt-1 text-ui-label font-medium text-muted-foreground">
<Trans>Recent</Trans>
</div>
) : null}
{botRecentRuns.slice(0, 8).map((run) => (
<ActivityRow key={run.runId} run={run} onOpen={() => onOpenRun(run)} />
))}
</div>
) : botActiveRuns.length === 0 ? (
<div className="rounded-xl border border-border/60 bg-card/30 px-3.5 py-3 text-ui-body-sm text-muted-foreground">
<Trans>No runs yet</Trans>
</div>
) : null}
</div>
{/* Scheduled Routines for this 1:1 Agent */}
<div className="space-y-2">
<div className="flex items-center justify-between px-1">
<span className="text-ui-label font-semibold uppercase tracking-wider text-muted-foreground">
<Trans>Routines</Trans>
</span>
{onAddRoutine ? (
<Button
variant="outline"
size="xs"
onClick={onAddRoutine}
className="gap-1 text-ui-label"
>
<Plus size={13} strokeWidth={2.2} />
<Trans>Routine</Trans>
</Button>
) : null}
</div>
{routines.length > 0 ? (
<div className="rounded-xl border border-border/60 bg-card/30 p-1.5 space-y-0.5">
{routines.map((routine) => (
<RoutineListRow
key={routine.id}
routine={routine}
running={runningRoutineId === routine.id}
togglingActive={togglingRoutineId === routine.id}
onActiveChange={(next) => onToggleRoutineActive?.(routine, next)}
onOpen={() => onOpenRoutine?.(routine)}
onStop={() => onStopRun?.()}
/>
))}
</div>
) : (
<div className="flex items-center gap-2 rounded-xl border border-border/60 bg-card/30 px-3.5 py-2.5 text-ui-body-sm text-muted-foreground">
<Clock size={14} className="shrink-0" />
<span>
<Trans>On a schedule</Trans>
</span>
</div>
)}
</div>
</div>
);
}

View File

@ -42,8 +42,10 @@ export function SidePanelHeader({
<span className="text-ui-body font-semibold text-foreground tracking-tight">
{panel === "settings" ? (
<Trans>Settings</Trans>
) : inProject && (!panel || activeTab === "overview") ? (
) : inProject && activeTab === "overview" ? (
<Trans>Project overview</Trans>
) : !inProject && activeTab === "overview" ? (
<Trans>Overview</Trans>
) : (
<Trans>Living Canvas</Trans>
)}
@ -112,33 +114,31 @@ export function SidePanelHeader({
</div>
{/* Surface Switcher Tabs */}
{active && panel !== "settings" && onSelectTab ? (
{active && onSelectTab ? (
<div className="flex items-center gap-1 border-t border-border/40 pt-2">
{inProject ? (
<button
type="button"
data-tab="overview"
data-active={activeTab === "overview" ? "true" : undefined}
onClick={() => onSelectTab("overview")}
className={cn(
"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-ui-body-sm font-medium transition-colors cursor-pointer",
activeTab === "overview"
? "bg-accent text-accent-foreground font-semibold shadow-2xs"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
>
<Target className="size-3.5" />
<Trans>Overview</Trans>
</button>
) : null}
<button
type="button"
data-tab="overview"
data-active={panel !== "settings" && activeTab === "overview" ? "true" : undefined}
onClick={() => onSelectTab("overview")}
className={cn(
"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-ui-body-sm font-medium transition-colors cursor-pointer",
panel !== "settings" && activeTab === "overview"
? "bg-accent text-accent-foreground font-semibold shadow-2xs"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
>
<Target className="size-3.5" />
<Trans>Overview</Trans>
</button>
<button
type="button"
data-tab="files"
data-active={activeTab === "files" ? "true" : undefined}
data-active={panel !== "settings" && activeTab === "files" ? "true" : undefined}
onClick={() => onSelectTab("files")}
className={cn(
"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-ui-body-sm font-medium transition-colors cursor-pointer",
activeTab === "files"
panel !== "settings" && activeTab === "files"
? "bg-accent text-accent-foreground font-semibold shadow-2xs"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
@ -149,11 +149,11 @@ export function SidePanelHeader({
<button
type="button"
data-tab="sandbox"
data-active={activeTab === "sandbox" ? "true" : undefined}
data-active={panel !== "settings" && activeTab === "sandbox" ? "true" : undefined}
onClick={() => onSelectTab("sandbox")}
className={cn(
"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-ui-body-sm font-medium transition-colors cursor-pointer",
activeTab === "sandbox"
panel !== "settings" && activeTab === "sandbox"
? "bg-accent text-accent-foreground font-semibold shadow-2xs"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
@ -164,11 +164,11 @@ export function SidePanelHeader({
<button
type="button"
data-tab="browser"
data-active={activeTab === "browser" ? "true" : undefined}
data-active={panel !== "settings" && activeTab === "browser" ? "true" : undefined}
onClick={() => onSelectTab("browser")}
className={cn(
"inline-flex items-center gap-1.5 rounded-md px-2.5 py-1 text-ui-body-sm font-medium transition-colors cursor-pointer",
activeTab === "browser"
panel !== "settings" && activeTab === "browser"
? "bg-accent text-accent-foreground font-semibold shadow-2xs"
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}

View File

@ -39,7 +39,6 @@ import { MediaCard } from "../../components/media/MediaCard";
import { MediaPlayer } from "../../components/media/MediaPlayer";
import { ThreadCard } from "../../components/ThreadCard";
import { SkillDraftCard } from "../../components/teach/SkillDraftCard";
import { rosterDoingVerb } from "../../lib/agent-activity";
import type { ArtifactTarget } from "../../lib/artifact-open";
import { copyableMessageText } from "../../lib/message-text";
import { messageProviderLabel } from "../../lib/messaging";
@ -58,7 +57,7 @@ import {
} from "./message-cards";
import { previewMessageText } from "./message-preview";
import { RoutineEventLine } from "./routine-event-line";
import { JobProgressCard, RunSteps } from "./run-steps";
import { RunSteps } from "./run-steps";
import { FALLBACK_BOT_COLOR } from "./shell-types";
import { isInterruptedStepMessage } from "./transcript-status";
@ -202,36 +201,6 @@ export const Transcript = memo(function Transcript({
[reactionView.visibleMessages],
);
const transcriptSubordinate = encounterMode && voiceEncounterActive;
const workingBotName = workingBots.length === 1 ? workingBots[0]?.name : undefined;
const primaryRunStatus = workingBots[0]?.status;
const statusVerb = rosterDoingVerb(primaryRunStatus);
const workingLabel =
statusVerb ??
(workingBotName != null && workingBotName !== ""
? t`${workingBotName} is working`
: t`Bots are working`);
const lastUserTask = useMemo(() => {
let fallback: string | null = null;
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (m?.role === "user") {
const text = m.blocks
.filter((b): b is { kind: "text"; text: string } => b.kind === "text")
.map((b) => b.text)
.join(" ")
.trim();
if (!text) continue;
if (!fallback) fallback = text;
const isPunctuationOrFiller =
/^[?!.,:;]+$/.test(text) ||
(text.length <= 4 && /^(ok|ja|nein|yes|no|hi|hey)$/i.test(text));
if (!isPunctuationOrFiller) {
return text;
}
}
}
return fallback;
}, [messages]);
const [quoteDraft, setQuoteDraft] = useState<{
message: ThreadMessage;
text: string;
@ -634,17 +603,6 @@ export const Transcript = memo(function Transcript({
</div>
);
})}
{(running || messages.some((m) => m.id.startsWith("progress:"))) && !encounterMode ? (
<div className="mt-2.5">
<JobProgressCard
bot={workingBots[0]}
taskTitle={lastUserTask}
running={true}
status="running"
activeAction={workingLabel}
/>
</div>
) : null}
</div>
</div>
{quoteDraft ? (