stagent 0.5.0 → 0.6.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 (252) hide show
  1. package/README.md +8 -8
  2. package/dist/cli.js +146 -2
  3. package/docs/.coverage-gaps.json +21 -0
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +36 -14
  6. package/docs/features/chat.md +33 -56
  7. package/docs/features/cost-usage.md +14 -10
  8. package/docs/features/dashboard-kanban.md +30 -13
  9. package/docs/features/delivery-channels.md +198 -0
  10. package/docs/features/design-system.md +10 -10
  11. package/docs/features/documents.md +8 -8
  12. package/docs/features/home-workspace.md +20 -15
  13. package/docs/features/inbox-notifications.md +22 -10
  14. package/docs/features/keyboard-navigation.md +11 -11
  15. package/docs/features/monitoring.md +1 -1
  16. package/docs/features/playbook.md +30 -32
  17. package/docs/features/profiles.md +33 -11
  18. package/docs/features/projects.md +2 -2
  19. package/docs/features/provider-runtimes.md +58 -14
  20. package/docs/features/schedules.md +70 -40
  21. package/docs/features/settings.md +74 -46
  22. package/docs/features/shared-components.md +7 -15
  23. package/docs/features/tool-permissions.md +9 -9
  24. package/docs/features/workflows.md +32 -21
  25. package/docs/getting-started.md +33 -9
  26. package/docs/index.md +25 -16
  27. package/docs/journeys/developer.md +124 -207
  28. package/docs/journeys/personal-use.md +70 -79
  29. package/docs/journeys/power-user.md +107 -151
  30. package/docs/journeys/work-use.md +81 -113
  31. package/docs/manifest.json +77 -45
  32. package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
  33. package/docs/use-cases/agency-operator.md +84 -0
  34. package/docs/use-cases/solo-founder.md +75 -0
  35. package/docs/why-stagent.md +59 -0
  36. package/package.json +10 -3
  37. package/src/app/api/channels/[id]/route.ts +103 -0
  38. package/src/app/api/channels/[id]/test/route.ts +52 -0
  39. package/src/app/api/channels/inbound/slack/route.ts +109 -0
  40. package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
  41. package/src/app/api/channels/inbound/telegram/route.ts +76 -0
  42. package/src/app/api/channels/route.ts +71 -0
  43. package/src/app/api/chat/conversations/route.ts +15 -0
  44. package/src/app/api/chat/entities/search/route.ts +46 -31
  45. package/src/app/api/environment/profiles/suggest/route.ts +19 -3
  46. package/src/app/api/environment/scan/route.ts +8 -1
  47. package/src/app/api/handoffs/[id]/route.ts +76 -0
  48. package/src/app/api/handoffs/route.ts +89 -0
  49. package/src/app/api/memory/route.ts +181 -0
  50. package/src/app/api/profiles/[id]/route.ts +16 -1
  51. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  52. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  53. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  54. package/src/app/api/profiles/assist/route.ts +35 -0
  55. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  56. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  57. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  58. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  59. package/src/app/api/profiles/import-repo/route.ts +29 -0
  60. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  61. package/src/app/api/profiles/route.ts +73 -22
  62. package/src/app/api/runtimes/ollama/route.ts +86 -0
  63. package/src/app/api/runtimes/suggest/route.ts +29 -0
  64. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  65. package/src/app/api/schedules/[id]/route.ts +41 -3
  66. package/src/app/api/schedules/parse/route.ts +66 -0
  67. package/src/app/api/schedules/route.ts +71 -12
  68. package/src/app/api/settings/author-default/route.ts +7 -0
  69. package/src/app/api/settings/learning/route.ts +41 -0
  70. package/src/app/api/settings/ollama/route.ts +34 -0
  71. package/src/app/api/settings/providers/route.ts +57 -0
  72. package/src/app/api/settings/routing/route.ts +24 -0
  73. package/src/app/api/settings/web-search/route.ts +28 -0
  74. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  75. package/src/app/documents/page.tsx +3 -0
  76. package/src/app/environment/page.tsx +8 -1
  77. package/src/app/settings/page.tsx +10 -4
  78. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  79. package/src/app/workflows/new/page.tsx +2 -0
  80. package/src/components/chat/chat-command-popover.tsx +22 -19
  81. package/src/components/chat/chat-input.tsx +5 -0
  82. package/src/components/chat/chat-model-selector.tsx +42 -1
  83. package/src/components/chat/chat-shell.tsx +2 -0
  84. package/src/components/dashboard/welcome-landing.tsx +9 -9
  85. package/src/components/environment/artifact-card.tsx +27 -1
  86. package/src/components/environment/environment-dashboard.tsx +50 -2
  87. package/src/components/environment/environment-summary-card.tsx +5 -2
  88. package/src/components/environment/suggested-profiles.tsx +117 -52
  89. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  90. package/src/components/memory/memory-browser.tsx +315 -0
  91. package/src/components/profiles/learned-context-panel.tsx +4 -4
  92. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  93. package/src/components/profiles/profile-browser.tsx +109 -8
  94. package/src/components/profiles/profile-card.tsx +29 -1
  95. package/src/components/profiles/profile-detail-view.tsx +200 -28
  96. package/src/components/profiles/profile-form-view.tsx +220 -82
  97. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  98. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  99. package/src/components/schedules/schedule-create-sheet.tsx +9 -1
  100. package/src/components/schedules/schedule-form.tsx +348 -9
  101. package/src/components/schedules/schedule-list.tsx +15 -2
  102. package/src/components/settings/auth-method-selector.tsx +7 -1
  103. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  104. package/src/components/settings/channels-section.tsx +526 -0
  105. package/src/components/settings/chat-settings-section.tsx +27 -1
  106. package/src/components/settings/data-management-section.tsx +8 -6
  107. package/src/components/settings/learning-context-section.tsx +124 -0
  108. package/src/components/settings/ollama-section.tsx +270 -0
  109. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  110. package/src/components/settings/web-search-section.tsx +101 -0
  111. package/src/components/shared/tag-input.tsx +156 -0
  112. package/src/components/tasks/kanban-board.tsx +32 -0
  113. package/src/components/tasks/kanban-column.tsx +4 -2
  114. package/src/components/tasks/task-card.tsx +1 -0
  115. package/src/components/tasks/task-chip-bar.tsx +6 -1
  116. package/src/components/tasks/task-create-panel.tsx +55 -5
  117. package/src/components/workflows/workflow-form-view.tsx +38 -3
  118. package/src/hooks/use-chat-autocomplete.ts +24 -26
  119. package/src/hooks/use-project-skills.ts +66 -0
  120. package/src/hooks/use-tag-suggestions.ts +31 -0
  121. package/src/instrumentation.ts +4 -1
  122. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  123. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  124. package/src/lib/agents/agentic-loop.ts +235 -0
  125. package/src/lib/agents/browser-mcp.ts +59 -4
  126. package/src/lib/agents/claude-agent.ts +26 -199
  127. package/src/lib/agents/handoff/bus.ts +164 -0
  128. package/src/lib/agents/handoff/governance.ts +47 -0
  129. package/src/lib/agents/handoff/types.ts +16 -0
  130. package/src/lib/agents/learned-context.ts +27 -7
  131. package/src/lib/agents/memory/decay.ts +61 -0
  132. package/src/lib/agents/memory/extractor.ts +181 -0
  133. package/src/lib/agents/memory/retrieval.ts +96 -0
  134. package/src/lib/agents/memory/types.ts +6 -0
  135. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  136. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  137. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  138. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  139. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  140. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  141. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  142. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  143. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  144. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  145. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  146. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  147. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  148. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  149. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  150. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  151. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  152. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  153. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  154. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  155. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  156. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  157. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  158. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  159. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  160. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  162. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  163. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  164. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  165. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  166. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  168. package/src/lib/agents/profiles/registry.ts +130 -6
  169. package/src/lib/agents/profiles/types.ts +28 -0
  170. package/src/lib/agents/router.ts +174 -2
  171. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  172. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  173. package/src/lib/agents/runtime/catalog.ts +57 -2
  174. package/src/lib/agents/runtime/claude.ts +205 -1
  175. package/src/lib/agents/runtime/index.ts +22 -0
  176. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  177. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  178. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  179. package/src/lib/agents/runtime/types.ts +2 -0
  180. package/src/lib/agents/tool-permissions.ts +203 -0
  181. package/src/lib/channels/gateway.ts +321 -0
  182. package/src/lib/channels/poller.ts +268 -0
  183. package/src/lib/channels/registry.ts +90 -0
  184. package/src/lib/channels/slack-adapter.ts +188 -0
  185. package/src/lib/channels/telegram-adapter.ts +218 -0
  186. package/src/lib/channels/types.ts +43 -0
  187. package/src/lib/channels/webhook-adapter.ts +74 -0
  188. package/src/lib/chat/context-builder.ts +22 -2
  189. package/src/lib/chat/engine.ts +95 -13
  190. package/src/lib/chat/ollama-engine.ts +198 -0
  191. package/src/lib/chat/stagent-tools.ts +106 -20
  192. package/src/lib/chat/tool-catalog.ts +24 -0
  193. package/src/lib/chat/tool-registry.ts +90 -0
  194. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  195. package/src/lib/chat/tools/document-tools.ts +7 -7
  196. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  197. package/src/lib/chat/tools/notification-tools.ts +4 -4
  198. package/src/lib/chat/tools/profile-tools.ts +3 -3
  199. package/src/lib/chat/tools/project-tools.ts +3 -3
  200. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  201. package/src/lib/chat/tools/settings-tools.ts +2 -2
  202. package/src/lib/chat/tools/task-tools.ts +66 -11
  203. package/src/lib/chat/tools/usage-tools.ts +2 -2
  204. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  205. package/src/lib/chat/types.ts +11 -5
  206. package/src/lib/constants/known-tools.ts +19 -0
  207. package/src/lib/constants/prose-styles.ts +1 -1
  208. package/src/lib/constants/settings.ts +7 -0
  209. package/src/lib/data/channel-bindings.ts +85 -0
  210. package/src/lib/data/clear.ts +22 -0
  211. package/src/lib/data/profile-test-results.ts +48 -0
  212. package/src/lib/data/seed-data/conversations.ts +196 -0
  213. package/src/lib/data/seed-data/learned-context.ts +99 -0
  214. package/src/lib/data/seed-data/notifications.ts +54 -1
  215. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  216. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  217. package/src/lib/data/seed-data/views.ts +60 -0
  218. package/src/lib/data/seed.ts +51 -0
  219. package/src/lib/db/bootstrap.ts +162 -0
  220. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  221. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  222. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  223. package/src/lib/db/schema.ts +187 -1
  224. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  225. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  226. package/src/lib/environment/auto-scan.ts +48 -0
  227. package/src/lib/environment/data.ts +25 -0
  228. package/src/lib/environment/profile-generator.ts +40 -10
  229. package/src/lib/environment/profile-linker.ts +143 -0
  230. package/src/lib/environment/profile-rules.ts +96 -0
  231. package/src/lib/import/dedup.ts +149 -0
  232. package/src/lib/import/format-adapter.ts +631 -0
  233. package/src/lib/import/github-api.ts +219 -0
  234. package/src/lib/import/repo-scanner.ts +251 -0
  235. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  236. package/src/lib/schedules/active-hours.ts +120 -0
  237. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  238. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  239. package/src/lib/schedules/nlp-parser.ts +357 -0
  240. package/src/lib/schedules/scheduler.ts +218 -3
  241. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  242. package/src/lib/settings/helpers.ts +6 -0
  243. package/src/lib/settings/routing.ts +24 -0
  244. package/src/lib/settings/runtime-setup.ts +28 -1
  245. package/src/lib/usage/ledger.ts +2 -1
  246. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  247. package/src/lib/validators/profile.ts +39 -0
  248. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  249. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  250. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  251. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  252. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -0,0 +1,512 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Textarea } from "@/components/ui/textarea";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from "@/components/ui/select";
14
+ import {
15
+ Sparkles,
16
+ Check,
17
+ X,
18
+ Info,
19
+ ChevronDown,
20
+ ChevronUp,
21
+ } from "lucide-react";
22
+ import type { ProfileAssistResponse } from "@/lib/agents/runtime/profile-assist-types";
23
+ import type { SmokeTestDraft } from "./smoke-test-editor";
24
+
25
+ export interface ProfileAssistResult {
26
+ name: string;
27
+ description: string;
28
+ domain: "work" | "personal";
29
+ tags: string[];
30
+ skillMd: string;
31
+ allowedTools: string[];
32
+ canUseToolPolicy: { autoApprove: string[]; autoDeny: string[] };
33
+ maxTurns: number;
34
+ outputFormat: string;
35
+ supportedRuntimes: string[];
36
+ tests: SmokeTestDraft[];
37
+ reasoning: string;
38
+ }
39
+
40
+ interface ProfileAssistPanelProps {
41
+ onApplyAll: (result: ProfileAssistResult) => void;
42
+ onApplyField: (field: keyof ProfileAssistResult, value: unknown) => void;
43
+ /** Current mode — enables refine/suggest-tests on edit */
44
+ isEdit?: boolean;
45
+ existingSkillMd?: string;
46
+ existingTags?: string[];
47
+ }
48
+
49
+ const ACTIVITY_MESSAGES = [
50
+ "Analyzing your goal...",
51
+ "Designing agent capabilities...",
52
+ "Generating SKILL.md...",
53
+ "Creating smoke tests...",
54
+ "Finalizing profile...",
55
+ ];
56
+
57
+ const EXAMPLE_PROMPTS = [
58
+ "Security-focused code reviewer",
59
+ "Research agent that cites sources",
60
+ "Technical documentation writer",
61
+ "Personal fitness coach",
62
+ ];
63
+
64
+ function ProgressBar({ loading }: { loading: boolean }) {
65
+ const [messageIndex, setMessageIndex] = useState(0);
66
+
67
+ useEffect(() => {
68
+ if (!loading) {
69
+ setMessageIndex(0);
70
+ return;
71
+ }
72
+ const interval = setInterval(() => {
73
+ setMessageIndex((prev) =>
74
+ prev < ACTIVITY_MESSAGES.length - 1 ? prev + 1 : prev
75
+ );
76
+ }, 3000);
77
+ return () => clearInterval(interval);
78
+ }, [loading]);
79
+
80
+ if (!loading) return null;
81
+
82
+ return (
83
+ <div className="space-y-1.5">
84
+ <div className="h-1.5 w-full rounded-full bg-muted overflow-hidden">
85
+ <div className="h-full w-full rounded-full bg-primary animate-[progress-slide_1.5s_ease-in-out_infinite]" />
86
+ </div>
87
+ <p className="text-xs text-muted-foreground text-center">
88
+ {ACTIVITY_MESSAGES[messageIndex]}
89
+ </p>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ function toAssistResult(raw: ProfileAssistResponse): ProfileAssistResult {
95
+ return {
96
+ name: raw.name ?? "",
97
+ description: raw.description ?? "",
98
+ domain: raw.domain ?? "work",
99
+ tags: raw.tags ?? [],
100
+ skillMd: raw.skillMd ?? "",
101
+ allowedTools: raw.allowedTools ?? [],
102
+ canUseToolPolicy: raw.canUseToolPolicy ?? { autoApprove: [], autoDeny: [] },
103
+ maxTurns: raw.maxTurns ?? 30,
104
+ outputFormat: raw.outputFormat ?? "",
105
+ supportedRuntimes: raw.supportedRuntimes ?? ["claude-code"],
106
+ tests: (raw.tests ?? []).map((t) => ({
107
+ task: t.task,
108
+ expectedKeywords: t.expectedKeywords.join(", "),
109
+ })),
110
+ reasoning: raw.reasoning ?? "",
111
+ };
112
+ }
113
+
114
+ export function ProfileAssistPanel({
115
+ onApplyAll,
116
+ onApplyField,
117
+ isEdit = false,
118
+ existingSkillMd,
119
+ existingTags,
120
+ }: ProfileAssistPanelProps) {
121
+ const [goal, setGoal] = useState("");
122
+ const [domain, setDomain] = useState<"work" | "personal" | "auto">("auto");
123
+ const [loading, setLoading] = useState(false);
124
+ const [error, setError] = useState<string | null>(null);
125
+ const [result, setResult] = useState<ProfileAssistResult | null>(null);
126
+ const [expanded, setExpanded] = useState(true);
127
+ const [appliedSections, setAppliedSections] = useState<Set<string>>(new Set());
128
+ const [allApplied, setAllApplied] = useState(false);
129
+ const [skillMdExpanded, setSkillMdExpanded] = useState(false);
130
+
131
+ async function generate(mode: "generate" | "refine-skillmd" | "suggest-tests" = "generate") {
132
+ if (mode === "generate" && !goal.trim()) return;
133
+ setLoading(true);
134
+ setError(null);
135
+ setResult(null);
136
+ setAppliedSections(new Set());
137
+ setAllApplied(false);
138
+
139
+ try {
140
+ const res = await fetch("/api/profiles/assist", {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({
144
+ goal: goal.trim() || "Improve the existing profile",
145
+ domain: domain === "auto" ? undefined : domain,
146
+ mode,
147
+ existingSkillMd: mode !== "generate" ? existingSkillMd : undefined,
148
+ existingTags: mode !== "generate" ? existingTags : undefined,
149
+ }),
150
+ });
151
+
152
+ if (!res.ok) {
153
+ const data = await res.json().catch(() => null);
154
+ setError(data?.error ?? "AI assist failed");
155
+ return;
156
+ }
157
+
158
+ const data = await res.json();
159
+ setResult(toAssistResult(data));
160
+ setExpanded(true);
161
+ } catch {
162
+ setError("Network error");
163
+ } finally {
164
+ setLoading(false);
165
+ }
166
+ }
167
+
168
+ function applySection(section: string) {
169
+ if (!result) return;
170
+ setAppliedSections((prev) => new Set([...prev, section]));
171
+
172
+ switch (section) {
173
+ case "identity":
174
+ onApplyField("name", result.name);
175
+ onApplyField("description", result.description);
176
+ onApplyField("domain", result.domain);
177
+ onApplyField("tags", result.tags);
178
+ break;
179
+ case "config":
180
+ onApplyField("maxTurns", result.maxTurns);
181
+ onApplyField("outputFormat", result.outputFormat);
182
+ onApplyField("allowedTools", result.allowedTools);
183
+ onApplyField("supportedRuntimes", result.supportedRuntimes);
184
+ break;
185
+ case "policy":
186
+ onApplyField("canUseToolPolicy", result.canUseToolPolicy);
187
+ break;
188
+ case "skillmd":
189
+ onApplyField("skillMd", result.skillMd);
190
+ break;
191
+ case "tests":
192
+ onApplyField("tests", result.tests);
193
+ break;
194
+ }
195
+ }
196
+
197
+ function handleApplyAll() {
198
+ if (!result) return;
199
+ onApplyAll(result);
200
+ setAllApplied(true);
201
+ setAppliedSections(new Set(["identity", "config", "policy", "skillmd", "tests"]));
202
+ }
203
+
204
+ return (
205
+ <div className="surface-card-muted rounded-lg border border-primary/20 p-4 space-y-3">
206
+ {/* Header */}
207
+ <div className="flex items-center gap-2">
208
+ <Sparkles className="h-4 w-4 text-primary" />
209
+ <span className="text-sm font-medium">AI Assist</span>
210
+ {!result && (
211
+ <span className="text-xs text-muted-foreground">
212
+ Describe your agent and AI will generate the full profile
213
+ </span>
214
+ )}
215
+ {result && allApplied && (
216
+ <Badge variant="default" className="text-xs">
217
+ <Check className="h-3 w-3 mr-0.5" /> Applied
218
+ </Badge>
219
+ )}
220
+ </div>
221
+
222
+ {/* Goal input — always visible so user can edit & regenerate */}
223
+ <Textarea
224
+ value={goal}
225
+ onChange={(e) => setGoal(e.target.value)}
226
+ placeholder="I want an agent that..."
227
+ rows={2}
228
+ className="text-sm"
229
+ />
230
+
231
+ {/* Example prompts for first-time users */}
232
+ {!goal && !isEdit && !result && (
233
+ <div className="flex flex-wrap gap-1.5">
234
+ {EXAMPLE_PROMPTS.map((prompt) => (
235
+ <button
236
+ key={prompt}
237
+ type="button"
238
+ className="rounded-full border border-border/60 px-2.5 py-1 text-xs text-muted-foreground hover:bg-accent/50 hover:text-foreground transition-colors"
239
+ onClick={() => setGoal(prompt)}
240
+ >
241
+ {prompt}
242
+ </button>
243
+ ))}
244
+ </div>
245
+ )}
246
+
247
+ {/* Action buttons */}
248
+ <div className="flex items-center gap-2">
249
+ <Select
250
+ value={domain}
251
+ onValueChange={(v) => setDomain(v as "work" | "personal" | "auto")}
252
+ >
253
+ <SelectTrigger className="w-32 h-8 text-xs">
254
+ <SelectValue />
255
+ </SelectTrigger>
256
+ <SelectContent>
257
+ <SelectItem value="auto">Auto-detect</SelectItem>
258
+ <SelectItem value="work">Work</SelectItem>
259
+ <SelectItem value="personal">Personal</SelectItem>
260
+ </SelectContent>
261
+ </Select>
262
+
263
+ <Button
264
+ type="button"
265
+ size="sm"
266
+ onClick={() => generate("generate")}
267
+ disabled={loading || !goal.trim()}
268
+ >
269
+ <Sparkles className="h-3 w-3 mr-1" />
270
+ {result ? "Regenerate" : "Generate Profile"}
271
+ </Button>
272
+
273
+ {isEdit && (
274
+ <>
275
+ <Button
276
+ type="button"
277
+ variant="outline"
278
+ size="sm"
279
+ onClick={() => generate("refine-skillmd")}
280
+ disabled={loading}
281
+ >
282
+ Refine SKILL.md
283
+ </Button>
284
+ <Button
285
+ type="button"
286
+ variant="outline"
287
+ size="sm"
288
+ onClick={() => generate("suggest-tests")}
289
+ disabled={loading}
290
+ >
291
+ Suggest Tests
292
+ </Button>
293
+ </>
294
+ )}
295
+ </div>
296
+
297
+ <ProgressBar loading={loading} />
298
+ {error && <p className="text-xs text-destructive">{error}</p>}
299
+
300
+ {/* Results — collapsible */}
301
+ {result && (
302
+ <>
303
+ <div className="flex items-center justify-between border-t border-border/40 pt-3">
304
+ <span className="text-xs font-medium text-muted-foreground">Generated Profile</span>
305
+ <Button
306
+ type="button"
307
+ variant="ghost"
308
+ size="sm"
309
+ className="h-6 px-2"
310
+ onClick={() => setExpanded(!expanded)}
311
+ >
312
+ {expanded ? (
313
+ <ChevronUp className="h-3 w-3" />
314
+ ) : (
315
+ <ChevronDown className="h-3 w-3" />
316
+ )}
317
+ </Button>
318
+ </div>
319
+
320
+ {expanded && (
321
+ <div className="space-y-3">
322
+ {/* Reasoning */}
323
+ {result.reasoning && (
324
+ <div className="rounded-md border border-primary/20 bg-primary/5 p-3">
325
+ <div className="flex gap-2">
326
+ <Info className="h-4 w-4 mt-0.5 shrink-0 text-primary" />
327
+ <p className="text-xs text-muted-foreground">{result.reasoning}</p>
328
+ </div>
329
+ </div>
330
+ )}
331
+
332
+ {/* Two-column layout for compact result sections */}
333
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
334
+ {/* Identity section */}
335
+ <SectionCard
336
+ title="Identity"
337
+ applied={appliedSections.has("identity")}
338
+ onApply={() => applySection("identity")}
339
+ >
340
+ <div className="text-sm space-y-1">
341
+ <p><span className="text-muted-foreground">Name:</span> {result.name}</p>
342
+ <p><span className="text-muted-foreground">Domain:</span> {result.domain}</p>
343
+ <div className="flex items-center gap-1 flex-wrap">
344
+ <span className="text-muted-foreground">Tags:</span>
345
+ {result.tags.map((tag) => (
346
+ <Badge key={tag} variant="outline" className="text-xs">{tag}</Badge>
347
+ ))}
348
+ </div>
349
+ </div>
350
+ </SectionCard>
351
+
352
+ {/* Configuration section */}
353
+ <SectionCard
354
+ title="Configuration"
355
+ applied={appliedSections.has("config")}
356
+ onApply={() => applySection("config")}
357
+ >
358
+ <div className="text-sm space-y-1">
359
+ <p><span className="text-muted-foreground">Max Turns:</span> {result.maxTurns}</p>
360
+ <p><span className="text-muted-foreground">Output:</span> {result.outputFormat || "default"}</p>
361
+ <div className="flex items-center gap-1 flex-wrap">
362
+ <span className="text-muted-foreground">Tools:</span>
363
+ {result.allowedTools.length > 0 ? (
364
+ result.allowedTools.map((tool) => (
365
+ <Badge key={tool} variant="outline" className="text-xs">{tool}</Badge>
366
+ ))
367
+ ) : (
368
+ <span className="text-xs italic">unrestricted</span>
369
+ )}
370
+ </div>
371
+ </div>
372
+ </SectionCard>
373
+
374
+ {/* Policy section — only show if AI suggested non-empty policies */}
375
+ {(result.canUseToolPolicy.autoApprove.length > 0 ||
376
+ result.canUseToolPolicy.autoDeny.length > 0) && (
377
+ <SectionCard
378
+ title="Tool Policies"
379
+ applied={appliedSections.has("policy")}
380
+ onApply={() => applySection("policy")}
381
+ >
382
+ <div className="text-sm space-y-1">
383
+ {result.canUseToolPolicy.autoApprove.length > 0 && (
384
+ <div className="flex items-center gap-1 flex-wrap">
385
+ <span className="text-muted-foreground">Auto-approve:</span>
386
+ {result.canUseToolPolicy.autoApprove.map((tool) => (
387
+ <Badge key={tool} variant="outline" className="text-xs text-green-600">{tool}</Badge>
388
+ ))}
389
+ </div>
390
+ )}
391
+ {result.canUseToolPolicy.autoDeny.length > 0 && (
392
+ <div className="flex items-center gap-1 flex-wrap">
393
+ <span className="text-muted-foreground">Auto-deny:</span>
394
+ {result.canUseToolPolicy.autoDeny.map((tool) => (
395
+ <Badge key={tool} variant="outline" className="text-xs text-red-600">{tool}</Badge>
396
+ ))}
397
+ </div>
398
+ )}
399
+ </div>
400
+ </SectionCard>
401
+ )}
402
+ </div>
403
+
404
+ {/* SKILL.md section — full width */}
405
+ <SectionCard
406
+ title="SKILL.md"
407
+ applied={appliedSections.has("skillmd")}
408
+ onApply={() => applySection("skillmd")}
409
+ >
410
+ <div>
411
+ <button
412
+ type="button"
413
+ className="text-xs text-primary hover:underline"
414
+ onClick={() => setSkillMdExpanded(!skillMdExpanded)}
415
+ >
416
+ {skillMdExpanded ? "Collapse" : "Preview"} ({result.skillMd.split("\n").length} lines)
417
+ </button>
418
+ {skillMdExpanded && (
419
+ <pre className="mt-2 max-h-48 overflow-auto rounded-md bg-muted/50 p-3 text-xs font-mono whitespace-pre-wrap">
420
+ {result.skillMd}
421
+ </pre>
422
+ )}
423
+ </div>
424
+ </SectionCard>
425
+
426
+ {/* Tests section — full width */}
427
+ {result.tests.length > 0 && (
428
+ <SectionCard
429
+ title={`Smoke Tests (${result.tests.length})`}
430
+ applied={appliedSections.has("tests")}
431
+ onApply={() => applySection("tests")}
432
+ >
433
+ <div className="space-y-1.5">
434
+ {result.tests.map((test, i) => (
435
+ <div key={i} className="text-xs">
436
+ <p className="font-medium">{i + 1}. {test.task}</p>
437
+ <p className="text-muted-foreground ml-3">
438
+ Keywords: {test.expectedKeywords}
439
+ </p>
440
+ </div>
441
+ ))}
442
+ </div>
443
+ </SectionCard>
444
+ )}
445
+
446
+ {/* Action buttons */}
447
+ <div className="flex gap-2">
448
+ <Button
449
+ type="button"
450
+ size="sm"
451
+ onClick={handleApplyAll}
452
+ disabled={allApplied}
453
+ >
454
+ {allApplied ? (
455
+ <><Check className="h-3 w-3 mr-1" /> All Applied</>
456
+ ) : (
457
+ <><Sparkles className="h-3 w-3 mr-1" /> Apply All</>
458
+ )}
459
+ </Button>
460
+ <Button
461
+ type="button"
462
+ variant="ghost"
463
+ size="sm"
464
+ onClick={() => setResult(null)}
465
+ className="text-muted-foreground"
466
+ >
467
+ <X className="h-3 w-3 mr-1" /> Dismiss
468
+ </Button>
469
+ </div>
470
+ </div>
471
+ )}
472
+ </>
473
+ )}
474
+ </div>
475
+ );
476
+ }
477
+
478
+ /** Small card for each result section with Apply button */
479
+ function SectionCard({
480
+ title,
481
+ applied,
482
+ onApply,
483
+ children,
484
+ }: {
485
+ title: string;
486
+ applied: boolean;
487
+ onApply: () => void;
488
+ children: React.ReactNode;
489
+ }) {
490
+ return (
491
+ <div className="rounded-md border border-border/60 p-3 space-y-2">
492
+ <div className="flex items-center justify-between">
493
+ <span className="text-xs font-medium text-muted-foreground">{title}</span>
494
+ <Button
495
+ type="button"
496
+ variant="ghost"
497
+ size="sm"
498
+ className="h-6 text-xs"
499
+ onClick={onApply}
500
+ disabled={applied}
501
+ >
502
+ {applied ? (
503
+ <><Check className="h-3 w-3 mr-1" /> Applied</>
504
+ ) : (
505
+ "Apply"
506
+ )}
507
+ </Button>
508
+ </div>
509
+ {children}
510
+ </div>
511
+ );
512
+ }
@@ -1,14 +1,21 @@
1
1
  "use client";
2
2
 
3
- import { useState, useMemo } from "react";
3
+ import { useState, useMemo, useCallback } from "react";
4
4
  import { useRouter } from "next/navigation";
5
- import { Plus, Search, Bot, Download } from "lucide-react";
5
+ import { Plus, Search, Bot, Download, Copy, Package, ChevronDown } from "lucide-react";
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Input } from "@/components/ui/input";
8
8
  import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
9
+ import {
10
+ DropdownMenu,
11
+ DropdownMenuContent,
12
+ DropdownMenuItem,
13
+ DropdownMenuTrigger,
14
+ } from "@/components/ui/dropdown-menu";
9
15
  import { EmptyState } from "@/components/shared/empty-state";
10
16
  import { ProfileCard } from "@/components/profiles/profile-card";
11
17
  import { ProfileImportDialog } from "@/components/profiles/profile-import-dialog";
18
+ import { RepoImportWizard } from "@/components/profiles/repo-import-wizard";
12
19
  import type { AgentProfile } from "@/lib/agents/profiles/types";
13
20
 
14
21
  interface ProfileWithBuiltin extends AgentProfile {
@@ -27,11 +34,38 @@ export function ProfileBrowser({ initialProfiles }: ProfileBrowserProps) {
27
34
  "all" | "work" | "personal"
28
35
  >("all");
29
36
  const [showImport, setShowImport] = useState(false);
37
+ const [showRepoImport, setShowRepoImport] = useState(false);
38
+ const [showTemplates, setShowTemplates] = useState(false);
39
+ const [provenanceFilter, setProvenanceFilter] = useState<
40
+ "all" | "builtin" | "imported" | "custom"
41
+ >("all");
42
+
43
+ const refreshProfiles = useCallback(async () => {
44
+ try {
45
+ const res = await fetch("/api/profiles");
46
+ if (res.ok) {
47
+ const data = await res.json();
48
+ setProfiles(data);
49
+ }
50
+ } catch {
51
+ // silent — fall back to current state
52
+ }
53
+ router.refresh();
54
+ }, [router]);
55
+
56
+ const builtinProfiles = profiles.filter((p) => p.isBuiltin);
30
57
 
31
58
  const filteredProfiles = useMemo(() => {
32
59
  const q = search.toLowerCase();
33
60
  return profiles.filter((p) => {
34
61
  if (domainFilter !== "all" && p.domain !== domainFilter) return false;
62
+ if (provenanceFilter !== "all") {
63
+ const isImported = !!p.importMeta;
64
+ const isBi = p.isBuiltin;
65
+ if (provenanceFilter === "builtin" && !isBi) return false;
66
+ if (provenanceFilter === "imported" && !isImported) return false;
67
+ if (provenanceFilter === "custom" && (isBi || isImported)) return false;
68
+ }
35
69
  if (!q) return true;
36
70
  return (
37
71
  p.name.toLowerCase().includes(q) ||
@@ -39,22 +73,71 @@ export function ProfileBrowser({ initialProfiles }: ProfileBrowserProps) {
39
73
  p.tags.some((t) => t.toLowerCase().includes(q))
40
74
  );
41
75
  });
42
- }, [profiles, search, domainFilter]);
76
+ }, [profiles, search, domainFilter, provenanceFilter]);
43
77
 
44
78
  return (
45
79
  <div className="space-y-6">
46
80
  {/* Action buttons (title now provided by PageShell) */}
47
81
  <div className="flex items-center justify-end gap-2">
48
- <Button variant="outline" onClick={() => setShowImport(true)}>
49
- <Download className="mr-2 h-4 w-4" />
50
- Import
82
+ <Button variant="outline" onClick={() => setShowTemplates(!showTemplates)}>
83
+ <Copy className="mr-2 h-4 w-4" />
84
+ Start from Template
51
85
  </Button>
86
+ <DropdownMenu>
87
+ <DropdownMenuTrigger asChild>
88
+ <Button variant="outline">
89
+ <Download className="mr-2 h-4 w-4" />
90
+ Import
91
+ <ChevronDown className="ml-1 h-3 w-3" />
92
+ </Button>
93
+ </DropdownMenuTrigger>
94
+ <DropdownMenuContent align="end">
95
+ <DropdownMenuItem onClick={() => setShowImport(true)}>
96
+ <Download className="mr-2 h-4 w-4" />
97
+ Import from URL
98
+ </DropdownMenuItem>
99
+ <DropdownMenuItem onClick={() => setShowRepoImport(true)}>
100
+ <Package className="mr-2 h-4 w-4" />
101
+ Import from Repository
102
+ </DropdownMenuItem>
103
+ </DropdownMenuContent>
104
+ </DropdownMenu>
52
105
  <Button onClick={() => router.push("/profiles/new")}>
53
106
  <Plus className="mr-2 h-4 w-4" />
54
107
  Create Profile
55
108
  </Button>
56
109
  </div>
57
110
 
111
+ {/* Template picker */}
112
+ {showTemplates && builtinProfiles.length > 0 && (
113
+ <div className="surface-panel rounded-2xl p-4 space-y-3">
114
+ <div className="flex items-center justify-between">
115
+ <p className="text-sm font-medium">Use a built-in profile as a starting point</p>
116
+ <Button variant="ghost" size="sm" onClick={() => setShowTemplates(false)}>
117
+ <span className="text-xs">Close</span>
118
+ </Button>
119
+ </div>
120
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
121
+ {builtinProfiles.map((p) => (
122
+ <button
123
+ key={p.id}
124
+ type="button"
125
+ className="bg-muted text-left rounded-lg border border-border/60 p-3 cursor-pointer hover:border-primary/40 hover:bg-accent transition-colors"
126
+ onClick={() => {
127
+ setShowTemplates(false);
128
+ router.push(`/profiles/${p.id}/edit?duplicate=true`);
129
+ }}
130
+ >
131
+ <p className="text-sm font-medium truncate">{p.name}</p>
132
+ <p className="text-xs text-muted-foreground line-clamp-2 mt-0.5">
133
+ {p.description}
134
+ </p>
135
+ </button>
136
+ ))}
137
+ </div>
138
+ </div>
139
+ )}
140
+
58
141
  {/* Search + Domain Filter */}
59
142
  <div className="surface-panel flex flex-col gap-4 rounded-2xl p-4 sm:flex-row sm:items-center">
60
143
  <div className="relative flex-1">
@@ -72,19 +155,37 @@ export function ProfileBrowser({ initialProfiles }: ProfileBrowserProps) {
72
155
  setDomainFilter(v as "all" | "work" | "personal")
73
156
  }
74
157
  >
75
- <TabsList className="surface-control">
158
+ <TabsList>
76
159
  <TabsTrigger value="all">All</TabsTrigger>
77
160
  <TabsTrigger value="work">Work</TabsTrigger>
78
161
  <TabsTrigger value="personal">Personal</TabsTrigger>
79
162
  </TabsList>
80
163
  </Tabs>
164
+ <Tabs
165
+ value={provenanceFilter}
166
+ onValueChange={(v) =>
167
+ setProvenanceFilter(v as "all" | "builtin" | "imported" | "custom")
168
+ }
169
+ >
170
+ <TabsList>
171
+ <TabsTrigger value="all">All</TabsTrigger>
172
+ <TabsTrigger value="builtin">Built-in</TabsTrigger>
173
+ <TabsTrigger value="imported">Imported</TabsTrigger>
174
+ <TabsTrigger value="custom">Custom</TabsTrigger>
175
+ </TabsList>
176
+ </Tabs>
81
177
  </div>
82
178
 
83
179
  {/* Grid */}
84
180
  <ProfileImportDialog
85
181
  open={showImport}
86
182
  onOpenChange={setShowImport}
87
- onImported={() => router.refresh()}
183
+ onImported={refreshProfiles}
184
+ />
185
+ <RepoImportWizard
186
+ open={showRepoImport}
187
+ onOpenChange={setShowRepoImport}
188
+ onImported={refreshProfiles}
88
189
  />
89
190
 
90
191
  {filteredProfiles.length === 0 ? (