@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,336 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useQuery } from "@tanstack/react-query";
5
+ import type { Task } from "@polpo-ai/core";
6
+ import { usePolpoClient } from "#/lib/polpo-client";
7
+ import { ManualRefreshButton } from "#/components/dashboard/manual-refresh-button";
8
+ import type { BlueprintContext } from "../blueprint";
9
+
10
+ export interface LogEntry {
11
+ ts: string;
12
+ event: string;
13
+ data: unknown;
14
+ }
15
+
16
+ export interface RunRecord {
17
+ id: string;
18
+ taskId: string;
19
+ agentName: string;
20
+ sessionId?: string;
21
+ status: string;
22
+ startedAt: string;
23
+ updatedAt: string;
24
+ activity: {
25
+ toolCalls: number;
26
+ totalTokens: number;
27
+ filesCreated: string[];
28
+ filesEdited: string[];
29
+ lastUpdate: string;
30
+ summary?: string;
31
+ sessionId?: string;
32
+ };
33
+ }
34
+
35
+ export interface TaskActivityPayload {
36
+ task: Task | null;
37
+ run: RunRecord | null;
38
+ sessionId: string | null;
39
+ sessionResolution: "explicit" | "matched-log-session" | "missing";
40
+ entries: LogEntry[];
41
+ blueprint?: BlueprintContext | null;
42
+ }
43
+
44
+ function formatTime(ts: string) {
45
+ return new Date(ts).toLocaleTimeString("en-US", {
46
+ hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
47
+ });
48
+ }
49
+
50
+ function formatDuration(ms: number) {
51
+ const s = Math.floor(ms / 1000);
52
+ if (s < 60) return `${s}s`;
53
+ return `${Math.floor(s / 60)}m ${s % 60}s`;
54
+ }
55
+
56
+ interface Row {
57
+ id: string;
58
+ type: "tool_use" | "tool_result" | "assistant" | "event";
59
+ ts: string;
60
+ tool?: string;
61
+ content?: string;
62
+ state?: string;
63
+ args?: string;
64
+ isError?: boolean;
65
+ }
66
+
67
+ function parseEntries(entries: LogEntry[]): Row[] {
68
+ const rows: Row[] = [];
69
+ for (const entry of entries) {
70
+ const d = (entry.data ?? {}) as Record<string, unknown>;
71
+ const type = (d.type as string) ?? (d.role as string) ?? entry.event.replace("transcript:", "");
72
+
73
+ if (type === "tool_use" || entry.event === "transcript:tool_use") {
74
+ const input = d.input ?? d.arguments;
75
+ rows.push({
76
+ id: `${entry.ts}-${rows.length}`,
77
+ type: "tool_use",
78
+ ts: entry.ts,
79
+ tool: (d.tool ?? d.name ?? d.toolName) as string,
80
+ args: input != null
81
+ ? (typeof input === "string" ? input : JSON.stringify(input, null, 2))
82
+ : undefined,
83
+ });
84
+ } else if (type === "tool_result" || entry.event === "transcript:tool_result") {
85
+ const content = d.content ?? d.result ?? d.output ?? d.error;
86
+ rows.push({
87
+ id: `${entry.ts}-${rows.length}`,
88
+ type: "tool_result",
89
+ ts: entry.ts,
90
+ tool: ((d.tool ?? d.name ?? d.toolName) as string | undefined) ?? "result",
91
+ content: content != null ? String(content) : undefined,
92
+ isError: Boolean(d.isError ?? d.error),
93
+ state: (d.isError || d.error) ? "error" : undefined,
94
+ });
95
+ } else if (type === "assistant" || entry.event === "transcript:assistant") {
96
+ const content = d.text ?? d.content ?? d.message;
97
+ rows.push({
98
+ id: `${entry.ts}-${rows.length}`,
99
+ type: "assistant",
100
+ ts: entry.ts,
101
+ content: content != null ? String(content) : undefined,
102
+ });
103
+ } else {
104
+ const content = typeof entry.data === "string"
105
+ ? entry.data
106
+ : d.text ?? d.content ?? d.message ?? d.result;
107
+ rows.push({
108
+ id: `${entry.ts}-${rows.length}`,
109
+ type: "event",
110
+ ts: entry.ts,
111
+ tool: entry.event,
112
+ content: content != null ? String(content) : undefined,
113
+ });
114
+ }
115
+ }
116
+ return rows.sort((a, b) => {
117
+ const bTime = Date.parse(b.ts) || 0;
118
+ const aTime = Date.parse(a.ts) || 0;
119
+ return bTime - aTime;
120
+ });
121
+ }
122
+
123
+ interface TaskActivityViewProps {
124
+ projectId: string;
125
+ taskId: string;
126
+ initialActivity: TaskActivityPayload;
127
+ }
128
+
129
+ const ACTIVE_TASK_STATUSES = new Set(["pending", "assigned", "in_progress", "review", "awaiting_approval"]);
130
+
131
+ export default function TaskActivityView({
132
+ projectId,
133
+ taskId,
134
+ initialActivity,
135
+ }: TaskActivityViewProps) {
136
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
137
+
138
+ const toggle = (rowId: string) => {
139
+ setExpanded((prev) => {
140
+ const next = new Set(prev);
141
+ if (next.has(rowId)) next.delete(rowId); else next.add(rowId);
142
+ return next;
143
+ });
144
+ };
145
+
146
+ const polpo = usePolpoClient(projectId);
147
+ const { data: activity = initialActivity, isFetching, refetch } = useQuery({
148
+ queryKey: ["task-activity", projectId, taskId],
149
+ queryFn: async () => {
150
+ // Server returns the SDK's TaskActivityPayload shape; dashboard
151
+ // augments with `blueprint` (set only via SSR pre-fetch in
152
+ // page.tsx) to render the recurring-mission banner. We preserve
153
+ // blueprint from initialData on refetch.
154
+ const fresh = await polpo.getTaskActivityFull(taskId);
155
+ return {
156
+ ...fresh,
157
+ blueprint: initialActivity.blueprint,
158
+ } as unknown as TaskActivityPayload;
159
+ },
160
+ initialData: initialActivity,
161
+ refetchInterval: (query) => {
162
+ const current = query.state.data;
163
+ const task = current?.task;
164
+ const run = current?.run;
165
+ return run?.status === "running" || Boolean(task && ACTIVE_TASK_STATUSES.has(task.status)) ? 3000 : false;
166
+ },
167
+ });
168
+
169
+ const task = activity.task;
170
+ const run = activity.run;
171
+ const sessionId = activity.sessionId;
172
+ const rows = parseEntries(activity.entries ?? []);
173
+ const blueprint = activity.blueprint;
174
+
175
+ return (
176
+ <div className="space-y-6">
177
+ <div className="flex justify-end">
178
+ <ManualRefreshButton onRefresh={() => refetch()} isRefreshing={isFetching} />
179
+ </div>
180
+
181
+ {blueprint && (
182
+ <div className="rounded-md border border-border bg-muted/40 px-4 py-3 text-xs">
183
+ <p className="font-medium text-foreground">Blueprint task</p>
184
+ <p className="mt-1 text-muted-foreground">
185
+ This is the template definition from mission <span className="font-mono">{blueprint.missionName}</span>.
186
+ Runtime activity appears here only after the mission creates a real task instance.
187
+ </p>
188
+ </div>
189
+ )}
190
+
191
+ {/* Run info */}
192
+ {run && (
193
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
194
+ <span>Agent: <span className="font-mono font-medium text-foreground">{run.agentName}</span></span>
195
+ <span>Status: <span className="font-medium">{run.status}</span></span>
196
+ {task?.result?.duration != null && (
197
+ <span>Duration: <span className="font-mono font-medium text-foreground">{formatDuration(task.result.duration)}</span></span>
198
+ )}
199
+ {run.activity.toolCalls > 0 && (
200
+ <span>Tools: <span className="font-mono font-medium text-foreground">{run.activity.toolCalls}</span></span>
201
+ )}
202
+ {activity.sessionResolution === "matched-log-session" && (
203
+ <span>Matched logs by start time</span>
204
+ )}
205
+ {isFetching && <span>Updating...</span>}
206
+ </div>
207
+ )}
208
+
209
+ {/* Transcript table */}
210
+ {rows.length > 0 ? (
211
+ <div className="border border-border overflow-hidden">
212
+ <table className="w-full text-xs">
213
+ <thead>
214
+ <tr className="border-b border-border bg-muted/40 text-muted-foreground">
215
+ <th className="px-3 py-2 text-left font-medium w-16">Type</th>
216
+ <th className="px-3 py-2 text-left font-medium w-28">Tool</th>
217
+ <th className="px-3 py-2 text-left font-medium w-20">Time</th>
218
+ <th className="px-3 py-2 text-left font-medium w-20">State</th>
219
+ <th className="px-3 py-2 text-left font-medium">Content</th>
220
+ </tr>
221
+ </thead>
222
+ <tbody className="font-mono">
223
+ {rows.map((row) => {
224
+ const isExpanded = expanded.has(row.id);
225
+
226
+ return (
227
+ <tr
228
+ key={row.id}
229
+ onClick={() => toggle(row.id)}
230
+ className={`border-b border-border last:border-0 cursor-pointer transition-colors hover:bg-muted/20 ${
231
+ row.type === "tool_use" || row.type === "tool_result" ? "bg-muted/10" : ""
232
+ }`}
233
+ >
234
+ <td className="px-3 py-2 align-top">
235
+ <span className={`inline-block rounded px-1.5 py-0.5 text-[10px] font-medium ${
236
+ row.type === "tool_use"
237
+ ? "bg-blue-500/10 text-blue-400"
238
+ : row.type === "tool_result"
239
+ ? row.isError ? "bg-red-500/10 text-red-400" : "bg-green-500/10 text-green-400"
240
+ : row.type === "assistant"
241
+ ? "bg-foreground/10 text-foreground"
242
+ : "bg-muted-foreground/10 text-muted-foreground"
243
+ }`}>
244
+ {row.type === "tool_use" ? "call" : row.type === "tool_result" ? "result" : row.type === "assistant" ? "agent" : "event"}
245
+ </span>
246
+ </td>
247
+ <td className="px-3 py-2 align-top text-muted-foreground">
248
+ {row.tool ?? "—"}
249
+ </td>
250
+ <td className="px-3 py-2 align-top text-muted-foreground/60">
251
+ {formatTime(row.ts)}
252
+ </td>
253
+ <td className="px-3 py-2 align-top">
254
+ {row.state ? (
255
+ <span className={`inline-flex items-center gap-1 text-[10px] ${
256
+ row.state === "completed" ? "text-green-500" : "text-red-400"
257
+ }`}>
258
+ <span className={`h-1.5 w-1.5 rounded-full ${
259
+ row.state === "completed" ? "bg-green-500" : "bg-red-500"
260
+ }`} />
261
+ {row.state}
262
+ </span>
263
+ ) : (
264
+ <span className="text-muted-foreground/30">—</span>
265
+ )}
266
+ </td>
267
+ <td className="px-3 py-2 align-top">
268
+ {isExpanded ? (
269
+ <div className="space-y-3 pb-2">
270
+ {row.args && (
271
+ <div>
272
+ <span className="text-[10px] text-muted-foreground uppercase tracking-wider">arguments</span>
273
+ <pre className="mt-1 whitespace-pre-wrap break-words rounded bg-muted/50 p-2 text-muted-foreground max-h-40 overflow-auto">{row.args}</pre>
274
+ </div>
275
+ )}
276
+ {row.content && (
277
+ <div>
278
+ <span className={`text-[10px] uppercase tracking-wider ${row.isError ? "text-red-400" : "text-muted-foreground"}`}>
279
+ {row.type === "tool_result" ? (row.isError ? "error" : "result") : "content"}
280
+ </span>
281
+ <pre className={`mt-1 whitespace-pre-wrap break-words rounded bg-muted/50 p-2 max-h-60 overflow-auto ${
282
+ row.isError ? "text-red-400" : "text-foreground"
283
+ }`}>{row.content}</pre>
284
+ </div>
285
+ )}
286
+ {!row.args && !row.content && (
287
+ <span className="text-muted-foreground/40 italic">No data</span>
288
+ )}
289
+ </div>
290
+ ) : (
291
+ <span className="text-muted-foreground truncate block max-w-md">
292
+ {row.args
293
+ ? row.args.substring(0, 80) + (row.args.length > 80 ? "…" : "")
294
+ : row.content
295
+ ? row.content.substring(0, 100) + (row.content.length > 100 ? "…" : "")
296
+ : <span className="text-muted-foreground/30 italic">—</span>
297
+ }
298
+ </span>
299
+ )}
300
+ </td>
301
+ </tr>
302
+ );
303
+ })}
304
+ </tbody>
305
+ </table>
306
+ </div>
307
+ ) : (
308
+ <div className="border border-border p-8 text-center text-sm text-muted-foreground">
309
+ {blueprint
310
+ ? "No runtime activity yet. Execute the mission to create a task instance."
311
+ : sessionId
312
+ ? "No activity recorded for this task."
313
+ : run?.activity?.toolCalls
314
+ ? "Tool calls were recorded, but no matching transcript session was found yet."
315
+ : "No activity recorded for this task yet."}
316
+ </div>
317
+ )}
318
+
319
+ {/* Outcomes */}
320
+ {task?.outcomes && task.outcomes.length > 0 && (
321
+ <div className="border border-border bg-card p-4">
322
+ <h3 className="text-xs font-medium text-muted-foreground mb-3">Outcomes</h3>
323
+ <div className="space-y-2">
324
+ {task.outcomes.map((o, i) => (
325
+ <div key={i} className="flex items-center gap-2">
326
+ <span className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">{o.type}</span>
327
+ <span className="text-sm font-medium">{o.label}</span>
328
+ {o.path && <span className="font-mono text-xs text-muted-foreground">{o.path}</span>}
329
+ </div>
330
+ ))}
331
+ </div>
332
+ </div>
333
+ )}
334
+ </div>
335
+ );
336
+ }
@@ -0,0 +1,113 @@
1
+ import type { Task } from "@polpo-ai/core";
2
+ import { ManualRefreshButton } from "#/components/dashboard/manual-refresh-button";
3
+ import type { BlueprintContext } from "../blueprint";
4
+
5
+ export default function TaskAssessmentView({
6
+ task,
7
+ blueprint,
8
+ }: {
9
+ task: Task | null;
10
+ blueprint?: BlueprintContext | null;
11
+ }) {
12
+ const a = task?.result?.assessment;
13
+
14
+ if (!a) {
15
+ return (
16
+ <div className="mt-4 space-y-4">
17
+ <div className="flex justify-end">
18
+ <ManualRefreshButton />
19
+ </div>
20
+ {blueprint && (
21
+ <div className="rounded-md border border-border bg-muted/40 px-4 py-3 text-xs">
22
+ <p className="font-medium text-foreground">Blueprint task</p>
23
+ <p className="mt-1 text-muted-foreground">
24
+ This task is a template from mission <span className="font-mono">{blueprint.missionName}</span>.
25
+ </p>
26
+ </div>
27
+ )}
28
+ <div className="border border-border p-8 text-center text-sm text-muted-foreground">
29
+ {blueprint
30
+ ? "No runtime assessment yet. Execute the mission to create a task instance."
31
+ : "No assessment yet. The task may not have been assessed."}
32
+ </div>
33
+ </div>
34
+ );
35
+ }
36
+
37
+ const passed = a.passed ?? (a.globalScore != null && a.globalScore >= 3.5);
38
+ const checks = a.checks ?? [];
39
+ const scores = a.scores ?? [];
40
+
41
+ return (
42
+ <div>
43
+ <div className="mb-4 flex justify-end">
44
+ <ManualRefreshButton />
45
+ </div>
46
+
47
+ {/* Summary */}
48
+ <div className="flex items-center justify-between">
49
+ <div className="flex items-center gap-3">
50
+ <span className={`h-3 w-3 rounded-full ${passed ? "bg-brand" : "bg-destructive"}`} />
51
+ <span className="text-sm font-semibold">{passed ? "Passed" : "Failed"}</span>
52
+ </div>
53
+ {a.globalScore != null && (
54
+ <span className="text-2xl font-extrabold font-mono">{a.globalScore.toFixed(1)}<span className="text-sm text-muted-foreground font-normal">/5</span></span>
55
+ )}
56
+ </div>
57
+
58
+ {/* Checks */}
59
+ {checks.length > 0 && (
60
+ <section className="mt-8">
61
+ <h3 className="text-sm font-semibold">Expectation checks</h3>
62
+ <div className="mt-3 border border-border overflow-hidden">
63
+ {checks.map((check: { type?: string; passed?: boolean; message?: string; details?: string }, i: number) => (
64
+ <div key={i} className="flex items-center justify-between border-b border-border last:border-0 px-4 py-3">
65
+ <div>
66
+ <div className="flex items-center gap-2">
67
+ <span className="font-mono text-xs font-medium">{check.type}</span>
68
+ <span className="text-xs text-muted-foreground">{check.message}</span>
69
+ </div>
70
+ {check.details && <p className="mt-0.5 text-[11px] text-muted-foreground/50">{check.details}</p>}
71
+ </div>
72
+ <span className={`h-2 w-2 rounded-full shrink-0 ${check.passed ? "bg-brand" : "bg-destructive"}`} />
73
+ </div>
74
+ ))}
75
+ </div>
76
+ </section>
77
+ )}
78
+
79
+ {/* Dimension scores */}
80
+ {scores.length > 0 && (
81
+ <section className="mt-8">
82
+ <h3 className="text-sm font-semibold">G-Eval dimensions</h3>
83
+ <div className="mt-3 space-y-3">
84
+ {scores.map((s: { dimension?: string; score?: number; weight?: number; reasoning?: string }, i: number) => (
85
+ <div key={s.dimension ?? i} className="border border-border bg-card p-4">
86
+ <div className="flex items-center justify-between">
87
+ <div className="flex items-center gap-2">
88
+ <span className="text-sm font-medium capitalize">{(s.dimension ?? "").replace("_", " ")}</span>
89
+ {s.weight != null && <span className="text-[10px] text-muted-foreground/40">weight: {s.weight}</span>}
90
+ </div>
91
+ {s.score != null && <span className="font-mono text-sm font-bold">{s.score}/5</span>}
92
+ </div>
93
+ {s.score != null && (
94
+ <div className="mt-2 h-1.5 w-full rounded-full bg-secondary overflow-hidden">
95
+ <div className="h-full bg-foreground/60 transition-all" style={{ width: `${(s.score / 5) * 100}%` }} />
96
+ </div>
97
+ )}
98
+ {s.reasoning && <p className="mt-3 text-xs text-muted-foreground leading-relaxed">{s.reasoning}</p>}
99
+ </div>
100
+ ))}
101
+ </div>
102
+ </section>
103
+ )}
104
+
105
+ {/* Timestamp */}
106
+ {a.timestamp && (
107
+ <p className="mt-6 text-[11px] text-muted-foreground/40">
108
+ Assessed at {new Date(a.timestamp).toLocaleString()}
109
+ </p>
110
+ )}
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,72 @@
1
+ import { dataApi } from "#/lib/api";
2
+ import type { Mission, Task } from "@polpo-ai/core";
3
+
4
+ export interface BlueprintContext {
5
+ missionName: string;
6
+ missionStatus?: string;
7
+ }
8
+
9
+ interface BlueprintTaskDoc {
10
+ title: string;
11
+ description?: string;
12
+ assignTo?: string;
13
+ dependsOn?: string[];
14
+ expectations?: unknown[];
15
+ expectedOutcomes?: unknown[];
16
+ sideEffects?: boolean;
17
+ }
18
+
19
+ export async function resolveBlueprint(
20
+ projectId: string,
21
+ taskId: string,
22
+ ): Promise<{ task: Task; mission: Mission; context: BlueprintContext } | null> {
23
+ try {
24
+ const res = await dataApi<{ ok: boolean; data: Mission[] }>(projectId, "/v1/missions");
25
+ const missions = res.data ?? [];
26
+
27
+ for (const mission of missions) {
28
+ if (!mission.data) continue;
29
+
30
+ let parsed: { tasks?: BlueprintTaskDoc[] };
31
+ try {
32
+ parsed = typeof mission.data === "string"
33
+ ? JSON.parse(mission.data)
34
+ : (mission.data as unknown as { tasks?: BlueprintTaskDoc[] });
35
+ } catch {
36
+ continue;
37
+ }
38
+
39
+ const blueprint = parsed.tasks?.find((t) => t.title === taskId);
40
+ if (!blueprint) continue;
41
+
42
+ const synthetic: Task = {
43
+ id: blueprint.title,
44
+ title: blueprint.title,
45
+ description: blueprint.description ?? "",
46
+ assignTo: blueprint.assignTo ?? "",
47
+ status: "draft",
48
+ expectations: (blueprint.expectations ?? []) as Task["expectations"],
49
+ expectedOutcomes: (blueprint.expectedOutcomes ?? []) as Task["expectedOutcomes"],
50
+ dependsOn: blueprint.dependsOn ?? [],
51
+ missionId: mission.id,
52
+ retries: 0,
53
+ maxRetries: 0,
54
+ sideEffects: blueprint.sideEffects,
55
+ createdAt: mission.createdAt ?? "",
56
+ updatedAt: mission.updatedAt ?? "",
57
+ metrics: [],
58
+ };
59
+
60
+ return {
61
+ task: synthetic,
62
+ mission,
63
+ context: {
64
+ missionName: mission.name,
65
+ missionStatus: mission.status,
66
+ },
67
+ };
68
+ }
69
+ } catch {}
70
+
71
+ return null;
72
+ }
@@ -0,0 +1,90 @@
1
+ import type { Task } from "@polpo-ai/core";
2
+ import { Markdown } from "#/components/dashboard/markdown";
3
+ import { ManualRefreshButton } from "#/components/dashboard/manual-refresh-button";
4
+ import type { BlueprintContext } from "../blueprint";
5
+
6
+ function formatDuration(ms: number) {
7
+ const s = Math.floor(ms / 1000);
8
+ if (s < 60) return `${s}s`;
9
+ return `${Math.floor(s / 60)}m ${s % 60}s`;
10
+ }
11
+
12
+ export default function TaskOutputView({
13
+ task,
14
+ blueprint,
15
+ }: {
16
+ task: Task | null;
17
+ blueprint?: BlueprintContext | null;
18
+ }) {
19
+ const result = task?.result;
20
+
21
+ if (!result) {
22
+ return (
23
+ <div className="mt-4 space-y-4">
24
+ <div className="flex justify-end">
25
+ <ManualRefreshButton />
26
+ </div>
27
+ {blueprint && (
28
+ <div className="rounded-md border border-border bg-muted/40 px-4 py-3 text-xs">
29
+ <p className="font-medium text-foreground">Blueprint task</p>
30
+ <p className="mt-1 text-muted-foreground">
31
+ This task is a template from mission <span className="font-mono">{blueprint.missionName}</span>.
32
+ </p>
33
+ </div>
34
+ )}
35
+ <div className="border border-border p-8 text-center text-sm text-muted-foreground">
36
+ {blueprint
37
+ ? "No runtime output yet. Execute the mission to create a task instance."
38
+ : "No output yet. The task may not have run."}
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <div>
46
+ <div className="mb-4 flex justify-end">
47
+ <ManualRefreshButton />
48
+ </div>
49
+
50
+ {/* Meta */}
51
+ <div className="flex items-center gap-6 text-xs text-muted-foreground mb-4">
52
+ {result.exitCode != null && (
53
+ <span>Exit code: <span className="font-mono font-medium text-foreground">{result.exitCode}</span></span>
54
+ )}
55
+ {result.duration != null && (
56
+ <span>Duration: <span className="font-mono font-medium text-foreground">{formatDuration(result.duration)}</span></span>
57
+ )}
58
+ </div>
59
+
60
+ {/* Stdout — rendered as markdown (agents typically emit md), same component used by skills.
61
+ Stderr below stays raw — it's machine output, not narrative. */}
62
+ {result.stdout && (
63
+ <section>
64
+ <h3 className="text-sm font-semibold">stdout</h3>
65
+ <div className="mt-3 border border-border bg-card p-5 overflow-x-auto">
66
+ <Markdown content={result.stdout} />
67
+ </div>
68
+ </section>
69
+ )}
70
+
71
+ {/* Stderr */}
72
+ {result.stderr && (
73
+ <section className="mt-8">
74
+ <h3 className="text-sm font-semibold text-destructive">stderr</h3>
75
+ <div className="mt-3 border border-destructive/20 bg-card p-5 overflow-x-auto">
76
+ <pre className="font-mono text-xs text-destructive/80 whitespace-pre-wrap">
77
+ {result.stderr}
78
+ </pre>
79
+ </div>
80
+ </section>
81
+ )}
82
+
83
+ {!result.stdout && !result.stderr && (
84
+ <div className="mt-4 border border-border p-8 text-center text-sm text-muted-foreground">
85
+ Task completed with no stdout/stderr output.
86
+ </div>
87
+ )}
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,63 @@
1
+ import { Suspense } from "react";
2
+ import { dataApi } from "#/lib/api";
3
+ import type { Task } from "@polpo-ai/core";
4
+ import { TaskDetailSkeleton } from "#/components/dashboard/skeletons";
5
+ import { resolveBlueprint, type BlueprintContext } from "./blueprint";
6
+ import type { TaskActivityPayload } from "./activity/view";
7
+ import { TaskStudio, type TaskStudioInitialData } from "#/components/dashboard/task-studio";
8
+
9
+ /**
10
+ * Task detail = the Task Studio (fast client surface, like the Agent
11
+ * Studio). One server fetch (the activity payload already carries the full
12
+ * task) hydrates a client component with client-switched tabs; pure dogfood
13
+ * from there via the published SDK.
14
+ */
15
+ async function TaskData({ id, taskId }: { id: string; taskId: string }) {
16
+ let task: Task | null = null;
17
+ let activity: TaskActivityPayload = {
18
+ task: null,
19
+ run: null,
20
+ sessionId: null,
21
+ sessionResolution: "missing",
22
+ entries: [],
23
+ };
24
+ try {
25
+ // The activity payload already contains the full task — one call.
26
+ const res = await dataApi<{ ok: boolean; data: TaskActivityPayload }>(
27
+ id,
28
+ `/v1/tasks/${taskId}/activity`,
29
+ );
30
+ activity = res.data ?? activity;
31
+ task = activity.task;
32
+ } catch {
33
+ /* falls through to blueprint resolution below */
34
+ }
35
+
36
+ // Draft/blueprint tasks aren't persisted yet — synthesise from the mission.
37
+ let blueprint: BlueprintContext | null = null;
38
+ if (!task) {
39
+ const bp = await resolveBlueprint(id, taskId);
40
+ if (bp) {
41
+ task = bp.task;
42
+ blueprint = bp.context;
43
+ activity = { ...activity, task: bp.task, blueprint: bp.context };
44
+ }
45
+ }
46
+
47
+ const initialData: TaskStudioInitialData = { task, activity, blueprint };
48
+ return <TaskStudio projectId={id} taskId={taskId} initialData={initialData} />;
49
+ }
50
+
51
+ export default async function TaskDetailPage({
52
+ params,
53
+ }: {
54
+ params: Promise<{ id: string; taskId: string }>;
55
+ }) {
56
+ const { id, taskId } = await params;
57
+
58
+ return (
59
+ <Suspense fallback={<TaskDetailSkeleton />}>
60
+ <TaskData id={id} taskId={taskId} />
61
+ </Suspense>
62
+ );
63
+ }