@polpo-ai/dashboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +44 -0
  3. package/app/(dashboard)/error.tsx +47 -0
  4. package/app/(dashboard)/layout.tsx +41 -0
  5. package/app/(dashboard)/loading.tsx +30 -0
  6. package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
  7. package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
  8. package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
  9. package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
  10. package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
  11. package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
  12. package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
  13. package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
  14. package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
  15. package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
  16. package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
  17. package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
  18. package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
  19. package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
  20. package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
  21. package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
  22. package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
  23. package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
  24. package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
  25. package/app/(dashboard)/projects/[id]/page.tsx +68 -0
  26. package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
  27. package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
  28. package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
  29. package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
  30. package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
  31. package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
  32. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
  33. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
  34. package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
  35. package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
  36. package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
  37. package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
  38. package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
  39. package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
  40. package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
  41. package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
  42. package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
  43. package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
  44. package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
  45. package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
  46. package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
  47. package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
  48. package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
  49. package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
  50. package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
  51. package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
  52. package/app/(dashboard)/projects/[id]/view.tsx +421 -0
  53. package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
  54. package/app/(dashboard)/projects/projects-list.tsx +126 -0
  55. package/app/(playground)/error.tsx +42 -0
  56. package/app/(playground)/layout.tsx +27 -0
  57. package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
  58. package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
  59. package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
  60. package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
  61. package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
  62. package/app/favicon.ico +0 -0
  63. package/app/globals.css +385 -0
  64. package/app/icon.svg +6 -0
  65. package/app/layout.tsx +39 -0
  66. package/app/page.tsx +11 -0
  67. package/components/ai-elements/code-block.tsx +562 -0
  68. package/components/dashboard/agent-capabilities.tsx +326 -0
  69. package/components/dashboard/agent-identity-header.tsx +50 -0
  70. package/components/dashboard/agent-model-picker.tsx +195 -0
  71. package/components/dashboard/agent-model-selector.tsx +121 -0
  72. package/components/dashboard/agent-picker-card.tsx +46 -0
  73. package/components/dashboard/agent-studio.tsx +242 -0
  74. package/components/dashboard/agents-table.tsx +48 -0
  75. package/components/dashboard/breadcrumb.tsx +70 -0
  76. package/components/dashboard/builder-chat.tsx +418 -0
  77. package/components/dashboard/chat-shell.tsx +40 -0
  78. package/components/dashboard/client-picker.tsx +63 -0
  79. package/components/dashboard/command-snippet.tsx +77 -0
  80. package/components/dashboard/connect-dialog.tsx +680 -0
  81. package/components/dashboard/copy-button.tsx +45 -0
  82. package/components/dashboard/copy-card.tsx +48 -0
  83. package/components/dashboard/file-browser.tsx +161 -0
  84. package/components/dashboard/hint.tsx +22 -0
  85. package/components/dashboard/inference-mode.tsx +15 -0
  86. package/components/dashboard/manual-refresh-button.tsx +52 -0
  87. package/components/dashboard/markdown.tsx +31 -0
  88. package/components/dashboard/mcp-install-panel.tsx +196 -0
  89. package/components/dashboard/mission-graph.tsx +262 -0
  90. package/components/dashboard/nav-tabs.tsx +86 -0
  91. package/components/dashboard/polpo-chat.tsx +468 -0
  92. package/components/dashboard/project-copilot.tsx +253 -0
  93. package/components/dashboard/sdk-snippet-panel.tsx +194 -0
  94. package/components/dashboard/section-header.tsx +37 -0
  95. package/components/dashboard/settings-form.tsx +2477 -0
  96. package/components/dashboard/sidebar.tsx +385 -0
  97. package/components/dashboard/skeletons.tsx +1054 -0
  98. package/components/dashboard/skills-install-wizard.tsx +273 -0
  99. package/components/dashboard/swarm-runs.tsx +316 -0
  100. package/components/dashboard/tab-toggle.tsx +35 -0
  101. package/components/dashboard/task-studio.tsx +224 -0
  102. package/components/dashboard/top-header.tsx +203 -0
  103. package/components/dashboard/webhook-deliveries.tsx +234 -0
  104. package/components/icons/coding-agents.tsx +151 -0
  105. package/components/json-ld.tsx +8 -0
  106. package/components/providers.tsx +25 -0
  107. package/components/ui/badge.tsx +52 -0
  108. package/components/ui/button.tsx +60 -0
  109. package/components/ui/card.tsx +103 -0
  110. package/components/ui/chart.tsx +356 -0
  111. package/components/ui/command.tsx +196 -0
  112. package/components/ui/dialog.tsx +157 -0
  113. package/components/ui/input-group.tsx +158 -0
  114. package/components/ui/input.tsx +20 -0
  115. package/components/ui/multi-select.tsx +161 -0
  116. package/components/ui/popover.tsx +90 -0
  117. package/components/ui/select.tsx +201 -0
  118. package/components/ui/separator.tsx +25 -0
  119. package/components/ui/sheet.tsx +135 -0
  120. package/components/ui/skeleton.tsx +13 -0
  121. package/components/ui/tabs.tsx +56 -0
  122. package/components/ui/textarea.tsx +18 -0
  123. package/components/ui/tooltip.tsx +66 -0
  124. package/hooks/use-desktop-sidebar.tsx +50 -0
  125. package/hooks/use-mobile-sidebar.tsx +37 -0
  126. package/index.ts +14 -0
  127. package/lib/api.ts +194 -0
  128. package/lib/builder-context.ts +60 -0
  129. package/lib/data-client.ts +68 -0
  130. package/lib/get-query-client.ts +25 -0
  131. package/lib/polpo-client.tsx +49 -0
  132. package/lib/tool-catalog.ts +234 -0
  133. package/lib/use-event-catalog.ts +28 -0
  134. package/lib/utils.ts +6 -0
  135. package/package.json +99 -0
@@ -0,0 +1,135 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"
5
+
6
+ import { cn } from "#/lib/utils"
7
+ import { Button } from "#/components/ui/button"
8
+ import { XIcon } from "lucide-react"
9
+
10
+ function Sheet({ ...props }: SheetPrimitive.Root.Props) {
11
+ return <SheetPrimitive.Root data-slot="sheet" {...props} />
12
+ }
13
+
14
+ function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
15
+ return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
16
+ }
17
+
18
+ function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
19
+ return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
20
+ }
21
+
22
+ function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
23
+ return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
24
+ }
25
+
26
+ function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
27
+ return (
28
+ <SheetPrimitive.Backdrop
29
+ data-slot="sheet-overlay"
30
+ className={cn(
31
+ "fixed inset-0 z-50 bg-black/10 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-xs",
32
+ className
33
+ )}
34
+ {...props}
35
+ />
36
+ )
37
+ }
38
+
39
+ function SheetContent({
40
+ className,
41
+ children,
42
+ side = "right",
43
+ showCloseButton = true,
44
+ ...props
45
+ }: SheetPrimitive.Popup.Props & {
46
+ side?: "top" | "right" | "bottom" | "left"
47
+ showCloseButton?: boolean
48
+ }) {
49
+ return (
50
+ <SheetPortal>
51
+ <SheetOverlay />
52
+ <SheetPrimitive.Popup
53
+ data-slot="sheet-content"
54
+ data-side={side}
55
+ className={cn(
56
+ "fixed z-50 flex flex-col gap-4 bg-background bg-clip-padding text-sm shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
57
+ className
58
+ )}
59
+ {...props}
60
+ >
61
+ {children}
62
+ {showCloseButton && (
63
+ <SheetPrimitive.Close
64
+ data-slot="sheet-close"
65
+ render={
66
+ <Button
67
+ variant="ghost"
68
+ className="absolute top-3 right-3"
69
+ size="icon-sm"
70
+ />
71
+ }
72
+ >
73
+ <XIcon
74
+ />
75
+ <span className="sr-only">Close</span>
76
+ </SheetPrimitive.Close>
77
+ )}
78
+ </SheetPrimitive.Popup>
79
+ </SheetPortal>
80
+ )
81
+ }
82
+
83
+ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="sheet-header"
87
+ className={cn("flex flex-col gap-0.5 p-4", className)}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
94
+ return (
95
+ <div
96
+ data-slot="sheet-footer"
97
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
98
+ {...props}
99
+ />
100
+ )
101
+ }
102
+
103
+ function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
104
+ return (
105
+ <SheetPrimitive.Title
106
+ data-slot="sheet-title"
107
+ className={cn("text-base font-medium text-foreground", className)}
108
+ {...props}
109
+ />
110
+ )
111
+ }
112
+
113
+ function SheetDescription({
114
+ className,
115
+ ...props
116
+ }: SheetPrimitive.Description.Props) {
117
+ return (
118
+ <SheetPrimitive.Description
119
+ data-slot="sheet-description"
120
+ className={cn("text-sm text-muted-foreground", className)}
121
+ {...props}
122
+ />
123
+ )
124
+ }
125
+
126
+ export {
127
+ Sheet,
128
+ SheetTrigger,
129
+ SheetClose,
130
+ SheetContent,
131
+ SheetHeader,
132
+ SheetFooter,
133
+ SheetTitle,
134
+ SheetDescription,
135
+ }
@@ -0,0 +1,13 @@
1
+ import { cn } from "#/lib/utils"
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ data-slot="skeleton"
7
+ className={cn("animate-pulse rounded-md bg-muted", className)}
8
+ {...props}
9
+ />
10
+ )
11
+ }
12
+
13
+ export { Skeleton }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
4
+
5
+ import { cn } from "#/lib/utils";
6
+
7
+ const Tabs = TabsPrimitive.Root;
8
+
9
+ function TabsList({ className, ...props }: TabsPrimitive.List.Props) {
10
+ return (
11
+ <TabsPrimitive.List
12
+ data-slot="tabs-list"
13
+ className={cn(
14
+ "relative flex w-full items-center gap-1 overflow-x-auto border-b border-border scrollbar-none",
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
23
+ return (
24
+ <TabsPrimitive.Tab
25
+ data-slot="tabs-trigger"
26
+ className={cn(
27
+ // base
28
+ "relative inline-flex items-center gap-2 whitespace-nowrap px-3 py-2 text-sm text-muted-foreground transition-colors outline-none cursor-pointer",
29
+ // hover
30
+ "hover:text-foreground",
31
+ // active
32
+ "data-[selected]:text-foreground data-[selected]:font-medium",
33
+ // focus
34
+ "focus-visible:text-foreground",
35
+ // disabled
36
+ "disabled:pointer-events-none disabled:opacity-50",
37
+ // active underline (matches NavTabs — foreground, no layout shift)
38
+ "after:absolute after:-bottom-px after:left-0 after:right-0 after:h-[2px] after:bg-foreground after:opacity-0 data-[selected]:after:opacity-100 after:transition-opacity",
39
+ className,
40
+ )}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ function TabsPanel({ className, ...props }: TabsPrimitive.Panel.Props) {
47
+ return (
48
+ <TabsPrimitive.Panel
49
+ data-slot="tabs-panel"
50
+ className={cn("mt-4 outline-none", className)}
51
+ {...props}
52
+ />
53
+ );
54
+ }
55
+
56
+ export { Tabs, TabsList, TabsTrigger, TabsPanel };
@@ -0,0 +1,18 @@
1
+ import * as React from "react"
2
+
3
+ import { cn } from "#/lib/utils"
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
10
+ "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ export { Textarea }
@@ -0,0 +1,66 @@
1
+ "use client"
2
+
3
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
4
+
5
+ import { cn } from "#/lib/utils"
6
+
7
+ function TooltipProvider({
8
+ delay = 0,
9
+ ...props
10
+ }: TooltipPrimitive.Provider.Props) {
11
+ return (
12
+ <TooltipPrimitive.Provider
13
+ data-slot="tooltip-provider"
14
+ delay={delay}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
21
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
22
+ }
23
+
24
+ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
25
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
26
+ }
27
+
28
+ function TooltipContent({
29
+ className,
30
+ side = "top",
31
+ sideOffset = 4,
32
+ align = "center",
33
+ alignOffset = 0,
34
+ children,
35
+ ...props
36
+ }: TooltipPrimitive.Popup.Props &
37
+ Pick<
38
+ TooltipPrimitive.Positioner.Props,
39
+ "align" | "alignOffset" | "side" | "sideOffset"
40
+ >) {
41
+ return (
42
+ <TooltipPrimitive.Portal>
43
+ <TooltipPrimitive.Positioner
44
+ align={align}
45
+ alignOffset={alignOffset}
46
+ side={side}
47
+ sideOffset={sideOffset}
48
+ className="isolate z-50"
49
+ >
50
+ <TooltipPrimitive.Popup
51
+ data-slot="tooltip-content"
52
+ className={cn(
53
+ "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
54
+ className
55
+ )}
56
+ {...props}
57
+ >
58
+ {children}
59
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
60
+ </TooltipPrimitive.Popup>
61
+ </TooltipPrimitive.Positioner>
62
+ </TooltipPrimitive.Portal>
63
+ )
64
+ }
65
+
66
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { createContext, use, useState, useCallback, useEffect } from "react";
4
+
5
+ interface DesktopSidebarCtx {
6
+ open: boolean;
7
+ setOpen: (v: boolean) => void;
8
+ toggle: () => void;
9
+ }
10
+
11
+ const DesktopSidebarContext = createContext<DesktopSidebarCtx>({
12
+ open: true,
13
+ setOpen: () => {},
14
+ toggle: () => {},
15
+ });
16
+
17
+ const STORAGE_KEY = "polpo:sidebar-open";
18
+
19
+ export function DesktopSidebarProvider({ children }: { children: React.ReactNode }) {
20
+ const [open, setOpenState] = useState(true);
21
+
22
+ // Hydrate from localStorage after mount (avoid SSR mismatch)
23
+ useEffect(() => {
24
+ const stored = localStorage.getItem(STORAGE_KEY);
25
+ if (stored !== null) setOpenState(stored === "1");
26
+ }, []);
27
+
28
+ const setOpen = useCallback((v: boolean) => {
29
+ setOpenState(v);
30
+ localStorage.setItem(STORAGE_KEY, v ? "1" : "0");
31
+ }, []);
32
+
33
+ const toggle = useCallback(() => {
34
+ setOpenState((v) => {
35
+ const next = !v;
36
+ localStorage.setItem(STORAGE_KEY, next ? "1" : "0");
37
+ return next;
38
+ });
39
+ }, []);
40
+
41
+ return (
42
+ <DesktopSidebarContext.Provider value={{ open, setOpen, toggle }}>
43
+ {children}
44
+ </DesktopSidebarContext.Provider>
45
+ );
46
+ }
47
+
48
+ export function useDesktopSidebar() {
49
+ return use(DesktopSidebarContext);
50
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { createContext, use, useState, useCallback, useEffect } from "react";
4
+ import { usePathname } from "next/navigation";
5
+
6
+ interface MobileSidebarCtx {
7
+ open: boolean;
8
+ setOpen: (v: boolean) => void;
9
+ toggle: () => void;
10
+ }
11
+
12
+ const MobileSidebarContext = createContext<MobileSidebarCtx>({
13
+ open: false,
14
+ setOpen: () => {},
15
+ toggle: () => {},
16
+ });
17
+
18
+ export function MobileSidebarProvider({ children }: { children: React.ReactNode }) {
19
+ const [open, setOpen] = useState(false);
20
+ const pathname = usePathname();
21
+ const toggle = useCallback(() => setOpen((v) => !v), []);
22
+
23
+ // Close sidebar on navigation
24
+ useEffect(() => {
25
+ setOpen(false);
26
+ }, [pathname]);
27
+
28
+ return (
29
+ <MobileSidebarContext.Provider value={{ open, setOpen, toggle }}>
30
+ {children}
31
+ </MobileSidebarContext.Provider>
32
+ );
33
+ }
34
+
35
+ export function useMobileSidebar() {
36
+ return use(MobileSidebarContext);
37
+ }
package/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Public surface of @polpo-ai/dashboard — the per-project views plus the
3
+ * data-client seam, for a host app (e.g. the cloud dashboard) to render inside
4
+ * its own shell with its own transport injected via <DataClientProvider>.
5
+ *
6
+ * Phase-2 pilot: the agents view. The remaining views follow the same shape
7
+ * (default-exported `<XView>` + a server `page.tsx` that fetches initial data).
8
+ */
9
+
10
+ export { DataClientProvider, createPolpoClient } from "./lib/polpo-client";
11
+ export type { PolpoClientFactory } from "./lib/polpo-client";
12
+
13
+ export { default as AgentsView } from "./app/(dashboard)/projects/[id]/agents/view";
14
+ export type { Team } from "./app/(dashboard)/projects/[id]/agents/view";
package/lib/api.ts ADDED
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Server-side API client for the Control Plane.
3
+ * Used by Server Components to fetch data directly — no BFF, no API routes proxy.
4
+ *
5
+ * Data plane types (AgentConfig, Task, Mission, Team, etc.) come from @polpo-ai/core.
6
+ * Control plane types (Org, Project, ApiKey, ByokEntry) are defined here.
7
+ */
8
+
9
+ import { cache } from "react";
10
+ import { cookies } from "next/headers";
11
+ import { redirect } from "next/navigation";
12
+ import type { AgentConfig } from "@polpo-ai/core";
13
+
14
+ const API_URL = process.env.POLPO_API_URL ?? process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
15
+ const API_PREFIX = process.env.NEXT_PUBLIC_POLPO_API_PREFIX ?? "/api/v1";
16
+ const API_KEY = process.env.POLPO_API_KEY ?? process.env.NEXT_PUBLIC_POLPO_API_KEY;
17
+
18
+ /** Control-plane API error carrying the HTTP status, so callers can tell an
19
+ * auth failure (401) apart from a transient backend error (5xx/network). */
20
+ export class ApiError extends Error {
21
+ constructor(
22
+ public readonly status: number,
23
+ message: string,
24
+ ) {
25
+ super(message);
26
+ this.name = "ApiError";
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Authenticated server-side fetch to the control plane.
32
+ * Forwards session cookies from the incoming request.
33
+ */
34
+ export async function api<T>(path: string, opts?: RequestInit): Promise<T> {
35
+ const cookieStore = await cookies();
36
+ const cookieHeader = cookieStore.toString();
37
+
38
+ const res = await fetch(`${API_URL}${path}`, {
39
+ ...opts,
40
+ headers: {
41
+ "Content-Type": "application/json",
42
+ cookie: cookieHeader,
43
+ ...opts?.headers,
44
+ },
45
+ cache: "no-store",
46
+ });
47
+
48
+ if (!res.ok) {
49
+ throw new ApiError(res.status, `API error ${res.status}: ${await res.text()}`);
50
+ }
51
+
52
+ return res.json();
53
+ }
54
+
55
+ /**
56
+ * Authenticated fetch to the data plane via session proxy.
57
+ * Routes through /v1/projects/:projectId/data/* which uses session auth.
58
+ * No API key needed — the server resolves the project internally.
59
+ */
60
+ export async function dataApi<T>(_projectId: string, path: string, opts?: RequestInit): Promise<T> {
61
+ // Single-tenant self-host: straight at the OSS server (`/api/v1/...`) with a
62
+ // Bearer key. projectId is ignored. Callers pass `/v1/...`, so strip the
63
+ // leading `/v1` and let API_PREFIX carry the root. Cloud overrides with its
64
+ // session-proxy variant.
65
+ const res = await fetch(`${API_URL}${API_PREFIX}${path.replace(/^\/v1(?=\/|$)/, "")}`, {
66
+ ...opts,
67
+ headers: {
68
+ "Content-Type": "application/json",
69
+ ...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}),
70
+ ...opts?.headers,
71
+ },
72
+ cache: "no-store",
73
+ });
74
+
75
+ if (!res.ok) {
76
+ throw new Error(`Data API error ${res.status}: ${await res.text()}`);
77
+ }
78
+
79
+ return res.json();
80
+ }
81
+
82
+ /**
83
+ * Cached fetch of the user's orgs — deduplicated per request via React.cache().
84
+ * Multiple server components in the same render can call this without duplicate HTTP requests.
85
+ */
86
+ export const getOrgs = cache(async (): Promise<OrgWithRole[]> => {
87
+ try {
88
+ return await api<OrgWithRole[]>("/v1/orgs");
89
+ } catch (err) {
90
+ // A missing/expired session is NOT "no orgs" — sending the user to
91
+ // onboarding here was the bug that dumped people with a stale `.polpo.sh`
92
+ // cookie onto /onboarding. Re-authenticate instead.
93
+ if (err instanceof ApiError && err.status === 401) redirect("/login");
94
+ // Transient backend failure (5xx / network / DB down): surface it as a
95
+ // real error (error boundary) rather than masquerading as "no orgs".
96
+ throw err;
97
+ }
98
+ });
99
+
100
+ /**
101
+ * Non-throwing org fetch — returns `[]` on ANY failure (auth or transient).
102
+ * For contexts where a redirect/throw is wrong, e.g. API route handlers
103
+ * (billing identify) that must always return a response.
104
+ */
105
+ export const getOrgsSafe = cache(() =>
106
+ api<OrgWithRole[]>("/v1/orgs").catch(() => [] as OrgWithRole[])
107
+ );
108
+
109
+ /**
110
+ * Cached fetch of a single agent config — deduplicated per request via
111
+ * React.cache(). The agent detail layout and every tab page (capabilities,
112
+ * identity, tools, skills, prompt) call this with the same args, so within
113
+ * one server render the agent is fetched once instead of N times.
114
+ * Returns null on any error (renders the "Agent not found" empty state).
115
+ */
116
+ export const getAgent = cache((projectId: string, name: string) =>
117
+ dataApi<{ ok: boolean; data: AgentConfig }>(
118
+ projectId,
119
+ `/v1/agents/${encodeURIComponent(name)}`,
120
+ )
121
+ .then((r) => r.data ?? null)
122
+ .catch(() => null),
123
+ );
124
+
125
+ // ── Control plane types (cloud-only, match packages/server/src/db/schema.ts) ──
126
+
127
+ export interface Org {
128
+ id: string;
129
+ name: string;
130
+ slug: string;
131
+ createdAt: string;
132
+ updatedAt: string;
133
+ }
134
+
135
+ export interface OrgWithRole extends Org {
136
+ role: "owner" | "admin" | "member";
137
+ }
138
+
139
+ export interface ProjectGatewaySettings {
140
+ url?: string;
141
+ headers?: Record<string, string>;
142
+ }
143
+
144
+ export interface ProjectSettings {
145
+ gateway?: ProjectGatewaySettings;
146
+ [key: string]: unknown;
147
+ }
148
+
149
+ export interface OnboardingChecklist {
150
+ firstCompletion?: string;
151
+ firstSkill?: string;
152
+ firstTool?: string;
153
+ firstTask?: string;
154
+ firstDeploy?: string;
155
+ }
156
+
157
+ export interface Project {
158
+ id: string;
159
+ orgId: string;
160
+ name: string;
161
+ slug: string;
162
+ settings?: ProjectSettings | null;
163
+ status: "active" | "suspended" | "deleted";
164
+ onboardingChecklist?: OnboardingChecklist;
165
+ neonProjectId?: string | null;
166
+ neonConnectionString?: string | null;
167
+ createdAt: string;
168
+ updatedAt: string;
169
+ }
170
+
171
+ export interface ApiKey {
172
+ id: string;
173
+ projectId: string;
174
+ name: string;
175
+ keyPrefix: string;
176
+ keyHash?: string;
177
+ environment: "live" | "test";
178
+ lastUsedAt: string | null;
179
+ expiresAt: string | null;
180
+ createdAt: string;
181
+ revokedAt: string | null;
182
+ }
183
+
184
+ export interface ApiKeyCreated extends Omit<ApiKey, "keyHash"> {
185
+ rawKey: string;
186
+ }
187
+
188
+ export interface ByokEntry {
189
+ provider: string;
190
+ label: string | null;
191
+ maskedKey: string;
192
+ createdAt: string;
193
+ updatedAt: string;
194
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Derives the Builder/Copilot "context" from the current route, so the
5
+ * global side panel knows what the user is looking at and the Meta Agent
6
+ * can act accordingly (e.g. on an agent page → operate on that agent; on a
7
+ * skill page → that skill; elsewhere → the project section).
8
+ *
9
+ * Returns a STABLE object (memoised by value) so consumers can use it as a
10
+ * hook dependency without churning.
11
+ */
12
+ import { useMemo } from "react";
13
+ import { useParams, usePathname } from "next/navigation";
14
+
15
+ export interface BuilderContext {
16
+ type: "agent" | "skill" | "section" | "project";
17
+ /** Human label shown in the panel's context strip, e.g. "Agent · sales-bot". */
18
+ label: string;
19
+ agentName?: string;
20
+ skill?: string;
21
+ section?: string;
22
+ }
23
+
24
+ /** Where the Meta Agent's `navigate` tool wants to send the user. */
25
+ export interface NavigateTarget {
26
+ section: "agents" | "skills" | "tasks" | "missions" | "memory" | "playground" | "overview";
27
+ name?: string | null;
28
+ /** Agent Studio tab to focus (models/prompt/tools/skills/memory/vault). */
29
+ tab?: string | null;
30
+ }
31
+
32
+ export function useBuilderContext(): {
33
+ projectId: string | null;
34
+ context: BuilderContext;
35
+ } {
36
+ const params = useParams<{ id?: string; name?: string }>();
37
+ const pathname = usePathname() ?? "";
38
+
39
+ const projectId = params?.id ?? null;
40
+ const name = params?.name ? decodeURIComponent(params.name) : undefined;
41
+
42
+ // First path segment after /projects/:id/ — "agents", "skills", "tasks", …
43
+ const rest = pathname.match(/\/projects\/[^/]+\/([^?#]*)/)?.[1] ?? "";
44
+ const section = rest.split("/").filter(Boolean)[0];
45
+
46
+ return useMemo(() => {
47
+ let context: BuilderContext;
48
+ if (section === "agents" && name) {
49
+ context = { type: "agent", agentName: name, label: `Agent · ${name}` };
50
+ } else if (section === "skills" && name) {
51
+ context = { type: "skill", skill: name, label: `Skill · ${name}` };
52
+ } else if (section) {
53
+ const label = section.charAt(0).toUpperCase() + section.slice(1);
54
+ context = { type: "section", section, label };
55
+ } else {
56
+ context = { type: "project", label: "Project" };
57
+ }
58
+ return { projectId, context };
59
+ }, [projectId, section, name]);
60
+ }