diff --git a/apps/mesh/src/web/components/account-popover.tsx b/apps/mesh/src/web/components/account-popover.tsx index 93fcfdeb81..bef4e3195e 100644 --- a/apps/mesh/src/web/components/account-popover.tsx +++ b/apps/mesh/src/web/components/account-popover.tsx @@ -45,6 +45,7 @@ import { clearPersistedQueryCache } from "@/web/lib/query-persist"; import { CreateOrganizationDialog } from "@/web/components/create-organization-dialog"; import { usePreferences, type ThemeMode } from "@/web/hooks/use-preferences.ts"; import { toast } from "@deco/ui/components/sonner.js"; +import { USER_AGENTS } from "@/web/views/deco-redesign/mock-user"; function getOrgColorStyle(name: string): { backgroundColor: string; @@ -184,12 +185,12 @@ function OrganizationsPanel({ value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => e.key === "Escape" && toggleSearch()} - placeholder="Search organizations..." + placeholder="Search agents..." className="flex-1 min-w-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground/60" /> ) : ( - Your Organizations + Your Agents )}
@@ -204,9 +205,7 @@ function OrganizationsPanel({
{filtered.length === 0 && (

- {query - ? `No organizations match "${query}"` - : "No organizations available"} + {query ? `No agents match "${query}"` : "No agents available"}

)} {filtered.map((org) => ( @@ -538,22 +537,30 @@ export function AccountPopover() { const user = session?.user; const userImage = (user as { image?: string } | undefined)?.image; - const currentOrg = organizations?.find( - (o: { slug: string }) => o.slug === orgParam, - ); - const sortedOrgs = [...(organizations ?? [])].sort((a, b) => { if (a.slug === orgParam) return -1; if (b.slug === orgParam) return 1; return a.name.localeCompare(b.name); }); + // The switcher lists the user's agents: their real orgs (each an agent) plus + // the mock teammates (Deco, Farm Rio, Monte Carlo). + const agentList = [ + ...sortedOrgs, + ...USER_AGENTS.map((a) => ({ + id: a.id, + name: a.name, + slug: a.id, + logo: a.icon ?? null, + })), + ]; + const handleSelectOrg = (orgSlug: string) => { setOpen(false); - navigate({ - to: "/$org", - params: { org: orgSlug }, - }); + // Only real orgs are navigable; mock agents are illustrative. + if (organizations?.some((o: { slug: string }) => o.slug === orgSlug)) { + navigate({ to: "/$org", params: { org: orgSlug } }); + } }; const close = () => setOpen(false); @@ -636,7 +643,7 @@ export function AccountPopover() { themeOptions, preferences, setPreferences, - sortedOrgs, + sortedOrgs: agentList, orgParam, onSelectOrg: handleSelectOrg, onCreateOrg: () => { @@ -658,26 +665,22 @@ export function AccountPopover() { className="flex items-center gap-3 w-full px-3 py-2.5 rounded-lg transition-colors text-sm text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground" >
- {currentOrg?.logo ? ( + {userImage ? ( ) : ( - {(currentOrg?.name ?? "?").slice(0, 2).toUpperCase()} + {(user?.name ?? "?").slice(0, 2).toUpperCase()} )}
- {currentOrg?.name ?? "Account"} + {user?.name ?? "Account"} Account @@ -688,30 +691,28 @@ export function AccountPopover() {
- {currentOrg?.logo ? ( + {userImage ? ( ) : ( - {(currentOrg?.name ?? "?").slice(0, 2).toUpperCase()} + {(user?.name ?? "?").slice(0, 2).toUpperCase()} )}
- {currentOrg?.name ?? "Account"} + {user?.name ?? "Account"}
diff --git a/apps/mesh/src/web/components/chat/input.tsx b/apps/mesh/src/web/components/chat/input.tsx index 8a47bae5ca..9ef0caab01 100644 --- a/apps/mesh/src/web/components/chat/input.tsx +++ b/apps/mesh/src/web/components/chat/input.tsx @@ -461,7 +461,7 @@ export function ChatInput({
); } + if (fallback.type === "tool-system_health_spike") { + return ; + } + if (fallback.type === "tool-system_health_fix") { + return ; + } + if (fallback.type === "tool-cms_content") { + return ; + } if (fallback.type.startsWith("tool-")) { const toolCallId = (fallback as ToolUIPart).toolCallId; const meta = dataParts.toolMetadata.get(toolCallId); diff --git a/apps/mesh/src/web/components/chat/message/parts/tool-call-part/cms.tsx b/apps/mesh/src/web/components/chat/message/parts/tool-call-part/cms.tsx new file mode 100644 index 0000000000..9ffbf6d0f9 --- /dev/null +++ b/apps/mesh/src/web/components/chat/message/parts/tool-call-part/cms.tsx @@ -0,0 +1,234 @@ +// CMS tool UI — Deco's "makes" side, rendered inline in the chat like any tool +// call. One tool: tool-cms_content. It shows a CONTENT EDIT the way deco sites +// Content works — a prop diff on a page's section (or a global section): for each +// section, the fields that change (title, CTA, image, …) as before → after. This +// mirrors the real SectionsEditor (pages → sections → props), read-only, plus +// publish actions. Publishing writes the section block back (mocked via the +// redesign store so the home + sidebar reflect it). +import { useState } from "react"; +import type { ToolUIPart } from "ai"; +import { Button } from "@deco/ui/components/button.tsx"; +import { cn } from "@deco/ui/lib/utils.ts"; +import { ArrowRight, Check, Image01, LayoutAlt01 } from "@untitledui/icons"; +import type { + FieldChange, + SectionEdit, +} from "@/web/views/deco-redesign/mock-cms"; +import { + setIncidentState, + useOverrides, +} from "@/web/views/deco-redesign/mock-store"; +import type { + AutonomyMode, + IncidentState, +} from "@/web/views/deco-redesign/mock-data"; +import { unwrapResult } from "./utils.tsx"; + +interface ContentOutput { + proposalId: string; + seedState: IncidentState; + autonomy: AutonomyMode; + scope: string; + edits: SectionEdit[]; +} + +type Outcome = "published" | "editing" | "dismissed" | "reverted" | null; + +export function CmsContentEditPart({ part }: { part: ToolUIPart }) { + const data = unwrapResult(part.output); + const overrides = useOverrides(); + const [outcome, setOutcome] = useState(null); + if (!data) return null; + + const base = overrides[data.proposalId] ?? data.seedState; + const published = base === "resolved"; + + const act = (next: Exclude, state: IncidentState) => { + setOutcome(next); + setIncidentState(data.proposalId, state); + }; + + return ( +
+ {data.edits.map((edit, i) => ( + + ))} + + {published && ( + + + Published + + )} + + {outcome === null ? ( + + ) : ( +

{followupText(outcome)}

+ )} +
+ ); +} + +function SectionEditCard({ edit }: { edit: SectionEdit }) { + return ( +
+
+ + + {edit.global + ? "Global" + : `${edit.page}${edit.pagePath ? ` · ${edit.pagePath}` : ""}`} + + + + {edit.section} section + + {edit.op === "add" && ( + + New + + )} + {edit.global && ( + + {edit.appliesTo ?? "all pages"} + + )} +
+
+ {edit.changes.map((c) => ( + + ))} +
+
+ ); +} + +function FieldDiffRow({ change }: { change: FieldChange }) { + const isNew = change.before === "—"; + return ( +
+
+ {change.label} + + {change.path} + +
+ {change.type === "image" ? ( +
+ {!isNew && ( + <> + + + + )} + +
+ ) : ( +
+ {!isNew && ( + + {change.before} + + )} + {change.after} +
+ )} +
+ ); +} + +function ImageThumb({ + name, + src, + muted = false, +}: { + name: string; + src?: string; + muted?: boolean; +}) { + const [imgError, setImgError] = useState(false); + return ( +
+
+ {src && !imgError ? ( + setImgError(true)} + className="w-full h-full object-cover" + /> + ) : ( + + )} +
+ + {name} + +
+ ); +} + +function ContentActions({ + base, + onAct, +}: { + base: IncidentState; + onAct: (outcome: Exclude, state: IncidentState) => void; +}) { + if (base === "needs_review") { + return ( +
+ + + +
+ ); + } + if (base === "resolved") { + return ( + + ); + } + return null; +} + +function followupText(outcome: Exclude): string { + switch (outcome) { + case "published": + return "Publishing now — I'll write the section blocks back and schedule them as planned."; + case "editing": + return "Opening this in Content so you can tweak any field before it goes live."; + case "dismissed": + return "Got it, leaving the content as-is. I'll note the call so I don't re-propose this."; + case "reverted": + return "Reverted — the section is back to its previous props. Nothing else changed."; + } +} diff --git a/apps/mesh/src/web/components/chat/message/parts/tool-call-part/index.ts b/apps/mesh/src/web/components/chat/message/parts/tool-call-part/index.ts index fe9f6417c3..69d7725401 100644 --- a/apps/mesh/src/web/components/chat/message/parts/tool-call-part/index.ts +++ b/apps/mesh/src/web/components/chat/message/parts/tool-call-part/index.ts @@ -13,3 +13,8 @@ export { export { AgentCreatePart } from "./agent-create.tsx"; export { AgentListPart } from "./agent-list.tsx"; export { ConnectionListPart } from "./connection-list.tsx"; +export { + SystemHealthSpikePart, + SystemHealthFixPart, +} from "./system-health.tsx"; +export { CmsContentEditPart } from "./cms.tsx"; diff --git a/apps/mesh/src/web/components/chat/message/parts/tool-call-part/system-health.tsx b/apps/mesh/src/web/components/chat/message/parts/tool-call-part/system-health.tsx new file mode 100644 index 0000000000..08223864ce --- /dev/null +++ b/apps/mesh/src/web/components/chat/message/parts/tool-call-part/system-health.tsx @@ -0,0 +1,307 @@ +// System Health tool UIs — rendered inside the chat exactly like any other tool +// call (e.g. brand context). Two tools: +// tool-system_health_spike → the error/metric spike graph +// tool-system_health_fix → the proposed fix (PR + checks) + approve actions +// They read their data from `part.output` (mock today; a real System Health tool +// would emit the same shape). The fix part writes to the redesign mock-store so +// the home + sidebar reflect an approval/dismissal. +import { useState } from "react"; +import type { ToolUIPart } from "ai"; +import { Button } from "@deco/ui/components/button.tsx"; +import { cn } from "@deco/ui/lib/utils.ts"; +import { + Check, + CheckCircle, + ChevronDown, + Loading01, + RefreshCw01, + Tool01, + XCircle, +} from "@untitledui/icons"; +import { SpikeGraph } from "@/web/views/deco-redesign/primitives"; +import { + setIncidentState, + useOverrides, +} from "@/web/views/deco-redesign/mock-store"; +import type { IncidentState } from "@/web/views/deco-redesign/mock-data"; +import { unwrapResult } from "./utils.tsx"; + +interface SpikeOutput { + label: string; + points: number[]; + baseline: number; + tone: "destructive" | "warning" | "primary" | "muted"; +} + +export function SystemHealthSpikePart({ part }: { part: ToolUIPart }) { + const data = unwrapResult(part.output); + if (!data) return null; + return ( +
+
+ {data.label} · last 2h + baseline ~{data.baseline.toLocaleString()} +
+ +
+ ); +} + +interface FixOutput { + incidentId: string; + seedState: IncidentState; + pr: number; + title: string; + summary: string; + diff: string; + filesChanged: number; + additions: number; + deletions: number; + qa: "passed" | "running" | "failed"; + aiReview: "passed" | "flagged"; + autonomy: "inform" | "propose" | "auto"; +} + +type Outcome = "approved" | "handed" | "dismissed" | "undone" | null; + +export function SystemHealthFixPart({ part }: { part: ToolUIPart }) { + const fix = unwrapResult(part.output); + const overrides = useOverrides(); + const [outcome, setOutcome] = useState(null); + const [showDiff, setShowDiff] = useState(false); + if (!fix) return null; + + const base = overrides[fix.incidentId] ?? fix.seedState; + const published = base === "resolved"; + const qaRunning = fix.qa === "running"; + + const act = (next: Outcome, state: IncidentState) => { + setOutcome(next); + setIncidentState(fix.incidentId, state); + }; + + const hasActions = + outcome !== null || base === "needs_review" || base === "resolved"; + + return ( +
+ {/* What: the change, in plain language. */} +
+ + + {fix.title} + + {published ? ( + + Published + + ) : base === "needs_review" ? ( + + Needs review + + ) : ( + + #{fix.pr} + + )} +
+ +
+

+ {fix.summary} +

+ + {/* Trust: the checks that justify shipping, scannable up front. */} +
+ + + +
+ + {/* Detail: the diff, collapsed by default so it doesn't dominate. */} +
+ + {showDiff && ( +
+              {fix.diff}
+            
+ )} +
+
+ + {/* Decide — in a footer, part of the card (not floating below it). */} + {hasActions && ( +
+ {outcome === null ? ( + + ) : ( +

{followupText(outcome)}

+ )} +
+ )} +
+ ); +} + +function Signal({ + icon: Icon, + label, + tone, + spin = false, +}: { + icon: typeof CheckCircle; + label: string; + tone: "success" | "warning" | "destructive" | "muted"; + spin?: boolean; +}) { + const toneClass = + tone === "success" + ? "text-success" + : tone === "warning" + ? "text-warning" + : tone === "destructive" + ? "text-destructive" + : "text-muted-foreground"; + return ( + + + {label} + + ); +} + +function FixActions({ + base, + qaRunning, + onAct, +}: { + base: IncidentState; + qaRunning: boolean; + onAct: (outcome: Outcome, state: IncidentState) => void; +}) { + if (base === "needs_review") { + return ( +
+
+ + + +
+ {qaRunning && ( +

+ QA is still running — I'll publish automatically once it passes. +

+ )} +
+ ); + } + if (base === "resolved") { + // Already live — a quiet revert affordance, not a loud standalone button. + return ( +
+ + Live in production. + + +
+ ); + } + return null; +} + +function followupText(outcome: Exclude): string { + switch (outcome) { + case "approved": + return "Approved — publishing the fix now. I'll watch the error rate for the next hour and tell you if it doesn't settle."; + case "handed": + return "Handed to a developer with the diagnosis and the change linked. I'll track it and update this task when it moves."; + case "dismissed": + return "Got it — not a real issue. I'll raise the bar for this pattern so it won't page you again, and note it in memory."; + case "undone": + return "Reverted the change. The store is back to the previous state — nothing else changed."; + } +} diff --git a/apps/mesh/src/web/components/chat/store/hooks.tsx b/apps/mesh/src/web/components/chat/store/hooks.tsx index f90da733df..d334e46756 100644 --- a/apps/mesh/src/web/components/chat/store/hooks.tsx +++ b/apps/mesh/src/web/components/chat/store/hooks.tsx @@ -14,10 +14,12 @@ */ import { + getWellKnownDecopilotVirtualMCP, SELF_MCP_ALIAS_ID, useMCPClient, useProjectContext, } from "@decocms/mesh-sdk"; +import { buildMockTasks } from "@/web/views/deco-redesign/mock-threads"; import { createContext, type ReactNode, @@ -40,7 +42,15 @@ export function ThreadManagerProvider({ children }: { children: ReactNode }) { orgId: org.id, orgSlug: org.slug, }); - const manager = getOrOpenManager(org.slug, locator, { client }); + // Redesign mock: System Health findings as real tasks under the Deco agent. + const seeds = buildMockTasks(getWellKnownDecopilotVirtualMCP(org.id).id); + const manager = getOrOpenManager(org.slug, locator, { + client, + seedTasks: seeds, + }); + // Ensure the findings are present even if this manager instance predates the + // seeds (HMR / earlier construction). Idempotent — merges once. + manager.ensureSeeded(seeds); return ( {children} diff --git a/apps/mesh/src/web/components/chat/store/thread-connection.ts b/apps/mesh/src/web/components/chat/store/thread-connection.ts index 02dad1e9ec..4df3d48ab3 100644 --- a/apps/mesh/src/web/components/chat/store/thread-connection.ts +++ b/apps/mesh/src/web/components/chat/store/thread-connection.ts @@ -52,6 +52,12 @@ import type { ChatMode } from "../types"; import { toast } from "sonner"; import type { SandboxProviderKind } from "@decocms/sandbox/provider"; import type { HarnessId } from "@/harnesses"; +// Redesign mock: System Health findings are served locally (no backend) so the +// real chat renders their seeded messages verbatim. See mock-threads.ts. +import { + getMockMessages, + isMockThread, +} from "@/web/views/deco-redesign/mock-threads"; export { Store }; @@ -309,6 +315,13 @@ export class ThreadConnection { } this.messages.set(next); + // Redesign mock: local threads have no server — apply optimistically and + // stop (no POST to /messages, which would 404 for a non-persisted id). + if (isMockThread(this.threadId)) { + this.status.set({ kind: "ready" }); + return; + } + // A new user turn always POSTs. For approval / toolOutput actions, only // POST once the assistant turn has no remaining client-side resolutions // (other pending approvals or unresolved local tool inputs). The two @@ -366,6 +379,19 @@ export class ThreadConnection { // ── Internal: bootstrap ───────────────────────────────────────────────── private async bootstrap(): Promise { + // Redesign mock: a System Health finding is a local thread. Seed its + // messages and skip all backend I/O (no /messages list, no /stream SSE). + if (isMockThread(this.threadId)) { + const seed = (getMockMessages(this.threadId) ?? []) as UIMessage[]; + this.messages.set(seed); + this.serverFetchedCount = seed.length; + this.hasMoreOlder.set(false); + if (this.status.get().kind === "loading") { + this.status.set({ kind: "ready" }); + } + this.resolveReady(); + return; + } // Initial-page fetch and SSE loop run concurrently. Chunks arriving // before the page resolves are queued via `chunkBuffer` and drained // through `handleChunk` after the page lands. diff --git a/apps/mesh/src/web/components/chat/store/thread-manager-store.ts b/apps/mesh/src/web/components/chat/store/thread-manager-store.ts index f24b7cef24..dd5aebe690 100644 --- a/apps/mesh/src/web/components/chat/store/thread-manager-store.ts +++ b/apps/mesh/src/web/components/chat/store/thread-manager-store.ts @@ -23,6 +23,12 @@ export interface ThreadManagerStoreOptions { * shared with `useDecopilotEvents`. */ sse?: SSESubscription; + /** + * Redesign mock: local-only rows merged into the thread list on every load + * (and reconnect) so System Health findings appear as ordinary tasks. No + * server round-trip — see `views/deco-redesign/mock-threads.ts`. + */ + seedTasks?: Task[]; } export type ThreadsStatus = @@ -74,6 +80,8 @@ export class ThreadManagerStore { */ private archivedTombstones = new Map(); private client: MCPClient | null; + private seedTasks: Task[]; + private seeded = false; private nextOffset = 0; private readonly pageSize = 10; /** @@ -91,6 +99,7 @@ export class ThreadManagerStore { ) { this.key = `${orgSlug}::${locator}`; this.client = opts.client ?? null; + this.seedTasks = opts.seedTasks ?? []; const sse = opts.sse ?? decopilotSSE; this.watchUnsubscribe = sse.subscribe( this.orgSlug, @@ -200,6 +209,19 @@ export class ThreadManagerStore { ); } + /** + * Redesign mock: ensure the seed rows (System Health findings) are present + * exactly once, using the latest set from the provider. Idempotent across + * renders/HMR — safe to call on every render; it merges only on the first + * call per instance, so it can't loop. + */ + ensureSeeded(tasks: Task[]): void { + this.seedTasks = tasks; + if (this.seeded) return; + this.seeded = true; + if (tasks.length) this.mergeThreads(tasks); + } + private async optimisticUpdate( id: string, patch: ThreadUpdateData, @@ -279,6 +301,7 @@ export class ThreadManagerStore { // No MCP client — drain the buffer immediately so SSE events are // dispatched live (store stays in "loading" status until a client // is provided). + if (this.seedTasks.length) this.mergeThreads(this.seedTasks); this.drainEventBuffer(); return; } @@ -301,6 +324,7 @@ export class ThreadManagerStore { .structuredContent ?? result) as { items?: Task[]; hasMore?: boolean }; const items = payload.items ?? []; this.threads.set(items); + if (this.seedTasks.length) this.mergeThreads(this.seedTasks); this.hasMore.set(payload.hasMore ?? false); this.nextOffset = items.length; this.threadsStatus.set({ kind: "ready" }); diff --git a/apps/mesh/src/web/components/sidebar/footer/inbox.tsx b/apps/mesh/src/web/components/sidebar/footer/inbox.tsx index 286cf09f5a..3c7b929a17 100644 --- a/apps/mesh/src/web/components/sidebar/footer/inbox.tsx +++ b/apps/mesh/src/web/components/sidebar/footer/inbox.tsx @@ -234,7 +234,7 @@ function SettingsFullButton() { tooltip="Settings" onClick={() => navigate({ - to: "/$org/settings", + to: "/$org/settings/profile", params: { org: org.slug }, }) } @@ -255,7 +255,7 @@ function SettingsIconButton() { aria-label="Settings" onClick={() => navigate({ - to: "/$org/settings", + to: "/$org/settings/profile", params: { org: org.slug }, }) } diff --git a/apps/mesh/src/web/components/sidebar/task-groups/task-groups-list.tsx b/apps/mesh/src/web/components/sidebar/task-groups/task-groups-list.tsx index 95c88a15da..967cac4339 100644 --- a/apps/mesh/src/web/components/sidebar/task-groups/task-groups-list.tsx +++ b/apps/mesh/src/web/components/sidebar/task-groups/task-groups-list.tsx @@ -1,5 +1,5 @@ -import { useState, type ReactNode } from "react"; -import { Activity, FilterLines, SearchSm, Users01 } from "@untitledui/icons"; +import { type ReactNode, useState } from "react"; +import { FilterLines, SearchSm } from "@untitledui/icons"; import { Popover, PopoverContent, @@ -12,13 +12,8 @@ import { SelectTrigger, SelectValue, } from "@deco/ui/components/select.tsx"; -import { SidebarMenu, useSidebar } from "@deco/ui/components/sidebar.tsx"; -import { - getWellKnownDecopilotVirtualMCP, - useProjectContext, - useVirtualMCPActions, - useVirtualMCPs, -} from "@decocms/mesh-sdk"; +import { useSidebar } from "@deco/ui/components/sidebar.tsx"; +import { useProjectContext } from "@decocms/mesh-sdk"; import { useNavigate, useParams } from "@tanstack/react-router"; import { authClient } from "@/web/lib/auth-client"; import { @@ -30,30 +25,12 @@ import { usePanelActions } from "@/web/layouts/shell-layout"; import { GlobalSearchDialog } from "@/web/layouts/tasks-panel/global-search-dialog"; import { track } from "@/web/lib/posthog-client"; import type { Task } from "@/web/components/chat/task/types"; -import { useNavigateToAgent } from "@/web/hooks/use-navigate-to-agent"; -import { useCanPinAgentsForOrg } from "@/web/hooks/use-can-pin-agents-for-org"; import { ToolbarIconButton } from "@/web/components/toolbar-icon-button"; -import { BrowseAgentsButton } from "../browse-agents-button"; -import { - SyncSidebarAgentGroupsEmpty, - useSidebarOrderRevision, -} from "../sidebar-agent-groups-context"; -import { SortableCollapsedTaskGroups } from "./sortable-collapsed-task-groups"; -import { - groupThreadsByVirtualMcp, - groupThreadsByStatus, - TOOL_CALL_RUNS_GROUP_KEY, -} from "./group-threads"; -import { removeGroupFromOrder, syncOrdersOnOrgPinToggle } from "./stable-order"; -import { SortableTaskGroups } from "./sortable-task-groups"; -import { StatusGroup } from "./task-group"; -import type { SidebarFilters } from "./next-page-offset"; -import { buildGroupThreadCounts } from "./next-page-offset"; -import { useSidebarGroupOrder } from "./use-sidebar-group-order"; +import { TaskRow } from "@/web/layouts/tasks-panel/task-row"; +// Single-agent model: the sidebar is a flat list of TASKS — no agent grouping. type TypeFilter = "all" | "manual" | "automation"; type MemberFilter = "all" | "mine"; -type GroupBy = "agent" | "status"; const TYPE_LABELS: Record = { all: "All tasks", @@ -66,123 +43,45 @@ const MEMBER_LABELS: Record = { mine: "Mine only", }; -const GROUP_BY_LABELS: Record = { - agent: "Agent", - status: "Status", -}; - export function TaskGroupsList() { const { data: session } = authClient.useSession(); const currentUserId = session?.user?.id; - const sidebarUserId = currentUserId ?? "anon"; const { org } = useProjectContext(); - const decopilotId = getWellKnownDecopilotVirtualMCP(org.id).id; - const agents = useVirtualMCPs(); - const serverOrgPinnedIds = agents.filter((a) => a.pinned).map((a) => a.id); - const canManageOrgPin = useCanPinAgentsForOrg(); - const virtualMcpActions = useVirtualMCPActions(); - - const [orgPinOverrides, setOrgPinOverrides] = useState< - Record - >({}); - const serverOrgPinnedSet = new Set(serverOrgPinnedIds); - - // Partition overrides into active (server hasn't confirmed yet) vs confirmed. - // Confirmed entries are pruned from state so they can't reactivate if server - // data fluctuates (cache miss, background refetch, etc.). - const activeOverrides: Record = {}; - const confirmedKeys: string[] = []; - for (const [id, pinned] of Object.entries(orgPinOverrides)) { - if (serverOrgPinnedSet.has(id) !== pinned) { - activeOverrides[id] = pinned; - } else { - confirmedKeys.push(id); - } - } - if (confirmedKeys.length > 0) { - setOrgPinOverrides((prev) => { - const next = { ...prev }; - for (const k of confirmedKeys) delete next[k]; - return next; - }); - } - const orgPinnedIds = (() => { - const set = new Set(serverOrgPinnedIds); - for (const [id, pinned] of Object.entries(activeOverrides)) { - if (pinned) set.add(id); - else set.delete(id); - } - return [...set]; - })(); - const orgPinnedSet = new Set(orgPinnedIds); const { threads: allThreads } = useThreads(); const visibleThreads = filterThreads(allThreads, { hidden: false }); const { hide } = useThreadActions(); const navigate = useNavigate(); - const { setTaskId, createNewTask } = usePanelActions(); - const params = useParams({ strict: false }) as { - taskId?: string; - }; + const { setTaskId } = usePanelActions(); + const params = useParams({ strict: false }) as { taskId?: string }; const activeTaskId = params.taskId ?? null; - const sortedThreads = [...visibleThreads].sort((a, b) => - (b.updated_at ?? "").localeCompare(a.updated_at ?? ""), - ); - const [typeFilter, setTypeFilter] = useState("all"); - const [memberFilter, setMemberFilter] = useState("mine"); - const [groupBy, setGroupBy] = useState("agent"); + const [memberFilter, setMemberFilter] = useState("all"); const [searchOpen, setSearchOpen] = useState(false); const [searchEverOpened, setSearchEverOpened] = useState(false); - const [localOrderRevision, setLocalOrderRevision] = useState(0); - const contextOrderRevision = useSidebarOrderRevision(); - const orderRevision = localOrderRevision + contextOrderRevision; - const orderScope = { orgId: org.id, userId: sidebarUserId }; - - const filters: SidebarFilters = { - type: typeFilter, - member: memberFilter, - currentUserId: currentUserId ?? null, - }; - - const agentThreadCounts = buildGroupThreadCounts( - sortedThreads, - "agent", - filters, - ); - - const groups = useSidebarGroupOrder( - orderScope, - groupThreadsByVirtualMcp(sortedThreads, decopilotId), - decopilotId, - orgPinnedIds, - orderRevision, - ); - - const memberFiltered = (threads: Task[]) => - memberFilter === "mine" && currentUserId - ? threads.filter((t) => t.created_by === currentUserId) - : threads; - const typeFiltered = (threads: Task[]) => { - if (typeFilter === "automation") { - return threads.filter((t) => Boolean(t.trigger_id)); - } - if (typeFilter === "manual") { - return threads.filter((t) => !t.trigger_id); - } - return threads; - }; + const tasks = visibleThreads + .filter((t) => + memberFilter === "mine" && currentUserId + ? t.created_by === currentUserId + : true, + ) + .filter((t) => + typeFilter === "automation" + ? Boolean(t.trigger_id) + : typeFilter === "manual" + ? !t.trigger_id + : true, + ) + .sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")); const handleArchive = (task: Task) => { const wasActive = task.id === activeTaskId; hide(task.id); if (!wasActive) return; - const next = sortedThreads.find( - (t) => t.id !== task.id && t.virtual_mcp_id === task.virtual_mcp_id, - ); + const next = tasks.find((t) => t.id !== task.id); if (next) { setTaskId(next.id, next.virtual_mcp_id); } else { @@ -190,244 +89,98 @@ export function TaskGroupsList() { } }; - const handleNewInGroup = (virtualMcpId: string) => { - track("sidebar_group_new_clicked", { virtual_mcp_id: virtualMcpId }); - createNewTask(virtualMcpId); - }; - - const navigateToAgent = useNavigateToAgent(); - const handleShowSettings = (virtualMcpId: string) => { - track("sidebar_group_settings_clicked", { virtual_mcp_id: virtualMcpId }); - navigateToAgent(virtualMcpId, { search: { main: "instructions" } }); - }; - - const handleHideGroup = (virtualMcpId: string) => { - if (orgPinnedSet.has(virtualMcpId)) return; - track("sidebar_group_hide_clicked", { virtual_mcp_id: virtualMcpId }); - const group = groups.find((g) => g.virtualMcpId === virtualMcpId); - if (group) { - for (const t of group.threads) hide(t.id); - } - removeGroupFromOrder(orderScope, virtualMcpId, orgPinnedIds); - setLocalOrderRevision((n) => n + 1); - }; - - const handleToggleOrgPin = async (virtualMcpId: string, pinned: boolean) => { - if (!canManageOrgPin) return; - if (activeOverrides[virtualMcpId] !== undefined) return; - track("sidebar_group_org_pin_toggled", { - virtual_mcp_id: virtualMcpId, - pinned, - }); - setOrgPinOverrides((prev) => ({ ...prev, [virtualMcpId]: pinned })); - try { - await virtualMcpActions.update.mutateAsync({ - id: virtualMcpId, - data: { pinned }, - }); - // The override is pruned from state automatically on the next render once - // serverOrgPinnedIds reflects the change — no explicit cleanup needed here. - } catch { - syncOrdersOnOrgPinToggle(orderScope, virtualMcpId, !pinned); - setOrgPinOverrides((prev) => { - const next = { ...prev }; - delete next[virtualMcpId]; - return next; - }); - } - }; - - const groupContextMenuProps = (virtualMcpId: string) => { - const isNonAgentGroup = - virtualMcpId === decopilotId || virtualMcpId === TOOL_CALL_RUNS_GROUP_KEY; - return { - isOrgPinned: orgPinnedSet.has(virtualMcpId), - canManageOrgPin: isNonAgentGroup ? false : canManageOrgPin, - onToggleOrgPin: isNonAgentGroup ? undefined : handleToggleOrgPin, - }; - }; - - const buildAgentGroupRenderProps = (group: (typeof groups)[number]) => { - const filtered = typeFiltered(memberFiltered(group.threads)); - return { - virtualMcpId: group.virtualMcpId, - threads: filtered, - activeTaskId, - filters, - groupVisibleCount: agentThreadCounts.get(group.virtualMcpId) ?? 0, - onSelectTask: (t: Task) => setTaskId(t.id, t.virtual_mcp_id), - onArchiveTask: handleArchive, - onNewTaskInGroup: handleNewInGroup, - onShowSettings: handleShowSettings, - onHideGroup: handleHideGroup, - ...groupContextMenuProps(group.virtualMcpId), - }; - }; - - const filtersActive = typeFilter !== "all" || memberFilter !== "mine"; + const filtersActive = typeFilter !== "all" || memberFilter !== "all"; const { state: sidebarState, isMobile } = useSidebar(); - const isCollapsed = sidebarState === "collapsed" && !isMobile; - if (isCollapsed) { - const visibleGroups = groups.filter((group) => { - const filtered = typeFiltered(memberFiltered(group.threads)); - return !(filtersActive && filtered.length === 0); - }); - - return ( - <> - - - setLocalOrderRevision((n) => n + 1)} - renderGroup={(group) => buildAgentGroupRenderProps(group)} - /> - - - ); - } + // Collapsed rail shows only nav icons — no task list. + if (isCollapsed) return null; return (
- -
-
- { - track("tasks_panel_search_opened"); - setSearchEverOpened(true); - setSearchOpen(true); - }} - > - - - { - const next: GroupBy = groupBy === "agent" ? "status" : "agent"; - track("tasks_panel_group_by_changed", { to_value: next }); - setGroupBy(next); - }} +
+ { + track("tasks_panel_search_opened"); + setSearchEverOpened(true); + setSearchOpen(true); + }} + > + + + + + + + {filtersActive && ( + + )} + + + - {groupBy === "agent" ? ( - - ) : ( - - )} - - - - - - {filtersActive && ( - - )} - - - -
- - - - - setMemberFilter(v as MemberFilter)} + > + + + + + {(Object.keys(MEMBER_LABELS) as MemberFilter[]).map( + (opt) => ( - {TYPE_LABELS[opt]} + {MEMBER_LABELS[opt]} - ))} - - - -
-
-
-
- + ), + )} + + + + + + +
+ +
- {groupBy === "status" ? ( - <> - {groupThreadsByStatus( - typeFiltered(memberFiltered(sortedThreads)), - ).map((group) => ( - setTaskId(t.id, t.virtual_mcp_id)} - onArchiveTask={handleArchive} - filters={filters} - /> - ))} - + {tasks.length === 0 ? ( +
+ No tasks yet +
) : ( - setLocalOrderRevision((n) => n + 1)} - renderGroup={(group) => ({ - ...buildAgentGroupRenderProps(group), - })} - /> + tasks.map((task) => ( + setTaskId(task.id, task.virtual_mcp_id)} + onArchive={() => handleArchive(task)} + showAutomationBadge={Boolean(task.trigger_id)} + /> + )) )}
{searchEverOpened && ( diff --git a/apps/mesh/src/web/hooks/use-project-sidebar-items.tsx b/apps/mesh/src/web/hooks/use-project-sidebar-items.tsx index e6915eb9ef..d7a5dad9d5 100644 --- a/apps/mesh/src/web/hooks/use-project-sidebar-items.tsx +++ b/apps/mesh/src/web/hooks/use-project-sidebar-items.tsx @@ -4,14 +4,7 @@ import type { SidebarSection, } from "@/web/components/sidebar/types"; import { useNavigate, useRouterState } from "@tanstack/react-router"; -import { Home01 } from "@untitledui/icons"; -import { - SidebarGroup, - SidebarGroupContent, - SidebarMenu, - useSidebar, -} from "@deco/ui/components/sidebar.tsx"; -import { BrowseAgentsButton } from "@/web/components/sidebar/browse-agents-button"; +import { Home01, Inbox01, LayoutAlt01, Target04 } from "@untitledui/icons"; export function useProjectSidebarItems(): SidebarSection[] { const { org } = useProjectContext(); @@ -19,8 +12,6 @@ export function useProjectSidebarItems(): SidebarSection[] { const routerState = useRouterState(); const pathname = routerState.location.pathname; const slug = org.slug; - const { state, isMobile } = useSidebar(); - const isCollapsed = !isMobile && state === "collapsed"; const homeItem: NavigationSidebarItem = { key: "home", @@ -32,23 +23,37 @@ export function useProjectSidebarItems(): SidebarSection[] { }, }; - const sections: SidebarSection[] = [{ type: "items", items: [homeItem] }]; + const goalsItem: NavigationSidebarItem = { + key: "goals", + label: "Goals", + icon: , + isActive: pathname.startsWith(`/${slug}/goal`), + onClick: () => { + navigate({ to: "/$org/goal", params: { org: slug }, search: {} }); + }, + }; + + const inboxItem: NavigationSidebarItem = { + key: "inbox", + label: "Inbox", + icon: , + isActive: pathname === `/${slug}/inbox`, + onClick: () => { + navigate({ to: "/$org/inbox", params: { org: slug } }); + }, + }; - if (isCollapsed) { - sections.push({ - type: "custom", - key: "new-task", - content: ( - - - - - - - - ), - }); - } + const contentItem: NavigationSidebarItem = { + key: "content", + label: "Content", + icon: , + isActive: pathname === `/${slug}/content`, + onClick: () => { + navigate({ to: "/$org/content", params: { org: slug } }); + }, + }; - return sections; + return [ + { type: "items", items: [homeItem, goalsItem, inboxItem, contentItem] }, + ]; } diff --git a/apps/mesh/src/web/index.tsx b/apps/mesh/src/web/index.tsx index b3c2ed040a..c4c0cfdc9d 100644 --- a/apps/mesh/src/web/index.tsx +++ b/apps/mesh/src/web/index.tsx @@ -19,7 +19,6 @@ import type { ReactNode } from "react"; import "../../index.css"; import { listOrganizationsCached } from "@/web/lib/auth-client"; -import { LOCALSTORAGE_KEYS } from "@/web/lib/localstorage-keys"; import { sourcePlugins } from "./plugins.ts"; import type { @@ -114,42 +113,13 @@ const shellLayout = createRoute({ component: lazyRouteComponent(() => import("./layouts/shell-layout.tsx")), }); -// Home route (landing, redirects to last or only org) +// Home route (landing) — redesign: you land in your personal space (/me), +// above any org (like chatgpt.com). Entering an Agent from there goes to /$org. const homeRoute = createRoute({ getParentRoute: () => shellLayout, path: "/", - beforeLoad: async () => { - // Fast path: redirect returning users immediately from the cached slug, - // WITHOUT awaiting the org-list network call. This is what keeps a cold - // load from blocking on a round-trip (the previous blank/white screen). - // The org layout validates membership via getFullOrganization, and a stale - // slug self-heals in OrgAccessGate (clears the slug + bounces back to "/"). - const lastOrgSlug = localStorage.getItem(LOCALSTORAGE_KEYS.lastOrgSlug()); - if (lastOrgSlug) { - throw redirect({ - to: "/$org", - params: { org: lastOrgSlug }, - }); - } - - // No cached slug — fetch the list (cached) to pick a destination. - const { data: orgs } = await listOrganizationsCached(); - - // If the list call failed, skip redirect logic to avoid a misfire on a - // transient API failure. Archived orgs are already filtered by the helper. - if (!orgs) return; - - // Redirect to first available org (every user gets a default org on signup) - const firstOrg = orgs[0]; - if (firstOrg) { - throw redirect({ - to: "/$org", - params: { org: firstOrg.slug }, - }); - } - - // No orgs at all — send to onboarding - throw redirect({ to: "/onboarding" }); + beforeLoad: () => { + throw redirect({ to: "/me" }); }, }); @@ -167,6 +137,15 @@ const onboardingRoute = createRoute({ component: lazyRouteComponent(() => import("./routes/onboarding.tsx")), }); +// Redesign: the USER's personal space (/me) — above any org, like chatgpt.com. +// Standalone full-screen (under rootRoute, no org/ProjectContext needed). Your +// personal agent + your connections + your Agents (entering one → /$org). +const personalHomeRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/me", + component: lazyRouteComponent(() => import("./routes/me.tsx")), +}); + // ============================================ // ORG LAYOUT // ============================================ @@ -177,6 +156,14 @@ const orgLayout = createRoute({ component: lazyRouteComponent(() => import("./layouts/org-layout.tsx")), }); +// Redesign mock: a standalone onboarding flow at /$org/onboarding (full-screen, +// no shell chrome). Static path → never collides with /$org/$taskId. +const onboardingDemoRoute = createRoute({ + getParentRoute: () => orgLayout, + path: "/onboarding", + component: lazyRouteComponent(() => import("./routes/orgs/onboarding.tsx")), +}); + // ============================================ // ORG SHELL LAYOUT (pathless — sidebar + Toolbar + ChatPrefsProvider for / and /$taskId) // ============================================ @@ -233,9 +220,44 @@ const unifiedChatRoute = createRoute({ const orgIndexRoute = createRoute({ getParentRoute: () => orgShellLayout, path: "/", + validateSearch: z.lazy(() => + z.object({ + // Redesign: when set, the home opens this finding as an incident task in place. + task: z.string().optional(), + // Redesign: when "1", the home opens the New Task composer dialog. + new: z.string().optional(), + }), + ), component: lazyRouteComponent(() => import("./layouts/org-home/index.tsx")), }); +// Redesign: the Inbox / Findings page (/$org/inbox) — inside the org shell, so +// it has the sidebar + toolbar. Static path, never collides with /$org/$taskId. +const inboxRoute = createRoute({ + getParentRoute: () => orgShellLayout, + path: "/inbox", + component: lazyRouteComponent(() => import("./routes/orgs/inbox.tsx")), +}); + +// Redesign: the storefront content / CMS (/$org/content) — inside the shell. +const contentRoute = createRoute({ + getParentRoute: () => orgShellLayout, + path: "/content", + component: lazyRouteComponent(() => import("./routes/orgs/content.tsx")), +}); + +// Redesign: a goal's closed-loop detail (/$org/goal?g=) — inside the org +// shell. SINGLE static segment + a search param for the id, so it ranks like +// `/inbox` and never collides with the 2-segment `/$taskId/$pluginId` chat route +// (a `/goal/$goalId` path loses that precedence fight and falls through to the +// empty task view). +const goalRoute = createRoute({ + getParentRoute: () => orgShellLayout, + path: "/goal", + validateSearch: z.lazy(() => z.object({ g: z.string().optional() })), + component: lazyRouteComponent(() => import("./routes/orgs/goal.tsx")), +}); + // ============================================ // SETTINGS LAYOUT (/$org/settings) // ============================================ @@ -319,6 +341,71 @@ const monitoringRoute = createRoute({ ), }); +// Agent settings overview — the home of everything about one agent +// `?agent` selects which of the user's agents to show (current org by default). +const agentSearchSchema = z.lazy(() => + z.object({ agent: z.string().optional() }), +); + +const settingsAgentRoute = createRoute({ + getParentRoute: () => settingsLayout, + path: "/agent", + component: lazyRouteComponent( + () => import("./routes/orgs/settings/agent.tsx"), + ), + validateSearch: agentSearchSchema, +}); + +// Agent personalization — the editable user layer (guidance, skills, connections) +const settingsAgentPersonalizationRoute = createRoute({ + getParentRoute: () => settingsLayout, + path: "/agent/personalization", + component: lazyRouteComponent( + () => import("./routes/orgs/settings/agent-personalization.tsx"), + ), + validateSearch: agentSearchSchema, +}); + +// Agent automations — Studio automations split System (managed) / Yours +const settingsAgentAutomationsRoute = createRoute({ + getParentRoute: () => settingsLayout, + path: "/agent/automations", + component: lazyRouteComponent( + () => import("./routes/orgs/settings/agent-automations.tsx"), + ), + validateSearch: agentSearchSchema, +}); + +// Agent findings — what the agent watches and how far it acts (agent-wide) +const settingsAgentFindingsRoute = createRoute({ + getParentRoute: () => settingsLayout, + path: "/agent/findings", + component: lazyRouteComponent( + () => import("./routes/orgs/settings/agent-findings.tsx"), + ), + validateSearch: agentSearchSchema, +}); + +// Agent memory — what the agent remembers about you and the work +const settingsAgentMemoryRoute = createRoute({ + getParentRoute: () => settingsLayout, + path: "/agent/memory", + component: lazyRouteComponent( + () => import("./routes/orgs/settings/agent-memory.tsx"), + ), + validateSearch: agentSearchSchema, +}); + +// Agent files — what the agent reads and what it produces +const settingsAgentFilesRoute = createRoute({ + getParentRoute: () => settingsLayout, + path: "/agent/files", + component: lazyRouteComponent( + () => import("./routes/orgs/settings/agent-files.tsx"), + ), + validateSearch: agentSearchSchema, +}); + // Organization settings pages const settingsGeneralRoute = createRoute({ getParentRoute: () => settingsLayout, @@ -328,6 +415,14 @@ const settingsGeneralRoute = createRoute({ ), }); +const settingsFindingsRoute = createRoute({ + getParentRoute: () => settingsLayout, + path: "/findings", + component: lazyRouteComponent( + () => import("./routes/orgs/settings/findings.tsx"), + ), +}); + const settingsFeaturesRoute = createRoute({ getParentRoute: () => settingsLayout, path: "/features", @@ -540,12 +635,19 @@ const unifiedPluginWithChildren = unifiedPluginRoute.addChildren(pluginRoutes); const settingsWithChildren = settingsLayout.addChildren([ settingsIndexRoute, + settingsAgentRoute, + settingsAgentPersonalizationRoute, + settingsAgentAutomationsRoute, + settingsAgentFindingsRoute, + settingsAgentMemoryRoute, + settingsAgentFilesRoute, connectionsRoute, connectionDetailRoute, collectionDetailRoute, settingsAgentsRoute, settingsAutomationsRoute, monitoringRoute, + settingsFindingsRoute, settingsGeneralRoute, settingsFeaturesRoute, settingsBrandContextRoute, @@ -574,12 +676,16 @@ const agentShellWithChildren = agentShellLayout.addChildren([ const orgShellWithChildren = orgShellLayout.addChildren([ orgIndexRoute, + inboxRoute, + contentRoute, + goalRoute, agentShellWithChildren, ]); const orgLayoutWithChildren = orgLayout.addChildren([ orgShellWithChildren, settingsWithChildren, + onboardingDemoRoute, ]); const shellRouteTree = shellLayout.addChildren([ @@ -590,6 +696,7 @@ const shellRouteTree = shellLayout.addChildren([ const routeTree = rootRoute.addChildren([ shellRouteTree, onboardingRoute, + personalHomeRoute, loginRoute, cliAuthSuccessRoute, resetPasswordRoute, diff --git a/apps/mesh/src/web/layouts/home-page/background.tsx b/apps/mesh/src/web/layouts/home-page/background.tsx index 741c816eb0..dfea0f404c 100644 --- a/apps/mesh/src/web/layouts/home-page/background.tsx +++ b/apps/mesh/src/web/layouts/home-page/background.tsx @@ -1,14 +1,45 @@ /** - * Faded decorative corners for the home. Two SVGs (top-left and - * bottom-right) anchored to their respective corners, rendered at their - * natural viewBox size — no stretching or cropping by CSS, and crisp at - * any zoom level since SVG. Light/dark variants swap via Tailwind. + * Faded decorative graphics for the home, rendered at their natural viewBox + * size — crisp at any zoom since SVG. Light/dark variants swap via Tailwind. + * + * `variant`: + * - "corners" (default): top-left + bottom-right corner motifs. + * - "left": a single accent hugging the LEFT edge, vertically centered and + * faint, so it never sits behind the heading text (used by the redesign + * home where the brief reads over it). */ const TOP_LEFT_WIDTH_PX = 420; // ~50% of viewBox (834) const BOTTOM_RIGHT_WIDTH_PX = 305; // ~50% of viewBox (610) +const LEFT_WIDTH_PX = 360; + +export function HomeBackground({ + variant = "corners", +}: { + variant?: "corners" | "left"; +}) { + if (variant === "left") { + return ( +
+ + +
+ ); + } -export function HomeBackground() { return (
diff --git a/apps/mesh/src/web/layouts/org-home/index.tsx b/apps/mesh/src/web/layouts/org-home/index.tsx index c56f98e731..94f1256d6c 100644 --- a/apps/mesh/src/web/layouts/org-home/index.tsx +++ b/apps/mesh/src/web/layouts/org-home/index.tsx @@ -1,22 +1,56 @@ /** - * OrgHome — leaf component for /$org/. Renders HomePage inside the same - * panel chrome the chat surface uses, full-bleed (no chat-main split). + * OrgHome — leaf component for /$org/. * - * No Chat.Provider, no ActiveTaskProvider — the home composer is wired - * to the home submit path (URL autosend handoff) via Chat.Input's - * optional-context fallback. + * Redesign: Deco's brief (System Health). Findings are REAL threads (seeded + * into the thread manager), so opening one navigates to the real chat route + * `/$org/$taskId` — the normal task/thread UI, rendering the finding's seeded + * messages + tool UIs. `?new=1` opens the New Task composer (real Chat.Input). */ +import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; import { useIsMobile } from "@deco/ui/hooks/use-mobile.ts"; -import { HomePage } from "@/web/layouts/home-page"; +import { HomeBackground } from "@/web/layouts/home-page/background"; +import { RedesignHome } from "@/web/views/deco-redesign/home"; +import { NewTaskDialog } from "@/web/views/deco-redesign/new-task-dialog"; export default function OrgHome() { const isMobile = useIsMobile(); + const navigate = useNavigate(); + const { org } = useParams({ strict: false }) as { org?: string }; + const search = useSearch({ strict: false }) as { new?: string }; + + const openTask = (id: string) => { + if (org) navigate({ to: "/$org/$taskId", params: { org, taskId: id } }); + }; + const openNew = () => { + if (org) navigate({ to: "/$org", params: { org }, search: { new: "1" } }); + }; + const closeNew = () => { + if (org) navigate({ to: "/$org", params: { org }, search: {} }); + }; + const openInbox = () => { + if (org) navigate({ to: "/$org/inbox", params: { org } }); + }; + const openGoal = (id: string) => { + if (org) navigate({ to: "/$org/goal", params: { org }, search: { g: id } }); + }; + + const content = ( + + ); + const dialog = ; if (isMobile) { return ( -
- +
+ +
{content}
+ {dialog}
); } @@ -24,10 +58,14 @@ export default function OrgHome() { return (
-
- +
+ +
+ {content} +
+ {dialog}
); } diff --git a/apps/mesh/src/web/layouts/org-shell-layout/index.tsx b/apps/mesh/src/web/layouts/org-shell-layout/index.tsx index 8644d96f56..e802225038 100644 --- a/apps/mesh/src/web/layouts/org-shell-layout/index.tsx +++ b/apps/mesh/src/web/layouts/org-shell-layout/index.tsx @@ -22,9 +22,10 @@ import { SidebarLayout, SidebarProvider, } from "@deco/ui/components/sidebar.tsx"; +import { Button } from "@deco/ui/components/button.tsx"; import { useIsMobile } from "@deco/ui/hooks/use-mobile.ts"; -import { Loading01 } from "@untitledui/icons"; -import { Outlet } from "@tanstack/react-router"; +import { Loading01, Monitor04 } from "@untitledui/icons"; +import { Outlet, useNavigate, useParams } from "@tanstack/react-router"; import { SidebarResizeHandle } from "@/web/components/sidebar/sidebar-resize-handle"; import { useSidebarResize } from "@/web/hooks/use-sidebar-resize"; import { StudioSidebar, StudioSidebarMobile } from "@/web/components/sidebar"; @@ -40,6 +41,34 @@ import { useLocalStorage } from "@/web/hooks/use-local-storage"; const SIDEBAR_OPEN_STORAGE_KEY = "sidebar.open"; +/** + * Top-right entry to the storefront preview. Opens the REAL agent shell — the + * normal chat panel on the left and the main panel's Preview tab on the right + * (`?main=preview`), defaulting to the org's agent. Same surface the GitHub + * import opens. A fresh task id is fine; the thread row is created idempotently. + */ +function PreviewButton() { + const { org } = useParams({ from: "/shell/$org" }); + const navigate = useNavigate(); + return ( + + ); +} + function RouteFallback() { return (
@@ -52,7 +81,8 @@ export default function OrgShellLayout() { const isMobile = useIsMobile(); const [sidebarOpen, setSidebarOpen] = useLocalStorage( SIDEBAR_OPEN_STORAGE_KEY, - false, + // Redesign: default the rail expanded so the working set of tasks is visible. + true, ); const { width, wrapperRef, onStartResize, resetWidth } = useSidebarResize(); @@ -77,6 +107,7 @@ export default function OrgShellLayout() {
+ diff --git a/apps/mesh/src/web/layouts/settings-layout.tsx b/apps/mesh/src/web/layouts/settings-layout.tsx index 6d4c9ee685..9d11a8d01e 100644 --- a/apps/mesh/src/web/layouts/settings-layout.tsx +++ b/apps/mesh/src/web/layouts/settings-layout.tsx @@ -32,27 +32,15 @@ import { } from "@deco/ui/components/sidebar.tsx"; import { PageContentClassNameProvider } from "@/web/components/page"; import { - BarChart10, - BookOpen01, - Building02, + Bell01, ZapSquare, - CpuChip01, Loading01, - Lock01, LogOut01, - PackageCheck, - Shield01, User01, - Users03, - Zap, - Key01, - HardDrive, } from "@untitledui/icons"; import { useProjectContext } from "@decocms/mesh-sdk"; -import { useCapabilities, type CapabilityId } from "@/web/hooks/use-capability"; import { useIsMobile } from "@deco/ui/hooks/use-mobile.ts"; import { Suspense } from "react"; -import { pluginSettingsSidebarItems } from "@/web/index"; import { useStatusSounds } from "../hooks/use-status-sounds"; import { authClient } from "@/web/lib/auth-client"; import { track } from "@/web/lib/posthog-client"; @@ -62,191 +50,80 @@ import { MobileSidebarSheet, SidebarTriggerButton, } from "@/web/layouts/shell-controls"; +import { USER_AGENTS } from "@/web/views/deco-redesign/mock-user"; + +/** The agents shown in the settings sidebar — the current org plus the + * other agents the user has (mock). */ +function useSidebarAgents(currentName: string, currentLogo: string | null) { + return [ + { id: "current", name: currentName, logo: currentLogo, current: true }, + ...USER_AGENTS.map((a) => ({ + id: a.id, + name: a.name, + logo: a.icon ?? null, + current: false, + })), + ]; +} interface SettingsNavItem { key: string; label: string; icon: React.ReactNode; to: string; - /** Capability required to see this item. Omitted = visible to every member. */ - requires?: CapabilityId; - /** Restrict to privileged built-in roles (owner/admin). For screens backed - * by owner/admin-only APIs (e.g. role management). */ - privilegedOnly?: boolean; -} - -interface SettingsNavGroup { - label: string; - items: SettingsNavItem[]; } -function useSettingsSidebarGroups(): SettingsNavGroup[] { - const currentProject = useProjectContext().project; - const enabledPlugins = currentProject.enabledPlugins ?? []; - const { capabilities, isPrivileged, loading, error } = useCapabilities(); - - const enabledSettingsItems = pluginSettingsSidebarItems - .filter((item) => enabledPlugins.includes(item.pluginId)) - .map(({ key, label, icon, to }) => ({ key, label, icon, to })); +/** + * Settings are organized around the teammate model: + * - USER scope (top): what belongs to you and carries across every agent — + * your connections, profile, and how the teammate notifies you. + * - AGENT scope (below): each agent (org-as-teammate) is a single item; its + * own config — connections, subagents, knowledge, etc. — lives one level in, + * on the agent overview page (`/$org/settings/agent`). + */ +const USER_ITEMS: SettingsNavItem[] = [ + { + key: "connections", + label: "Connections", + icon: , + to: "/$org/settings/connections", + }, + { + key: "profile", + label: "Profile & Preferences", + icon: , + to: "/$org/settings/profile", + }, + { + key: "findings", + label: "Findings & notifications", + icon: , + to: "/$org/settings/findings", + }, +]; - const groups: SettingsNavGroup[] = [ - { - label: "Organization", - items: [ - { - key: "general", - label: "General", - icon: , - to: "/$org/settings/general", - requires: "org:manage", - }, - { - key: "brand-context", - label: "Brand Context", - icon: , - to: "/$org/settings/brand-context", - requires: "org:manage", - }, - { - key: "ai-providers", - label: "AI Providers", - icon: , - to: "/$org/settings/ai-providers", - requires: "ai-providers:manage", - }, - { - key: "secrets", - label: "Secrets", - icon: , - to: "/$org/settings/secrets", - requires: "secrets:manage", - }, - { - key: "files", - label: "Files", - icon: , - to: "/$org/settings/files", - requires: "file-configs:manage", - }, - ], - }, - { - label: "Build", - items: [ - // connections + agents stay visible to every member — viewing them is - // basic-usage. The create/update/delete affordances inside those pages - // are gated in-page by connections:manage / agents:manage. - { - key: "connections", - label: "Connections", - icon: , - to: "/$org/settings/connections", - }, - { - key: "agents", - label: "Agents", - icon: , - to: "/$org/settings/agents", - }, - { - key: "automations", - label: "Automations", - icon: , - to: "/$org/settings/automations", - requires: "automations:manage", - }, - { - key: "store", - label: "Store", - icon: , - to: "/$org/settings/store", - requires: "registry:manage", - }, - ], - }, - { - label: "Manage", - items: [ - { - key: "monitor", - label: "Monitor", - icon: , - to: "/$org/settings/monitor", - requires: "monitoring:view", - }, - { - key: "members", - label: "Members", - icon: , - to: "/$org/settings/members", - requires: "members:manage", - }, - { - key: "roles", - label: "Roles", - icon: , - to: "/$org/settings/roles", - // Role management uses owner/admin-only Better Auth APIs. - privilegedOnly: true, - }, - { - key: "sso", - label: "Security", - icon: , - to: "/$org/settings/sso", - requires: "org:manage", - }, - ], - }, - { - label: "Extensions", - items: [ - { - key: "features", - label: "Plugins", - icon: , - to: "/$org/settings/features", - requires: "org:manage", - }, - ...enabledSettingsItems, - ], - }, - { - label: "Account", - items: [ - { - key: "profile", - label: "Profile & Preferences", - icon: , - to: "/$org/settings/profile", - }, - ], - }, - ]; +const AGENT_OVERVIEW_TO = "/$org/settings/agent"; - // While capabilities load — or if the lookup errored — show every item - // optimistically. This avoids a flicker for the common privileged case and - // ensures a transient failure never hides nav from owners/admins. Once - // resolved, hide items the member's role can't open and drop any group left - // empty. Items without a `requires` (Profile, plugin items) are always shown. - if (loading || error) { - return groups; +function AgentAvatar({ name, logo }: { name: string; logo: string | null }) { + if (logo) { + return ( + + ); } - return groups - .map((group) => ({ - ...group, - items: group.items.filter((item) => { - if (item.privilegedOnly) return isPrivileged; - if (!item.requires) return true; - return isPrivileged || capabilities[item.requires]; - }), - })) - .filter((group) => group.items.length > 0); + return ( + + {name?.[0]?.toUpperCase() ?? "A"} + + ); } export function SettingsSidebar() { - const groups = useSettingsSidebarGroups(); const { org } = useParams({ from: "/shell/$org" }); + const { org: organization } = useProjectContext(); const pathname = useRouterState({ select: (s) => s.location.pathname, }); @@ -256,51 +133,83 @@ export function SettingsSidebar() { return pathname.startsWith(resolved); }; + // Any settings path that isn't a user-scoped page belongs to the agent, so + // the active agent stays highlighted while you're deep in its sub-pages. + const userActive = USER_ITEMS.some((item) => isActive(item.to)); + const agentActive = !userActive && pathname.includes("/settings"); + const agents = useSidebarAgents(organization.name, organization.logo); + const activeAgentId = useRouterState({ + select: (s) => (s.location.search as { agent?: string }).agent ?? "current", + }); + return ( - {groups.map((group, i) => ( - - {group.label && ( -

0 && "mt-3", - )} - > - {group.label} -

- )} - - - {group.items.map((item) => ( - - - - track("settings_nav_clicked", { - section_key: item.key, - section_label: item.label, - group_label: group.label || "main", - }) - } - className="flex items-center gap-2.5 text-sm" - > - {item.icon} - {item.label} - - - - ))} - - -
- ))} + {/* User scope */} + + + + {USER_ITEMS.map((item) => ( + + + + track("settings_nav_clicked", { + section_key: item.key, + section_label: item.label, + group_label: "user", + }) + } + className="flex items-center gap-2.5 text-sm" + > + {item.icon} + {item.label} + + + + ))} + + + + + {/* Agent scope — each agent is one item; its config lives one level in */} +
+ +

+ Agents +

+ + + {agents.map((agent) => ( + + + + track("settings_nav_clicked", { + section_key: "agent", + section_label: agent.name, + group_label: "agents", + }) + } + className="flex items-center gap-2.5 text-sm" + > + + {agent.name} + + + + ))} + + +
{/* Sign Out */} @@ -338,8 +247,8 @@ export function SettingsSidebar() { } export function SettingsSidebarMobile({ onClose }: { onClose: () => void }) { - const groups = useSettingsSidebarGroups(); const { org } = useParams({ from: "/shell/$org" }); + const { org: organization } = useProjectContext(); const pathname = useRouterState({ select: (s) => s.location.pathname, }); @@ -349,39 +258,56 @@ export function SettingsSidebarMobile({ onClose }: { onClose: () => void }) { return pathname.startsWith(resolved); }; + const userActive = USER_ITEMS.some((item) => isActive(item.to)); + const agentActive = !userActive && pathname.includes("/settings"); + const agents = useSidebarAgents(organization.name, organization.logo); + const activeAgentId = useRouterState({ + select: (s) => (s.location.search as { agent?: string }).agent ?? "current", + }); + + const rowBase = + "flex items-center gap-3 w-full px-3 py-2.5 rounded-lg transition-colors text-sm"; + const rowOn = "bg-sidebar-accent text-sidebar-accent-foreground font-medium"; + const rowOff = + "text-sidebar-foreground/70 hover:bg-sidebar-accent hover:text-sidebar-foreground"; + return (
- {groups.map((group, i) => ( -
- {group.label && ( -

0 && "mt-3", - )} - > - {group.label} -

+ {/* User scope */} + {USER_ITEMS.map((item) => ( + + {item.icon} + {item.label} + + ))} + + {/* Agent scope */} +
+

+ Agents +

+ {agents.map((agent) => ( + ( - - {item.icon} - {item.label} - - ))} -
+ > + + {agent.name} + ))} {/* Sign Out */} diff --git a/apps/mesh/src/web/routes/agents-list.tsx b/apps/mesh/src/web/routes/agents-list.tsx index 3ded380eb2..a72f7f3fcb 100644 --- a/apps/mesh/src/web/routes/agents-list.tsx +++ b/apps/mesh/src/web/routes/agents-list.tsx @@ -88,7 +88,7 @@ export default function AgentsListPage() {
- Agents + Subagents
; +} diff --git a/apps/mesh/src/web/routes/orgs/content.tsx b/apps/mesh/src/web/routes/orgs/content.tsx new file mode 100644 index 0000000000..6483ec4eee --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/content.tsx @@ -0,0 +1,13 @@ +import { ContentView } from "@/web/views/deco-redesign/content"; + +export default function ContentRoute() { + return ( +
+
+
+ +
+
+
+ ); +} diff --git a/apps/mesh/src/web/routes/orgs/goal.tsx b/apps/mesh/src/web/routes/orgs/goal.tsx new file mode 100644 index 0000000000..167ee86265 --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/goal.tsx @@ -0,0 +1,39 @@ +import { useNavigate, useParams, useSearch } from "@tanstack/react-router"; +import { + GoalDetailView, + GoalsListView, +} from "@/web/views/deco-redesign/goal-detail"; + +export default function GoalRoute() { + const navigate = useNavigate(); + const { org } = useParams({ strict: false }) as { org?: string }; + const { g } = useSearch({ strict: false }) as { g?: string }; + + const openGoal = (id: string) => { + if (org) navigate({ to: "/$org/goal", params: { org }, search: { g: id } }); + }; + const back = () => { + if (org) navigate({ to: "/$org/goal", params: { org }, search: {} }); + }; + const openFinding = (id: string) => { + if (org) navigate({ to: "/$org/$taskId", params: { org, taskId: id } }); + }; + + return ( +
+
+
+ {g ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/apps/mesh/src/web/routes/orgs/inbox.tsx b/apps/mesh/src/web/routes/orgs/inbox.tsx new file mode 100644 index 0000000000..ee848e7ab0 --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/inbox.tsx @@ -0,0 +1,13 @@ +import { InboxView } from "@/web/views/deco-redesign/inbox"; + +export default function InboxRoute() { + return ( +
+
+
+ +
+
+
+ ); +} diff --git a/apps/mesh/src/web/routes/orgs/onboarding.tsx b/apps/mesh/src/web/routes/orgs/onboarding.tsx new file mode 100644 index 0000000000..51483d1f22 --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/onboarding.tsx @@ -0,0 +1,5 @@ +import { OnboardingFlow } from "@/web/views/deco-redesign/onboarding"; + +export default function OnboardingDemoRoute() { + return ; +} diff --git a/apps/mesh/src/web/routes/orgs/settings/agent-automations.tsx b/apps/mesh/src/web/routes/orgs/settings/agent-automations.tsx new file mode 100644 index 0000000000..1c1475c844 --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/settings/agent-automations.tsx @@ -0,0 +1,5 @@ +import { AgentAutomations } from "@/web/views/deco-redesign/agent-automations"; + +export default function AgentAutomationsRoute() { + return ; +} diff --git a/apps/mesh/src/web/routes/orgs/settings/agent-files.tsx b/apps/mesh/src/web/routes/orgs/settings/agent-files.tsx new file mode 100644 index 0000000000..e7fe9876b8 --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/settings/agent-files.tsx @@ -0,0 +1,5 @@ +import { AgentFiles } from "@/web/views/deco-redesign/agent-files"; + +export default function AgentFilesRoute() { + return ; +} diff --git a/apps/mesh/src/web/routes/orgs/settings/agent-findings.tsx b/apps/mesh/src/web/routes/orgs/settings/agent-findings.tsx new file mode 100644 index 0000000000..88193aabe6 --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/settings/agent-findings.tsx @@ -0,0 +1,5 @@ +import { AgentFindings } from "@/web/views/deco-redesign/agent-findings"; + +export default function AgentFindingsRoute() { + return ; +} diff --git a/apps/mesh/src/web/routes/orgs/settings/agent-memory.tsx b/apps/mesh/src/web/routes/orgs/settings/agent-memory.tsx new file mode 100644 index 0000000000..8adae6346c --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/settings/agent-memory.tsx @@ -0,0 +1,5 @@ +import { AgentMemory } from "@/web/views/deco-redesign/agent-memory"; + +export default function AgentMemoryRoute() { + return ; +} diff --git a/apps/mesh/src/web/routes/orgs/settings/agent-personalization.tsx b/apps/mesh/src/web/routes/orgs/settings/agent-personalization.tsx new file mode 100644 index 0000000000..b05032d930 --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/settings/agent-personalization.tsx @@ -0,0 +1,5 @@ +import { AgentPersonalization } from "@/web/views/deco-redesign/agent-personalization"; + +export default function AgentPersonalizationRoute() { + return ; +} diff --git a/apps/mesh/src/web/routes/orgs/settings/agent.tsx b/apps/mesh/src/web/routes/orgs/settings/agent.tsx new file mode 100644 index 0000000000..febdf7e379 --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/settings/agent.tsx @@ -0,0 +1,10 @@ +import { AgentSettings } from "@/web/views/deco-redesign/agent-settings"; + +/** + * The agent's settings — one page that organizes the editable "outside" + * (personalization: guidance, your skills, connections) with the managed + * "inside" (definition: prompt, memory, files, automations). + */ +export default function AgentSettingsRoute() { + return ; +} diff --git a/apps/mesh/src/web/routes/orgs/settings/findings.tsx b/apps/mesh/src/web/routes/orgs/settings/findings.tsx new file mode 100644 index 0000000000..fede94219b --- /dev/null +++ b/apps/mesh/src/web/routes/orgs/settings/findings.tsx @@ -0,0 +1,5 @@ +import { FindingsSettingsPage } from "@/web/views/deco-redesign/settings-findings"; + +export default function FindingsRoute() { + return ; +} diff --git a/apps/mesh/src/web/views/deco-redesign/agent-automations.tsx b/apps/mesh/src/web/views/deco-redesign/agent-automations.tsx new file mode 100644 index 0000000000..4dab01e16a --- /dev/null +++ b/apps/mesh/src/web/views/deco-redesign/agent-automations.tsx @@ -0,0 +1,175 @@ +/** + * Agent automations (mock) + * + * Automation is a Studio primitive (the scheduler/trigger engine), surfaced + * here split by origin: + * • System — provisioned by a capability when it's enabled. Managed, + * view-only, labelled "from ". The user never hand-writes these. + * • Yours — explicit recurring work the user created. + * + * Same engine, two authors — like skills (included vs your own). Agent-aware via + * `?agent`. Mock only. + */ + +import { useState } from "react"; +import { useSearch } from "@tanstack/react-router"; +import { Lock01, Plus, Zap } from "@untitledui/icons"; +import { Switch } from "@deco/ui/components/switch.tsx"; +import { useProjectContext } from "@decocms/mesh-sdk"; +import { Page } from "@/web/components/page"; +import { + SettingsCard, + SettingsPage, + SettingsSection, +} from "@/web/components/settings/settings-section"; +import { resolveAgent } from "./agent-data"; + +interface SystemAutomation { + id: string; + name: string; + capability: string; + when: string; +} + +const SYSTEM: SystemAutomation[] = [ + { + id: "error-watch", + name: "Error watch", + capability: "System health", + when: "When 5xx spikes", + }, + { + id: "seo-sweep", + name: "SEO sweep", + capability: "SEO", + when: "Mondays at 06:00", + }, + { + id: "cache-research", + name: "Cache research", + capability: "Performance", + when: "Nightly", + }, + { + id: "checkout-qa", + name: "Checkout QA", + capability: "QA", + when: "On every deploy", + }, +]; + +interface UserAutomation { + id: string; + name: string; + when: string; + enabled: boolean; +} + +const INITIAL_YOURS: UserAutomation[] = [ + { + id: "traffic-email", + name: "Weekly traffic email", + when: "Mondays at 09:00", + enabled: true, + }, +]; + +function AutomationIcon() { + return ( + + + + ); +} + +export function AgentAutomations() { + const { agent } = useSearch({ + from: "/shell/$org/settings/agent/automations", + }); + const { org: organization } = useProjectContext(); + const profile = resolveAgent(agent, { + name: organization.name, + logo: organization.logo, + }); + const [yours, setYours] = useState(INITIAL_YOURS); + + const toggle = (id: string, enabled: boolean) => + setYours((prev) => prev.map((a) => (a.id === id ? { ...a, enabled } : a))); + + return ( + + + + +
+

+ Automations +

+

+ Recurring work {profile.name} runs. Capabilities bring their + own; add your own below. +

+
+ + {/* System — provisioned by capabilities */} + + + {SYSTEM.map((a) => ( +
+ + + + {a.name} + + + from {a.capability} · {a.when} + + + + + Managed by Deco + +
+ ))} +
+
+ + {/* Yours — explicit recurring work */} + + + {yours.map((a) => ( +
+ + + + {a.name} + + + {a.when} + + + toggle(a.id, v)} + /> +
+ ))} + +
+
+
+
+
+
+ ); +} diff --git a/apps/mesh/src/web/views/deco-redesign/agent-data.tsx b/apps/mesh/src/web/views/deco-redesign/agent-data.tsx new file mode 100644 index 0000000000..62a63772c4 --- /dev/null +++ b/apps/mesh/src/web/views/deco-redesign/agent-data.tsx @@ -0,0 +1,195 @@ +/** + * Mock agent data (redesign) + * + * Resolves an agent profile from the `?agent` search param so the settings + * pages can render any of the user's agents (current org, Farm Rio, Deco, + * Monte Carlo). `?agent` absent → the current org. Mock only. + */ + +import { Code02, SearchLg, Tag01 } from "@untitledui/icons"; +import { USER_AGENTS } from "./mock-user"; + +export interface AgentConnection { + id: string; + name: string; + description: string; + icon: string; +} + +export interface AgentSkill { + id: string; + name: string; + /** Tailwind classes for the icon tile. */ + tile: string; + icon: React.ReactNode; + description: string; + content: string; +} + +export interface AgentProfile { + /** The `?agent` value, or "" for the current org. */ + id: string; + name: string; + logo: string | null; + blurb: string; + connections: AgentConnection[]; + skills: AgentSkill[]; +} + +const favicon = (domain: string) => + `https://www.google.com/s2/favicons?domain=${domain}&sz=64`; + +const CONNECTIONS_CATALOG: Record = { + vtex: { + id: "vtex", + name: "VTEX", + description: "Catalog, orders, and storefront APIs", + icon: favicon("vtex.com"), + }, + ga: { + id: "ga", + name: "Google Analytics", + description: "Traffic, conversion, and behavior", + icon: favicon("analytics.google.com"), + }, + gsc: { + id: "gsc", + name: "Search Console", + description: "Indexing, impressions, and queries", + icon: favicon("search.google.com"), + }, + github: { + id: "github", + name: "GitHub", + description: "Repos, issues, and pull requests", + icon: favicon("github.com"), + }, + slack: { + id: "slack", + name: "Slack", + description: "Reach the team where they work", + icon: favicon("slack.com"), + }, + linear: { + id: "linear", + name: "Linear", + description: "Turn requests into tracked work", + icon: favicon("linear.app"), + }, +}; + +const SKILLS_CATALOG: Record = { + "review-code": { + id: "review-code", + name: "Review Code", + tile: "bg-violet-100 text-violet-600", + icon: , + description: + 'Structured code review — bugs, security, performance, and style with actionable feedback. Trigger with "review this code".', + content: `# Code Review + +Review code changes for bugs, security issues, performance regressions, and +style violations. Produces prioritised, actionable feedback. + +# Review Workflow + +Execute these steps in order for every review. Do not skip steps — a review +that catches a style nit but misses a security flaw has failed. + +## Step 1: Understand Context +Before reading a single line of code, establish what the change is meant to do.`, + }, + "seo-audit": { + id: "seo-audit", + name: "SEO audit", + tile: "bg-blue-100 text-blue-600", + icon: , + description: + "Crawl the site, diagnose indexing and canonical issues, and open PRs with fixes. Runs nightly.", + content: `# SEO audit + +Pull Search Console + Analytics, find pages losing impressions, check +canonicals and structured data, and open a PR with fixes.`, + }, + "pdp-schema": { + id: "pdp-schema", + name: "PDP schema", + tile: "bg-emerald-100 text-emerald-600", + icon: , + description: + "Detect PDPs missing Product structured data, generate it from the catalog, and open a PR.", + content: `# PDP schema + +Add Product structured data to product detail pages. Generate from the +catalog, validate, and PR.`, + }, +}; + +interface AgentExtras { + connectionIds: string[]; + skillIds: string[]; +} + +const AGENT_EXTRAS: Record = { + "farm-rio": { + connectionIds: ["vtex", "ga", "gsc"], + skillIds: ["review-code"], + }, + "deco-company": { + connectionIds: ["github", "slack", "linear"], + skillIds: ["review-code", "seo-audit"], + }, + "monte-carlo": { connectionIds: ["vtex", "ga"], skillIds: [] }, +}; + +const DEFAULT_EXTRAS: AgentExtras = { + connectionIds: ["vtex", "ga", "gsc"], + skillIds: ["review-code"], +}; + +function build(extras: AgentExtras) { + return { + connections: extras.connectionIds + .map((id) => CONNECTIONS_CATALOG[id]) + .filter((c): c is AgentConnection => Boolean(c)), + skills: extras.skillIds + .map((id) => SKILLS_CATALOG[id]) + .filter((s): s is AgentSkill => Boolean(s)), + }; +} + +/** + * Resolve the agent to render. `agentId` comes from the `?agent` search param; + * when it matches one of the user's mock agents we use its data, otherwise we + * fall back to the current org. + */ +export function resolveAgent( + agentId: string | undefined, + fallback: { name: string; logo: string | null }, +): AgentProfile { + const mock = agentId ? USER_AGENTS.find((a) => a.id === agentId) : undefined; + + if (mock) { + const { connections, skills } = build( + AGENT_EXTRAS[mock.id] ?? DEFAULT_EXTRAS, + ); + return { + id: mock.id, + name: mock.name, + logo: mock.icon ?? null, + blurb: mock.blurb, + connections, + skills, + }; + } + + const { connections, skills } = build(DEFAULT_EXTRAS); + return { + id: "", + name: fallback.name, + logo: fallback.logo, + blurb: "Powered by deco.cx", + connections, + skills, + }; +} diff --git a/apps/mesh/src/web/views/deco-redesign/agent-files.tsx b/apps/mesh/src/web/views/deco-redesign/agent-files.tsx new file mode 100644 index 0000000000..a0d6b13ddd --- /dev/null +++ b/apps/mesh/src/web/views/deco-redesign/agent-files.tsx @@ -0,0 +1,100 @@ +/** + * Agent files (mock) + * + * The agent's documents — both what it reads (knowledge you give it) and what + * it produces (reports, generated changes). This merges the old "Files" and + * "Artifacts": same surface, two directions. Mock only. + */ + +import { File02, FilePlus02, UploadCloud01 } from "@untitledui/icons"; +import { Button } from "@deco/ui/components/button.tsx"; +import { Page } from "@/web/components/page"; +import { + SettingsCard, + SettingsPage, + SettingsSection, +} from "@/web/components/settings/settings-section"; + +interface AgentFile { + name: string; + meta: string; +} + +const KNOWLEDGE: AgentFile[] = [ + { name: "Brand guidelines.pdf", meta: "2.4 MB · uploaded May 12" }, + { name: "Tone of voice.md", meta: "8 KB · uploaded May 12" }, + { name: "Product catalog feed.csv", meta: "1.1 MB · synced daily" }, + { name: "2026 campaign calendar.xlsx", meta: "320 KB · uploaded Jun 2" }, +]; + +const PRODUCED: AgentFile[] = [ + { name: "2026-06-08 traffic report.md", meta: "Generated Jun 8" }, + { name: "PR #3302 summary.md", meta: "Generated Jun 7" }, + { name: "alt-text batch.csv", meta: "84 rows · generated Jun 6" }, +]; + +function FileRow({ file }: { file: AgentFile }) { + return ( +
+ + + + + + {file.name} + + + {file.meta} + + +
+ ); +} + +export function AgentFiles() { + return ( + + + + +
+
+

Files

+

+ Documents the agent reads, and the files it produces. +

+
+ +
+ + + + {KNOWLEDGE.map((f) => ( + + ))} + + + + + {PRODUCED.length > 0 ? ( + + {PRODUCED.map((f) => ( + + ))} + + ) : ( +
+ + Nothing produced yet. +
+ )} +
+
+
+
+
+ ); +} diff --git a/apps/mesh/src/web/views/deco-redesign/agent-findings.tsx b/apps/mesh/src/web/views/deco-redesign/agent-findings.tsx new file mode 100644 index 0000000000..e2b980d11a --- /dev/null +++ b/apps/mesh/src/web/views/deco-redesign/agent-findings.tsx @@ -0,0 +1,186 @@ +/** + * Agent findings (mock) — agent-scope + * + * What the agent watches and how far it can act on its own, per capability. + * This is agent config: it applies to everyone who has the agent. Personal + * notification delivery lives on the user's Notifications page. Agent-aware via + * `?agent`. Mock only. + */ + +import { useState } from "react"; +import { + Activity, + LayoutAlt01, + SearchSm, + ShieldTick, + ShoppingBag03, + Zap, +} from "@untitledui/icons"; +import { + ToggleGroup, + ToggleGroupItem, +} from "@deco/ui/components/toggle-group.tsx"; +import { useSearch } from "@tanstack/react-router"; +import { useProjectContext } from "@decocms/mesh-sdk"; +import { Page } from "@/web/components/page"; +import { + SettingsCard, + SettingsPage, + SettingsSection, +} from "@/web/components/settings/settings-section"; +import { resolveAgent } from "./agent-data"; + +type Autonomy = "off" | "inform" | "propose" | "auto"; + +const AUTONOMY_STEPS: { value: Autonomy; label: string }[] = [ + { value: "off", label: "Off" }, + { value: "inform", label: "Inform" }, + { value: "propose", label: "Propose" }, + { value: "auto", label: "Auto" }, +]; + +interface Capability { + id: string; + label: string; + icon: typeof Activity; + watches: string; + defaultMode: Autonomy; +} + +const CAPABILITIES: Capability[] = [ + { + id: "system-health", + label: "System health", + icon: Activity, + watches: "Errors, latency, and 5xx spikes", + defaultMode: "propose", + }, + { + id: "seo", + label: "SEO", + icon: SearchSm, + watches: "Canonicals, metadata, and broken links", + defaultMode: "auto", + }, + { + id: "performance", + label: "Performance", + icon: Zap, + watches: "Core Web Vitals and page speed", + defaultMode: "inform", + }, + { + id: "qa", + label: "QA", + icon: ShieldTick, + watches: "Checkout and the purchase journey", + defaultMode: "propose", + }, + { + id: "plp", + label: "PLP optimizer", + icon: LayoutAlt01, + watches: "Collection-page ranking and merchandising", + defaultMode: "off", + }, + { + id: "pdp", + label: "PDP optimizer", + icon: ShoppingBag03, + watches: "Product-page content and conversion", + defaultMode: "off", + }, +]; + +function CapabilityRow({ + capability, + mode, + onChange, +}: { + capability: Capability; + mode: Autonomy; + onChange: (next: Autonomy) => void; +}) { + const Icon = capability.icon; + return ( +
+
+ + + + + + {capability.label} + + + {capability.watches} + + +
+ value && onChange(value as Autonomy)} + className="w-full" + > + {AUTONOMY_STEPS.map((step) => ( + + {step.label} + + ))} + +
+ ); +} + +export function AgentFindings() { + const { agent } = useSearch({ from: "/shell/$org/settings/agent/findings" }); + const { org: organization } = useProjectContext(); + const profile = resolveAgent(agent, { + name: organization.name, + logo: organization.logo, + }); + + const [modes, setModes] = useState>(() => + Object.fromEntries(CAPABILITIES.map((c) => [c.id, c.defaultMode])), + ); + + return ( + + + + +
+

Findings

+

+ What {profile.name} watches and how far it can act. Applies to + everyone. +

+
+ + + + {CAPABILITIES.map((c) => ( + + setModes((prev) => ({ ...prev, [c.id]: next })) + } + /> + ))} + + +
+
+
+
+ ); +} diff --git a/apps/mesh/src/web/views/deco-redesign/agent-memory.tsx b/apps/mesh/src/web/views/deco-redesign/agent-memory.tsx new file mode 100644 index 0000000000..7edfa22a95 --- /dev/null +++ b/apps/mesh/src/web/views/deco-redesign/agent-memory.tsx @@ -0,0 +1,222 @@ +/** + * Agent memory (mock) + * + * Memory is a small repo of markdown, like skills: + * + * memory/ + * ├── MEMORY.md # concise index, loaded into every session + * ├── goals.md # one topic note per file + * ├── cms-workflow.md + * └── ... + * + * This page shows the index (openable, read it) plus a table of the topic + * notes (each openable). Agent-aware via `?agent`. Mock only. + */ + +import { useState } from "react"; +import { useSearch } from "@tanstack/react-router"; +import { BookOpen01, File02, XClose } from "@untitledui/icons"; +import { Dialog, DialogContent } from "@deco/ui/components/dialog.tsx"; +import { Button } from "@deco/ui/components/button.tsx"; +import { useProjectContext } from "@decocms/mesh-sdk"; +import { Page } from "@/web/components/page"; +import { + SettingsCard, + SettingsPage, + SettingsSection, +} from "@/web/components/settings/settings-section"; +import { resolveAgent } from "./agent-data"; + +interface MemoryFile { + name: string; + summary: string; + updated: string; + content: string; +} + +const INDEX: MemoryFile = { + name: "MEMORY.md", + summary: "Concise index — loaded into every session", + updated: "today", + content: `# Memory index + +- [goals.md](goals.md) — conversion target 2.4%, grow organic traffic +- [cms-workflow.md](cms-workflow.md) — banner copy comes from Trello +- [blockers.md](blockers.md) — Bazaarvoice API key expired Thursday +- [preferences.md](preferences.md) — one morning briefing; propose, then act +- [brand-voice.md](brand-voice.md) — direct, numbers-first, no hype`, +}; + +const NOTES: MemoryFile[] = [ + { + name: "goals.md", + summary: "Conversion and traffic targets", + updated: "2 days ago", + content: `# Goals + +- Conversion target is 2.4% (currently 2.1%). +- Grow organic traffic — the owner's standing focus. +- Keep the error rate near zero.`, + }, + { + name: "cms-workflow.md", + summary: "Where banner copy comes from", + updated: "5 days ago", + content: `# CMS workflow + +Banner copy comes from Trello, not written in the CMS. Pull the latest card +before scheduling a campaign.`, + }, + { + name: "blockers.md", + summary: "Open access and credential gaps", + updated: "Thursday", + content: `# Blockers + +- Bazaarvoice API key expired Thursday — need a new one to resume reviews. +- No write access to the checkout repo yet.`, + }, + { + name: "preferences.md", + summary: "How the owner likes to work", + updated: "1 week ago", + content: `# Preferences + +- One briefing per morning, not a stream of pings. +- Propose changes, then act on approval. +- Escalate urgent issues over WhatsApp.`, + }, + { + name: "brand-voice.md", + summary: "Tone and wording rules", + updated: "2 weeks ago", + content: `# Brand voice + +Direct, concrete, numbers-first. Sentence case. No hype, no exclamation marks.`, + }, +]; + +function MemoryFileModal({ + file, + onClose, +}: { + file: MemoryFile; + onClose: () => void; +}) { + return ( + !open && onClose()}> + +
+ + + {file.name} + + +
+
+
+            {file.content}
+          
+
+
+
+ ); +} + +export function AgentMemory() { + const { agent } = useSearch({ from: "/shell/$org/settings/agent/memory" }); + const { org: organization } = useProjectContext(); + const profile = resolveAgent(agent, { + name: organization.name, + logo: organization.logo, + }); + const [openFile, setOpenFile] = useState(null); + + return ( + + + + +
+

Memory

+

+ What {profile.name} remembers about you and the work. It updates + this as you work together. +

+
+ + {/* Index — loaded into every session */} + + + + + + + {/* Notes — one topic file each */} + + + {NOTES.map((note) => ( + + ))} + + +
+
+
+ + {openFile && ( + setOpenFile(null)} /> + )} +
+ ); +} diff --git a/apps/mesh/src/web/views/deco-redesign/agent-personalization.tsx b/apps/mesh/src/web/views/deco-redesign/agent-personalization.tsx new file mode 100644 index 0000000000..1599a78050 --- /dev/null +++ b/apps/mesh/src/web/views/deco-redesign/agent-personalization.tsx @@ -0,0 +1,233 @@ +/** + * Agent personalization (mock) — the editable user layer + * + * Your personal layer on top of a managed agent: guidance, your own skills, + * and the connections you authorize. Agent-aware via `?agent`. Mock only. + */ + +import { Link, useParams, useSearch } from "@tanstack/react-router"; +import { useState } from "react"; +import { ArrowLeft, Code02, Plus, Stars01, XClose } from "@untitledui/icons"; +import { Dialog, DialogContent } from "@deco/ui/components/dialog.tsx"; +import { Button } from "@deco/ui/components/button.tsx"; +import { Textarea } from "@deco/ui/components/textarea.tsx"; +import { cn } from "@deco/ui/lib/utils.ts"; +import { useProjectContext } from "@decocms/mesh-sdk"; +import { IntegrationIcon } from "@/web/components/integration-icon"; +import { Page } from "@/web/components/page"; +import { + SettingsPage, + SettingsSection, +} from "@/web/components/settings/settings-section"; +import { resolveAgent, type AgentSkill } from "./agent-data"; + +function SkillModal({ + skill, + onClose, +}: { + skill: AgentSkill; + onClose: () => void; +}) { + return ( + !open && onClose()}> + +
+ + {skill.icon} + +

+ {skill.name} +

+ + +
+
+ +
+

+ Description +

+

+ {skill.description} +

+
+
+                {skill.content}
+              
+
+
+
+
+
+ ); +} + +export function AgentPersonalization() { + const { org } = useParams({ from: "/shell/$org" }); + const { agent } = useSearch({ + from: "/shell/$org/settings/agent/personalization", + }); + const { org: organization } = useProjectContext(); + const profile = resolveAgent(agent, { + name: organization.name, + logo: organization.logo, + }); + + const [guidance, setGuidance] = useState(""); + const [openSkill, setOpenSkill] = useState(null); + + return ( + + + + +
+ + + {profile.name} + +

+ Agent personalization +

+

+ Your personal layer on top of {profile.name}. +

+
+ + + + Improve + + } + > +