@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
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2026-present Lumea Labs
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @polpo-ai/dashboard
2
+
3
+ The self-hosted Polpo dashboard — the single-instance debugger UI for one Polpo
4
+ server: agents, tasks, missions, sessions, memory, skills, playbooks, schedules,
5
+ storage, logs and a chat playground.
6
+
7
+ It talks to your Polpo server through the public SDK (`@polpo-ai/sdk`). There is
8
+ no auth, billing, org or onboarding here — those are cloud-only. One server, one
9
+ dashboard.
10
+
11
+ ## Run it
12
+
13
+ You need a running Polpo server (the data plane). Then:
14
+
15
+ ```bash
16
+ cp .env.example .env.local # point NEXT_PUBLIC_API_URL at your server
17
+ pnpm install
18
+ pnpm dev # http://localhost:3000
19
+ ```
20
+
21
+ `/` redirects to `/projects/local/agents`. `local` is a placeholder project id —
22
+ single-tenant self-host has one project, and the data client ignores the id.
23
+
24
+ ## Config
25
+
26
+ | Env var | Default | What |
27
+ | --- | --- | --- |
28
+ | `NEXT_PUBLIC_API_URL` | `http://localhost:4000` | Your Polpo server base URL |
29
+ | `NEXT_PUBLIC_POLPO_API_PREFIX` | `/api/v1` | API root the server mounts under (the standalone server serves at `/api/v1`) |
30
+ | `NEXT_PUBLIC_POLPO_API_KEY` | — | `sk_...` key for the server (optional if it runs without auth) |
31
+
32
+ ## How the data layer works
33
+
34
+ Views address the data plane by path (`/v1/agents`, `/v1/tasks`, …). `lib/data-client.ts`
35
+ drives a single `PolpoClient` from `@polpo-ai/sdk` (transport, auth, errors) and
36
+ re-wraps the result in the `{ ok, data }` envelope the views expect. Swapping the
37
+ server is just `NEXT_PUBLIC_API_URL`.
38
+
39
+ ## Not wired yet
40
+
41
+ A few shell/settings surfaces still reach for cloud-only control-plane endpoints
42
+ (project name, BYOK keys, billing). These are gated to single-tenant stubs so the
43
+ dashboard runs; see the `TODO` in `lib/data-client.ts`. BYOK will be re-pointed to
44
+ the data-plane `vault` route.
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Error boundary for the dashboard group. A transient backend failure
5
+ * (control-plane 5xx / network / DB unreachable) now surfaces here instead
6
+ * of being swallowed into "no orgs → /onboarding". Auth failures (401) are
7
+ * handled upstream by `getOrgs` (redirect to /login).
8
+ */
9
+ export default function DashboardError({
10
+ error,
11
+ reset,
12
+ }: {
13
+ error: Error & { digest?: string };
14
+ reset: () => void;
15
+ }) {
16
+ return (
17
+ <div className="flex min-h-[60vh] items-center justify-center p-6">
18
+ <div className="w-full max-w-md border border-border bg-card p-8 text-center">
19
+ <h2 className="text-base font-semibold text-foreground">Something went wrong</h2>
20
+ <p className="mt-2 text-sm text-muted-foreground">
21
+ We couldn&apos;t reach the control plane. This is usually temporary —
22
+ give it a moment and retry.
23
+ </p>
24
+ {error?.digest ? (
25
+ <p className="mt-3 font-mono text-[11px] text-muted-foreground/50">
26
+ ref: {error.digest}
27
+ </p>
28
+ ) : null}
29
+ <div className="mt-6 flex items-center justify-center gap-3">
30
+ <button
31
+ type="button"
32
+ onClick={() => reset()}
33
+ className="border border-foreground bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90"
34
+ >
35
+ Try again
36
+ </button>
37
+ <a
38
+ href="/login"
39
+ className="border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-foreground/40"
40
+ >
41
+ Sign in
42
+ </a>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,41 @@
1
+ import { Sidebar, MobileSidebar, MobileHeader } from "#/components/dashboard/sidebar";
2
+ import { TopHeader } from "#/components/dashboard/top-header";
3
+ import { Providers } from "#/components/providers";
4
+ import { MobileSidebarProvider } from "#/hooks/use-mobile-sidebar";
5
+ import { DesktopSidebarProvider } from "#/hooks/use-desktop-sidebar";
6
+ import { CopilotLayout } from "#/components/dashboard/project-copilot";
7
+
8
+ /**
9
+ * Single-tenant self-host shell. No Better Auth session, no org list, no
10
+ * onboarding gate — those live only in the cloud build. One local instance,
11
+ * straight into the dashboard chrome.
12
+ */
13
+ export default function DashboardLayout({
14
+ children,
15
+ }: {
16
+ children: React.ReactNode;
17
+ }) {
18
+ return (
19
+ <Providers>
20
+ <MobileSidebarProvider>
21
+ <DesktopSidebarProvider>
22
+ <div className="flex h-screen flex-col overflow-hidden">
23
+ <div className="hidden md:block">
24
+ <TopHeader />
25
+ </div>
26
+
27
+ <div className="flex flex-1 overflow-hidden">
28
+ <Sidebar />
29
+ <MobileSidebar />
30
+
31
+ <div className="flex flex-1 flex-col overflow-hidden">
32
+ <MobileHeader />
33
+ <CopilotLayout>{children}</CopilotLayout>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ </DesktopSidebarProvider>
38
+ </MobileSidebarProvider>
39
+ </Providers>
40
+ );
41
+ }
@@ -0,0 +1,30 @@
1
+ export default function DashboardLoading() {
2
+ return (
3
+ <div className="px-4 py-6 md:px-8 md:py-8">
4
+ <div className="space-y-4">
5
+ {/* Header skeleton */}
6
+ <div className="h-8 w-48 animate-pulse rounded bg-muted" />
7
+ <div className="h-4 w-80 animate-pulse rounded bg-muted/60" />
8
+
9
+ {/* Content skeleton */}
10
+ <div className="mt-8 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
11
+ {Array.from({ length: 4 }).map((_, i) => (
12
+ <div
13
+ key={i}
14
+ className="h-24 animate-pulse rounded-lg border border-border bg-card"
15
+ />
16
+ ))}
17
+ </div>
18
+
19
+ <div className="mt-8 space-y-3">
20
+ {Array.from({ length: 5 }).map((_, i) => (
21
+ <div
22
+ key={i}
23
+ className="h-12 animate-pulse rounded border border-border bg-card"
24
+ />
25
+ ))}
26
+ </div>
27
+ </div>
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,36 @@
1
+ import Link from "next/link";
2
+ import { Markdown } from "#/components/dashboard/markdown";
3
+ import { CopyButton } from "#/components/dashboard/copy-button";
4
+ import { Hint } from "#/components/dashboard/hint";
5
+
6
+ export default function AgentMemoryView({ id, content }: { id: string; content: string }) {
7
+ return (
8
+ <div>
9
+ <div className="mb-4">
10
+ <Hint action={content ? <CopyButton value={content} label="Copy" className="shrink-0" /> : undefined}>
11
+ Private memory this agent accumulates across sessions. It also inherits the
12
+ shared project memory.
13
+ </Hint>
14
+ </div>
15
+
16
+ {content ? (
17
+ <div data-testid="agent-memory-content" className="border border-border bg-card p-6">
18
+ <Markdown content={content} />
19
+ </div>
20
+ ) : (
21
+ <div className="border border-border p-8 text-center text-sm text-muted-foreground">
22
+ No memory yet. Memory is created automatically when the agent works.
23
+ </div>
24
+ )}
25
+
26
+ <div className="mt-4">
27
+ <Link
28
+ href={`/projects/${id}/memory`}
29
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors"
30
+ >
31
+ View shared project memory &rarr;
32
+ </Link>
33
+ </div>
34
+ </div>
35
+ );
36
+ }
@@ -0,0 +1,30 @@
1
+ import type { AgentConfig } from "@polpo-ai/core";
2
+ import { AgentCapabilities } from "#/components/dashboard/agent-capabilities";
3
+
4
+ /** Models tab — the per-capability model grid (one row per modality). */
5
+ export default function AgentModelsView({
6
+ agent,
7
+ projectId,
8
+ agentName,
9
+ }: {
10
+ agent: AgentConfig | null;
11
+ projectId: string;
12
+ agentName: string;
13
+ }) {
14
+ if (!agent) {
15
+ return (
16
+ <div
17
+ data-testid="agent-not-found"
18
+ className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground"
19
+ >
20
+ Agent not found.
21
+ </div>
22
+ );
23
+ }
24
+
25
+ return (
26
+ <section className="pt-2">
27
+ <AgentCapabilities agent={agent} projectId={projectId} agentName={agentName} />
28
+ </section>
29
+ );
30
+ }
@@ -0,0 +1,51 @@
1
+ import { Suspense } from "react";
2
+ import { dataApi, getAgent } from "#/lib/api";
3
+ import { AgentProfileSkeleton } from "#/components/dashboard/skeletons";
4
+ import { AgentStudio, type VaultEntry } from "#/components/dashboard/agent-studio";
5
+
6
+ const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
7
+
8
+ async function AgentData({ id, name }: { id: string; name: string }) {
9
+ // The agent detail IS the Studio (chat canvas). Fetch everything its
10
+ // tabs need (agent + memory + vault) up front, server-side.
11
+ const [agent, memoryContent, vaultEntries] = await Promise.all([
12
+ getAgent(id, name),
13
+ dataApi<{ ok: boolean; data: { content: string } }>(
14
+ id,
15
+ `/v1/memory/agent/${encodeURIComponent(name)}`,
16
+ )
17
+ .then((r) => r.data?.content ?? "")
18
+ .catch(() => ""),
19
+ dataApi<{ ok: boolean; data: VaultEntry[] }>(
20
+ id,
21
+ `/v1/vault/entries/${encodeURIComponent(name)}`,
22
+ )
23
+ .then((r) => r.data ?? [])
24
+ .catch(() => [] as VaultEntry[]),
25
+ ]);
26
+
27
+ return (
28
+ <AgentStudio
29
+ projectId={id}
30
+ apiUrl={API_URL}
31
+ agent={agent}
32
+ memoryContent={memoryContent}
33
+ vaultEntries={vaultEntries}
34
+ />
35
+ );
36
+ }
37
+
38
+ export default async function AgentDetailPage({
39
+ params,
40
+ }: {
41
+ params: Promise<{ id: string; name: string }>;
42
+ }) {
43
+ const { id, name } = await params;
44
+ const decodedName = decodeURIComponent(name);
45
+
46
+ return (
47
+ <Suspense fallback={<AgentProfileSkeleton />}>
48
+ <AgentData id={id} name={decodedName} />
49
+ </Suspense>
50
+ );
51
+ }
@@ -0,0 +1,211 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Eye } from "lucide-react";
5
+ import type { AgentConfig } from "@polpo-ai/core";
6
+ import { Markdown } from "#/components/dashboard/markdown";
7
+ import { CopyButton } from "#/components/dashboard/copy-button";
8
+ import { Hint } from "#/components/dashboard/hint";
9
+ import {
10
+ Dialog,
11
+ DialogContent,
12
+ DialogDescription,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ } from "#/components/ui/dialog";
16
+
17
+ const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
18
+
19
+ /** Rough token estimate — ~4 chars/token, good enough for a budget hint. */
20
+ const approxTokens = (s: string) => Math.round(s.length / 4);
21
+
22
+ export default function AgentPromptView({
23
+ agent,
24
+ projectId,
25
+ agentName,
26
+ }: {
27
+ agent: AgentConfig | null;
28
+ projectId: string;
29
+ agentName: string;
30
+ }) {
31
+ if (!agent) {
32
+ return (
33
+ <div className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
34
+ Agent not found.
35
+ </div>
36
+ );
37
+ }
38
+
39
+ const instructions = agent.systemPrompt ?? "";
40
+ const chars = instructions.length;
41
+
42
+ return (
43
+ <div>
44
+ <Hint action={<PreviewButton projectId={projectId} agentName={agentName} />}>
45
+ Extra instructions added on top of the agent&apos;s base prompt — they shape
46
+ tone and rules without replacing Polpo&apos;s defaults. Hit Preview for the
47
+ full composed prompt.
48
+ </Hint>
49
+
50
+ {instructions ? (
51
+ <>
52
+ <div className="mt-2 flex items-center gap-2">
53
+ <span className="inline-flex items-center gap-1 border border-border bg-secondary/40 px-2 py-0.5 font-mono text-[11px] font-medium text-foreground">
54
+ {chars.toLocaleString()}
55
+ <span className="text-muted-foreground">chars</span>
56
+ </span>
57
+ <span className="inline-flex items-center gap-1 border border-border bg-secondary/40 px-2 py-0.5 font-mono text-[11px] font-medium text-foreground">
58
+ ~{approxTokens(instructions).toLocaleString()}
59
+ <span className="text-muted-foreground">tokens</span>
60
+ </span>
61
+ </div>
62
+ <div className="mt-3 border border-border bg-card p-6">
63
+ <Markdown content={instructions} />
64
+ </div>
65
+ </>
66
+ ) : (
67
+ <div className="mt-3 border border-border p-8 text-center">
68
+ <p className="text-sm text-muted-foreground">No extra instructions configured.</p>
69
+ <p className="mt-1 text-xs text-muted-foreground">
70
+ This agent runs on the default base prompt. Preview it to see what that includes.
71
+ </p>
72
+ </div>
73
+ )}
74
+ </div>
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Loading state for the prompt preview — a sharp skeleton that mimics the
80
+ * composed prompt materialising (metadata bar + paragraph blocks of
81
+ * varying width), in keeping with the rest of the surface.
82
+ */
83
+ function PromptSkeleton() {
84
+ // Width pattern per "paragraph" — last line of each block is shorter,
85
+ // so it reads as real prose rather than a uniform block.
86
+ const blocks = [
87
+ ["w-1/3"],
88
+ ["w-full", "w-full", "w-11/12", "w-2/3"],
89
+ ["w-full", "w-5/6", "w-full", "w-1/2"],
90
+ ["w-3/4", "w-full", "w-4/5"],
91
+ ];
92
+ let line = 0;
93
+ return (
94
+ <div className="flex min-h-0 flex-1 flex-col">
95
+ <div className="flex items-center justify-between gap-4 border-y border-border py-2">
96
+ <span className="h-3 w-40 animate-pulse bg-muted" />
97
+ <span className="h-6 w-16 animate-pulse bg-muted" />
98
+ </div>
99
+ <div className="min-h-0 flex-1 space-y-5 overflow-hidden bg-card p-4">
100
+ {blocks.map((widths, b) => (
101
+ <div key={b} className="space-y-2">
102
+ {widths.map((w) => {
103
+ const delay = (line++ % 6) * 90;
104
+ return (
105
+ <div
106
+ key={w + line}
107
+ className={`h-3 ${w} animate-pulse bg-muted`}
108
+ style={{ animationDelay: `${delay}ms` }}
109
+ />
110
+ );
111
+ })}
112
+ </div>
113
+ ))}
114
+ </div>
115
+ <p className="border-t border-border pt-2 font-mono text-[11px] text-muted-foreground/60">
116
+ Composing the full prompt…
117
+ </p>
118
+ </div>
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Preview — fetches the FULLY composed system prompt from the data plane
124
+ * (`GET /v1/agents/:name/prompt`), which runs the same `buildAgentSystemPrompt`
125
+ * + skill-loading the runtime uses. Shows the exact text sent to the model.
126
+ */
127
+ function PreviewButton({
128
+ projectId,
129
+ agentName,
130
+ }: {
131
+ projectId: string;
132
+ agentName: string;
133
+ }) {
134
+ const [open, setOpen] = useState(false);
135
+ const [loading, setLoading] = useState(false);
136
+ const [prompt, setPrompt] = useState<string | null>(null);
137
+ const [error, setError] = useState<string | null>(null);
138
+
139
+ async function load() {
140
+ setOpen(true);
141
+ setLoading(true);
142
+ setError(null);
143
+ setPrompt(null);
144
+ try {
145
+ const res = await fetch(
146
+ `${API_URL}/v1/projects/${projectId}/data/v1/agents/${encodeURIComponent(agentName)}/prompt`,
147
+ { credentials: "include" },
148
+ );
149
+ const body = await res.json().catch(() => ({}));
150
+ if (!res.ok || !body.ok) {
151
+ setError(body.error ?? `Failed to compose prompt (${res.status})`);
152
+ } else {
153
+ setPrompt(body.data?.prompt ?? "");
154
+ }
155
+ } catch (err) {
156
+ setError((err as Error).message ?? "Failed to compose prompt");
157
+ }
158
+ setLoading(false);
159
+ }
160
+
161
+ return (
162
+ <>
163
+ <button
164
+ type="button"
165
+ onClick={() => void load()}
166
+ className="inline-flex shrink-0 items-center gap-1.5 border border-border bg-card px-2.5 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
167
+ >
168
+ <Eye className="h-3.5 w-3.5" strokeWidth={1.5} />
169
+ Preview
170
+ </button>
171
+
172
+ <Dialog open={open} onOpenChange={setOpen}>
173
+ <DialogContent className="flex max-h-[85vh] flex-col overflow-hidden sm:max-w-3xl">
174
+ <DialogHeader>
175
+ <DialogTitle>Composed system prompt</DialogTitle>
176
+ <DialogDescription>
177
+ The complete prompt the platform sends to the model — base prompt,
178
+ identity, your instructions and loaded skills, exactly as the
179
+ runtime composes it.
180
+ </DialogDescription>
181
+ </DialogHeader>
182
+
183
+ {loading ? (
184
+ <PromptSkeleton />
185
+ ) : error ? (
186
+ <p className="py-6 text-sm text-destructive">{error}</p>
187
+ ) : prompt !== null ? (
188
+ <>
189
+ <div className="flex items-center justify-between gap-4 border-y border-border py-2">
190
+ <div className="flex items-center gap-2">
191
+ <span className="inline-flex items-center gap-1 border border-border bg-secondary/40 px-2 py-0.5 font-mono text-[11px] font-medium text-foreground">
192
+ {prompt.length.toLocaleString()}
193
+ <span className="text-muted-foreground">chars</span>
194
+ </span>
195
+ <span className="inline-flex items-center gap-1 border border-border bg-secondary/40 px-2 py-0.5 font-mono text-[11px] font-medium text-foreground">
196
+ ~{approxTokens(prompt).toLocaleString()}
197
+ <span className="text-muted-foreground">tokens</span>
198
+ </span>
199
+ </div>
200
+ <CopyButton value={prompt} label="Copy" className="shrink-0" />
201
+ </div>
202
+ <div className="min-h-0 flex-1 overflow-auto bg-card p-4">
203
+ <Markdown content={prompt} />
204
+ </div>
205
+ </>
206
+ ) : null}
207
+ </DialogContent>
208
+ </Dialog>
209
+ </>
210
+ );
211
+ }
@@ -0,0 +1,84 @@
1
+ import Link from "next/link";
2
+ import type { AgentConfig } from "@polpo-ai/core";
3
+ import { BookMarked, ExternalLink } from "lucide-react";
4
+ import { SectionHeader } from "#/components/dashboard/section-header";
5
+
6
+ export default function AgentSkillsView({
7
+ agent,
8
+ projectId,
9
+ }: {
10
+ agent: AgentConfig | null;
11
+ projectId: string;
12
+ }) {
13
+ if (!agent) {
14
+ return (
15
+ <div
16
+ data-testid="agent-not-found"
17
+ className="border border-border p-8 text-center text-sm text-muted-foreground"
18
+ >
19
+ Agent not found.
20
+ </div>
21
+ );
22
+ }
23
+
24
+ const skills = agent.skills ?? [];
25
+
26
+ return (
27
+ <div>
28
+ <SectionHeader
29
+ description={
30
+ <>
31
+ Installed skills extend this agent with reusable procedures. Manage
32
+ them with <span className="font-mono">polpo install</span> or in the
33
+ agent config <span className="font-mono">skills</span> field.
34
+ </>
35
+ }
36
+ />
37
+
38
+ {skills.length === 0 ? (
39
+ <div className="mt-4 border border-dashed border-border p-8 text-center">
40
+ <BookMarked
41
+ className="mx-auto h-5 w-5 text-muted-foreground/40"
42
+ strokeWidth={1.5}
43
+ />
44
+ <p className="mt-2 text-sm text-muted-foreground">No skills installed.</p>
45
+ <p className="mt-1 text-xs text-muted-foreground/60">
46
+ Browse the catalog at{" "}
47
+ <a
48
+ href="https://docs.polpo.sh/docs/agents/skills"
49
+ target="_blank"
50
+ rel="noopener noreferrer"
51
+ className="underline decoration-dotted underline-offset-2 transition-colors hover:text-foreground"
52
+ >
53
+ docs.polpo.sh
54
+ </a>
55
+ .
56
+ </p>
57
+ </div>
58
+ ) : (
59
+ <div className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
60
+ {skills.map((skill) => (
61
+ <Link
62
+ key={skill}
63
+ href={`/projects/${projectId}/skills/${encodeURIComponent(skill)}`}
64
+ target="_blank"
65
+ rel="noopener noreferrer"
66
+ className="group flex items-center gap-2.5 border border-border bg-card p-3 transition-colors hover:border-foreground/30"
67
+ >
68
+ <span className="grid size-7 shrink-0 place-items-center bg-secondary text-muted-foreground">
69
+ <BookMarked className="h-3.5 w-3.5" strokeWidth={1.5} />
70
+ </span>
71
+ <span className="min-w-0 flex-1 truncate font-mono text-sm text-foreground">
72
+ {skill}
73
+ </span>
74
+ <ExternalLink
75
+ className="h-3.5 w-3.5 shrink-0 text-muted-foreground/40 transition-colors group-hover:text-foreground"
76
+ strokeWidth={1.5}
77
+ />
78
+ </Link>
79
+ ))}
80
+ </div>
81
+ )}
82
+ </div>
83
+ );
84
+ }