feat(ui): level up moves to the agents sidebar; repos open collapsed
Level up now sits at the bottom of the Agents sidebar, labelled with the selected agent's name, and renders only once an agent is selected — it is hidden during select mode so the reap bar stays the single footer action there. It is gone from the ClawCommandCenter header. Repos open with every org folded. Rather than seeding a "collapsed" set with all keys on load, the state tracks EXPANDED: a smaller change that also stays correct for orgs that arrive later from a sync, which a seeded set would render open. With 182 repos across 8 orgs, an all-expanded default buried the org names the list is meant to be navigated by. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5db695460f
commit
7cf77a9248
@@ -7,10 +7,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Activity, Brain, Camera, Sparkles } from "lucide-react";
|
import { Activity, Brain, Camera } from "lucide-react";
|
||||||
|
|
||||||
import { proposeForAgent } from "@/lib/api/level-up";
|
|
||||||
import { LevelUpDrawer } from "./LevelUpDrawer";
|
|
||||||
|
|
||||||
import type { DemoAgent } from "@/lib/dashboard-demo";
|
import type { DemoAgent } from "@/lib/dashboard-demo";
|
||||||
import { useAgentTelemetry, useLiveEvent } from "@/lib/live/useClawmatesLive";
|
import { useAgentTelemetry, useLiveEvent } from "@/lib/live/useClawmatesLive";
|
||||||
@@ -208,21 +205,8 @@ export function ClawCommandCenter({
|
|||||||
const tele = useAgentTelemetry(agent.id);
|
const tele = useAgentTelemetry(agent.id);
|
||||||
const doors = tele?.doorsPending ?? 0;
|
const doors = tele?.doorsPending ?? 0;
|
||||||
|
|
||||||
const [levelUpBusy, setLevelUpBusy] = useState(false);
|
// Level up now lives at the bottom of the Agents sidebar (Dashboard), where
|
||||||
const [levelUpOpenId, setLevelUpOpenId] = useState<string | null>(null);
|
// it sits next to the agent you picked rather than in this header.
|
||||||
const [levelUpError, setLevelUpError] = useState<string | null>(null);
|
|
||||||
const proposeLevelUp = async () => {
|
|
||||||
setLevelUpBusy(true);
|
|
||||||
setLevelUpError(null);
|
|
||||||
try {
|
|
||||||
const { proposal_id } = await proposeForAgent(agent.id);
|
|
||||||
setLevelUpOpenId(proposal_id);
|
|
||||||
} catch (e) {
|
|
||||||
setLevelUpError(e instanceof Error ? e.message : "level-up failed");
|
|
||||||
} finally {
|
|
||||||
setLevelUpBusy(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", background: "#08080a" }}>
|
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", background: "#08080a" }}>
|
||||||
@@ -239,41 +223,7 @@ export function ClawCommandCenter({
|
|||||||
<div style={{ fontFamily: mono, fontSize: 11.5, letterSpacing: ".06em", color: "#ff8a7a", marginTop: 2 }}>{agent.role}<span style={{ color: "#5a5a62" }}> · part of {teamName}</span></div>
|
<div style={{ fontFamily: mono, fontSize: 11.5, letterSpacing: ".06em", color: "#ff8a7a", marginTop: 2 }}>{agent.role}<span style={{ color: "#5a5a62" }}> · part of {teamName}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ flex: 1 }} />
|
<span style={{ flex: 1 }} />
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={proposeLevelUp}
|
|
||||||
disabled={levelUpBusy}
|
|
||||||
title="Ask the proposer to suggest improvements to this claw's identity, skills, and brain"
|
|
||||||
style={{
|
|
||||||
padding: "6px 12px",
|
|
||||||
borderRadius: 8,
|
|
||||||
border: "1px solid rgba(201,160,255,.45)",
|
|
||||||
background: "rgba(201,160,255,.1)",
|
|
||||||
color: "#c9a0ff",
|
|
||||||
fontFamily: mono,
|
|
||||||
fontSize: 11,
|
|
||||||
letterSpacing: ".08em",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
cursor: "pointer",
|
|
||||||
display: "inline-flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 6,
|
|
||||||
opacity: levelUpBusy ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Sparkles size={12} />
|
|
||||||
{levelUpBusy ? "Proposing…" : "Level up"}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{levelUpError && (
|
|
||||||
<div style={{ padding: "6px 22px", color: "#ff8a7a", fontSize: 11 }}>{levelUpError}</div>
|
|
||||||
)}
|
|
||||||
{levelUpOpenId && (
|
|
||||||
<LevelUpDrawer
|
|
||||||
proposalId={levelUpOpenId}
|
|
||||||
onClose={() => setLevelUpOpenId(null)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Metric grid — 2-column, wraps to 3 rows for the 5 tiles.
|
{/* Metric grid — 2-column, wraps to 3 rows for the 5 tiles.
|
||||||
Activity now lives here (top-of-fold quick-glance); the beefier
|
Activity now lives here (top-of-fold quick-glance); the beefier
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { useEffect, useRef, useState, useSyncExternalStore, type CSSProperties }
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useQueryStates } from "nuqs";
|
import { useQueryStates } from "nuqs";
|
||||||
import { Bot, Brain, History, MessageSquare, Monitor, PanelLeftClose, PanelLeftOpen, PanelRight, Trash2, Users, Wrench, X } from "lucide-react";
|
import { Bot, Brain, History, MessageSquare, Monitor, PanelLeftClose, PanelLeftOpen, PanelRight, Sparkles, Trash2, Users, Wrench, X } from "lucide-react";
|
||||||
|
|
||||||
import type { DemoAgent, DemoCompany, DemoOrg, DemoTeam } from "@/lib/dashboard-demo";
|
import type { DemoAgent, DemoCompany, DemoOrg, DemoTeam } from "@/lib/dashboard-demo";
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
@@ -27,6 +27,8 @@ import { StructureTree, orgNode, type TreeItem, clawNode } from "./StructureTree
|
|||||||
import { MissionsList } from "./MissionsList";
|
import { MissionsList } from "./MissionsList";
|
||||||
import { MissionWizard } from "./MissionWizard";
|
import { MissionWizard } from "./MissionWizard";
|
||||||
import { LevelUpInbox } from "./LevelUpInbox";
|
import { LevelUpInbox } from "./LevelUpInbox";
|
||||||
|
import { LevelUpDrawer } from "./LevelUpDrawer";
|
||||||
|
import { proposeForAgent } from "@/lib/api/level-up";
|
||||||
import { MissionCanvas } from "./MissionCanvas";
|
import { MissionCanvas } from "./MissionCanvas";
|
||||||
import { RepoList } from "./RepoList";
|
import { RepoList } from "./RepoList";
|
||||||
import { RepoCanvas } from "./RepoCanvas";
|
import { RepoCanvas } from "./RepoCanvas";
|
||||||
@@ -387,6 +389,26 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
(agent ? agentById.get(agent.id) : undefined) ??
|
(agent ? agentById.get(agent.id) : undefined) ??
|
||||||
(agent ? { id: agent.id, workspace_id: "", name: agent.name, job_title: agent.role, system_prompt: agent.systemPrompt, avatar: "", accent: "#ff6f61", wallpaper: "", managed_by: "", status: "online" } : undefined);
|
(agent ? { id: agent.id, workspace_id: "", name: agent.name, job_title: agent.role, system_prompt: agent.systemPrompt, avatar: "", accent: "#ff6f61", wallpaper: "", managed_by: "", status: "online" } : undefined);
|
||||||
|
|
||||||
|
// Level up — moved here from the ClawCommandCenter header so it sits at the
|
||||||
|
// bottom of the Agents sidebar, next to the agent you picked. Keyed off
|
||||||
|
// `clawAgent`, so the control simply is not rendered until one is selected.
|
||||||
|
const [levelUpBusy, setLevelUpBusy] = useState(false);
|
||||||
|
const [levelUpOpenId, setLevelUpOpenId] = useState<string | null>(null);
|
||||||
|
const [levelUpError, setLevelUpError] = useState<string | null>(null);
|
||||||
|
const proposeLevelUp = async () => {
|
||||||
|
if (!clawAgent) return;
|
||||||
|
setLevelUpBusy(true);
|
||||||
|
setLevelUpError(null);
|
||||||
|
try {
|
||||||
|
const { proposal_id } = await proposeForAgent(clawAgent.id);
|
||||||
|
setLevelUpOpenId(proposal_id);
|
||||||
|
} catch (e) {
|
||||||
|
setLevelUpError(e instanceof Error ? e.message : "level-up failed");
|
||||||
|
} finally {
|
||||||
|
setLevelUpBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Computer panel state (size + open app), shared with DevicePanel via nuqs.
|
// Computer panel state (size + open app), shared with DevicePanel via nuqs.
|
||||||
const [{ app, device }, setParams] = useQueryStates(panelParsers, { shallow: true });
|
const [{ app, device }, setParams] = useQueryStates(panelParsers, { shallow: true });
|
||||||
// Custom computer width (px) from dragging the panel's left edge; null = use the
|
// Custom computer width (px) from dragging the panel's left edge; null = use the
|
||||||
@@ -1046,6 +1068,26 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
router.refresh();
|
router.refresh();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* Level up — pinned to the bottom of the Agents sidebar, and only
|
||||||
|
once an agent is actually selected. Hidden during select mode so
|
||||||
|
the reap bar below is the single footer action in that flow. */}
|
||||||
|
{isClaw && clawAgent && !selectMode ? (
|
||||||
|
<div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 6 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={proposeLevelUp}
|
||||||
|
disabled={levelUpBusy}
|
||||||
|
title={`Ask the proposer to suggest improvements to ${clawAgent.name}'s identity, skills, and brain`}
|
||||||
|
style={{ width: "100%", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 7, padding: "9px 0", borderRadius: 9, border: "1px solid rgba(201,160,255,.45)", background: "rgba(201,160,255,.1)", color: "#c9a0ff", fontFamily: mono, fontSize: 11, letterSpacing: ".08em", textTransform: "uppercase", cursor: levelUpBusy ? "default" : "pointer", opacity: levelUpBusy ? 0.5 : 1 }}
|
||||||
|
>
|
||||||
|
<Sparkles aria-hidden size={13} />
|
||||||
|
{levelUpBusy ? "Proposing…" : `Level up ${clawAgent.name}`}
|
||||||
|
</button>
|
||||||
|
{levelUpError ? (
|
||||||
|
<div style={{ color: "#ff8a7a", fontSize: 11 }}>{levelUpError}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{selectMode ? (
|
{selectMode ? (
|
||||||
<div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
|
<div style={{ flex: "none", borderTop: "1px solid rgba(255,255,255,.08)", padding: "10px 12px", display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
<div style={{ fontFamily: mono, fontSize: 11, color: selectedItems.length ? "#ff8a7a" : "#6a6a72" }}>{selectedItems.length} selected{selectedItems.length ? ` · ${reapKind}` : ""}</div>
|
<div style={{ fontFamily: mono, fontSize: 11, color: selectedItems.length ? "#ff8a7a" : "#6a6a72" }}>{selectedItems.length} selected{selectedItems.length ? ` · ${reapKind}` : ""}</div>
|
||||||
@@ -1352,6 +1394,9 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
{historyOpen && clawAgent ? <BrainHistoryModal clawId={clawAgent.id} clawName={clawAgent.name} onClose={() => setHistoryOpen(false)} onRolledBack={() => setEnrichBump((b) => b + 1)} /> : null}
|
{historyOpen && clawAgent ? <BrainHistoryModal clawId={clawAgent.id} clawName={clawAgent.name} onClose={() => setHistoryOpen(false)} onRolledBack={() => setEnrichBump((b) => b + 1)} /> : null}
|
||||||
{(() => { const sel = allNodes.find((n) => n.id === worldSel); return runsOpen && sel?.level === "team" ? <TeamRunsModal teamId={sel.id} teamName={sel.label} onClose={() => setRunsOpen(false)} /> : null; })()}
|
{(() => { const sel = allNodes.find((n) => n.id === worldSel); return runsOpen && sel?.level === "team" ? <TeamRunsModal teamId={sel.id} teamName={sel.label} onClose={() => setRunsOpen(false)} /> : null; })()}
|
||||||
{reapOpen ? <ReapProgressModal items={selectedItems} kind={reapKind} onClose={() => setReapOpen(false)} onDone={() => { setReapOpen(false); setSelectMode(false); setSelectedAgents(new Set()); router.refresh(); }} /> : null}
|
{reapOpen ? <ReapProgressModal items={selectedItems} kind={reapKind} onClose={() => setReapOpen(false)} onDone={() => { setReapOpen(false); setSelectMode(false); setSelectedAgents(new Set()); router.refresh(); }} /> : null}
|
||||||
|
{/* Level-up review drawer. Mounted here rather than inside the sidebar
|
||||||
|
footer so it is not clipped by the sidebar's own bounds. */}
|
||||||
|
{levelUpOpenId ? <LevelUpDrawer proposalId={levelUpOpenId} onClose={() => setLevelUpOpenId(null)} /> : null}
|
||||||
|
|
||||||
{/* Group existing claws into a new named team, then land on its team page. */}
|
{/* Group existing claws into a new named team, then land on its team page. */}
|
||||||
|
|
||||||
|
|||||||
@@ -59,11 +59,18 @@ export function RepoList({
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [busyId, setBusyId] = useState<string | null>(null);
|
const [busyId, setBusyId] = useState<string | null>(null);
|
||||||
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
||||||
// Collapsed-org state is per-<connection_id>::<owner> — the same owner
|
// Org fold state is per-<connection_id>::<owner> — the same owner string
|
||||||
// string appearing under two different providers folds independently.
|
// appearing under two different providers folds independently.
|
||||||
const [collapsedOrgs, setCollapsedOrgs] = useState<Set<string>>(new Set());
|
//
|
||||||
|
// Tracks EXPANDED, not collapsed, so the empty initial set means every org
|
||||||
|
// starts folded: with 180+ repos across a dozen orgs, an all-expanded default
|
||||||
|
// buries the org names the list is meant to be navigated by. Inverting the set
|
||||||
|
// (rather than seeding it with every key on load) also keeps the default
|
||||||
|
// correct for orgs that arrive later from a sync, which a seeded set would
|
||||||
|
// render expanded.
|
||||||
|
const [expandedOrgs, setExpandedOrgs] = useState<Set<string>>(new Set());
|
||||||
const toggleOrg = (key: string) =>
|
const toggleOrg = (key: string) =>
|
||||||
setCollapsedOrgs((prev) => {
|
setExpandedOrgs((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(key)) next.delete(key);
|
if (next.has(key)) next.delete(key);
|
||||||
else next.add(key);
|
else next.add(key);
|
||||||
@@ -342,7 +349,7 @@ export function RepoList({
|
|||||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||||
{groupByOwner(list).map(([owner, orgRepos]) => {
|
{groupByOwner(list).map(([owner, orgRepos]) => {
|
||||||
const key = `${c.id}::${owner}`;
|
const key = `${c.id}::${owner}`;
|
||||||
const collapsed = collapsedOrgs.has(key);
|
const collapsed = !expandedOrgs.has(key);
|
||||||
return (
|
return (
|
||||||
<OrgGroup
|
<OrgGroup
|
||||||
key={key}
|
key={key}
|
||||||
|
|||||||
Reference in New Issue
Block a user