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.
- package/README.md +8 -8
- package/dist/cli.js +146 -2
- package/docs/.coverage-gaps.json +21 -0
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +36 -14
- package/docs/features/chat.md +33 -56
- package/docs/features/cost-usage.md +14 -10
- package/docs/features/dashboard-kanban.md +30 -13
- package/docs/features/delivery-channels.md +198 -0
- package/docs/features/design-system.md +10 -10
- package/docs/features/documents.md +8 -8
- package/docs/features/home-workspace.md +20 -15
- package/docs/features/inbox-notifications.md +22 -10
- package/docs/features/keyboard-navigation.md +11 -11
- package/docs/features/monitoring.md +1 -1
- package/docs/features/playbook.md +30 -32
- package/docs/features/profiles.md +33 -11
- package/docs/features/projects.md +2 -2
- package/docs/features/provider-runtimes.md +58 -14
- package/docs/features/schedules.md +70 -40
- package/docs/features/settings.md +74 -46
- package/docs/features/shared-components.md +7 -15
- package/docs/features/tool-permissions.md +9 -9
- package/docs/features/workflows.md +32 -21
- package/docs/getting-started.md +33 -9
- package/docs/index.md +25 -16
- package/docs/journeys/developer.md +124 -207
- package/docs/journeys/personal-use.md +70 -79
- package/docs/journeys/power-user.md +107 -151
- package/docs/journeys/work-use.md +81 -113
- package/docs/manifest.json +77 -45
- package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
- package/docs/use-cases/agency-operator.md +84 -0
- package/docs/use-cases/solo-founder.md +75 -0
- package/docs/why-stagent.md +59 -0
- package/package.json +10 -3
- package/src/app/api/channels/[id]/route.ts +103 -0
- package/src/app/api/channels/[id]/test/route.ts +52 -0
- package/src/app/api/channels/inbound/slack/route.ts +109 -0
- package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
- package/src/app/api/channels/inbound/telegram/route.ts +76 -0
- package/src/app/api/channels/route.ts +71 -0
- package/src/app/api/chat/conversations/route.ts +15 -0
- package/src/app/api/chat/entities/search/route.ts +46 -31
- package/src/app/api/environment/profiles/suggest/route.ts +19 -3
- package/src/app/api/environment/scan/route.ts +8 -1
- package/src/app/api/handoffs/[id]/route.ts +76 -0
- package/src/app/api/handoffs/route.ts +89 -0
- package/src/app/api/memory/route.ts +181 -0
- package/src/app/api/profiles/[id]/route.ts +16 -1
- package/src/app/api/profiles/[id]/test/route.ts +4 -0
- package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
- package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
- package/src/app/api/profiles/assist/route.ts +35 -0
- package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
- package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
- package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
- package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
- package/src/app/api/profiles/import-repo/route.ts +29 -0
- package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
- package/src/app/api/profiles/route.ts +73 -22
- package/src/app/api/runtimes/ollama/route.ts +86 -0
- package/src/app/api/runtimes/suggest/route.ts +29 -0
- package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
- package/src/app/api/schedules/[id]/route.ts +41 -3
- package/src/app/api/schedules/parse/route.ts +66 -0
- package/src/app/api/schedules/route.ts +71 -12
- package/src/app/api/settings/author-default/route.ts +7 -0
- package/src/app/api/settings/learning/route.ts +41 -0
- package/src/app/api/settings/ollama/route.ts +34 -0
- package/src/app/api/settings/providers/route.ts +57 -0
- package/src/app/api/settings/routing/route.ts +24 -0
- package/src/app/api/settings/web-search/route.ts +28 -0
- package/src/app/api/tasks/[id]/execute/route.ts +13 -1
- package/src/app/documents/page.tsx +3 -0
- package/src/app/environment/page.tsx +8 -1
- package/src/app/settings/page.tsx +10 -4
- package/src/app/workflows/[id]/edit/page.tsx +2 -0
- package/src/app/workflows/new/page.tsx +2 -0
- package/src/components/chat/chat-command-popover.tsx +22 -19
- package/src/components/chat/chat-input.tsx +5 -0
- package/src/components/chat/chat-model-selector.tsx +42 -1
- package/src/components/chat/chat-shell.tsx +2 -0
- package/src/components/dashboard/welcome-landing.tsx +9 -9
- package/src/components/environment/artifact-card.tsx +27 -1
- package/src/components/environment/environment-dashboard.tsx +50 -2
- package/src/components/environment/environment-summary-card.tsx +5 -2
- package/src/components/environment/suggested-profiles.tsx +117 -52
- package/src/components/handoffs/handoff-approval-card.tsx +159 -0
- package/src/components/memory/memory-browser.tsx +315 -0
- package/src/components/profiles/learned-context-panel.tsx +4 -4
- package/src/components/profiles/profile-assist-panel.tsx +512 -0
- package/src/components/profiles/profile-browser.tsx +109 -8
- package/src/components/profiles/profile-card.tsx +29 -1
- package/src/components/profiles/profile-detail-view.tsx +200 -28
- package/src/components/profiles/profile-form-view.tsx +220 -82
- package/src/components/profiles/repo-import-wizard.tsx +648 -0
- package/src/components/profiles/smoke-test-editor.tsx +106 -0
- package/src/components/schedules/schedule-create-sheet.tsx +9 -1
- package/src/components/schedules/schedule-form.tsx +348 -9
- package/src/components/schedules/schedule-list.tsx +15 -2
- package/src/components/settings/auth-method-selector.tsx +7 -1
- package/src/components/settings/budget-guardrails-section.tsx +111 -48
- package/src/components/settings/channels-section.tsx +526 -0
- package/src/components/settings/chat-settings-section.tsx +27 -1
- package/src/components/settings/data-management-section.tsx +8 -6
- package/src/components/settings/learning-context-section.tsx +124 -0
- package/src/components/settings/ollama-section.tsx +270 -0
- package/src/components/settings/providers-runtimes-section.tsx +499 -0
- package/src/components/settings/web-search-section.tsx +101 -0
- package/src/components/shared/tag-input.tsx +156 -0
- package/src/components/tasks/kanban-board.tsx +32 -0
- package/src/components/tasks/kanban-column.tsx +4 -2
- package/src/components/tasks/task-card.tsx +1 -0
- package/src/components/tasks/task-chip-bar.tsx +6 -1
- package/src/components/tasks/task-create-panel.tsx +55 -5
- package/src/components/workflows/workflow-form-view.tsx +38 -3
- package/src/hooks/use-chat-autocomplete.ts +24 -26
- package/src/hooks/use-project-skills.ts +66 -0
- package/src/hooks/use-tag-suggestions.ts +31 -0
- package/src/instrumentation.ts +4 -1
- package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
- package/src/lib/agents/agentic-loop.ts +235 -0
- package/src/lib/agents/browser-mcp.ts +59 -4
- package/src/lib/agents/claude-agent.ts +26 -199
- package/src/lib/agents/handoff/bus.ts +164 -0
- package/src/lib/agents/handoff/governance.ts +47 -0
- package/src/lib/agents/handoff/types.ts +16 -0
- package/src/lib/agents/learned-context.ts +27 -7
- package/src/lib/agents/memory/decay.ts +61 -0
- package/src/lib/agents/memory/extractor.ts +181 -0
- package/src/lib/agents/memory/retrieval.ts +96 -0
- package/src/lib/agents/memory/types.ts +6 -0
- package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
- package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
- package/src/lib/agents/profiles/project-profiles.ts +193 -0
- package/src/lib/agents/profiles/registry.ts +130 -6
- package/src/lib/agents/profiles/types.ts +28 -0
- package/src/lib/agents/router.ts +174 -2
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
- package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
- package/src/lib/agents/runtime/catalog.ts +57 -2
- package/src/lib/agents/runtime/claude.ts +205 -1
- package/src/lib/agents/runtime/index.ts +22 -0
- package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
- package/src/lib/agents/runtime/openai-direct.ts +514 -0
- package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
- package/src/lib/agents/runtime/types.ts +2 -0
- package/src/lib/agents/tool-permissions.ts +203 -0
- package/src/lib/channels/gateway.ts +321 -0
- package/src/lib/channels/poller.ts +268 -0
- package/src/lib/channels/registry.ts +90 -0
- package/src/lib/channels/slack-adapter.ts +188 -0
- package/src/lib/channels/telegram-adapter.ts +218 -0
- package/src/lib/channels/types.ts +43 -0
- package/src/lib/channels/webhook-adapter.ts +74 -0
- package/src/lib/chat/context-builder.ts +22 -2
- package/src/lib/chat/engine.ts +95 -13
- package/src/lib/chat/ollama-engine.ts +198 -0
- package/src/lib/chat/stagent-tools.ts +106 -20
- package/src/lib/chat/tool-catalog.ts +24 -0
- package/src/lib/chat/tool-registry.ts +90 -0
- package/src/lib/chat/tools/chat-history-tools.ts +4 -4
- package/src/lib/chat/tools/document-tools.ts +7 -7
- package/src/lib/chat/tools/handoff-tools.ts +70 -0
- package/src/lib/chat/tools/notification-tools.ts +4 -4
- package/src/lib/chat/tools/profile-tools.ts +3 -3
- package/src/lib/chat/tools/project-tools.ts +3 -3
- package/src/lib/chat/tools/schedule-tools.ts +29 -13
- package/src/lib/chat/tools/settings-tools.ts +2 -2
- package/src/lib/chat/tools/task-tools.ts +66 -11
- package/src/lib/chat/tools/usage-tools.ts +2 -2
- package/src/lib/chat/tools/workflow-tools.ts +8 -8
- package/src/lib/chat/types.ts +11 -5
- package/src/lib/constants/known-tools.ts +19 -0
- package/src/lib/constants/prose-styles.ts +1 -1
- package/src/lib/constants/settings.ts +7 -0
- package/src/lib/data/channel-bindings.ts +85 -0
- package/src/lib/data/clear.ts +22 -0
- package/src/lib/data/profile-test-results.ts +48 -0
- package/src/lib/data/seed-data/conversations.ts +196 -0
- package/src/lib/data/seed-data/learned-context.ts +99 -0
- package/src/lib/data/seed-data/notifications.ts +54 -1
- package/src/lib/data/seed-data/profile-test-results.ts +96 -0
- package/src/lib/data/seed-data/repo-imports.ts +51 -0
- package/src/lib/data/seed-data/views.ts +60 -0
- package/src/lib/data/seed.ts +51 -0
- package/src/lib/db/bootstrap.ts +162 -0
- package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
- package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
- package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
- package/src/lib/db/schema.ts +187 -1
- package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
- package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
- package/src/lib/environment/auto-scan.ts +48 -0
- package/src/lib/environment/data.ts +25 -0
- package/src/lib/environment/profile-generator.ts +40 -10
- package/src/lib/environment/profile-linker.ts +143 -0
- package/src/lib/environment/profile-rules.ts +96 -0
- package/src/lib/import/dedup.ts +149 -0
- package/src/lib/import/format-adapter.ts +631 -0
- package/src/lib/import/github-api.ts +219 -0
- package/src/lib/import/repo-scanner.ts +251 -0
- package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
- package/src/lib/schedules/active-hours.ts +120 -0
- package/src/lib/schedules/heartbeat-parser.ts +224 -0
- package/src/lib/schedules/heartbeat-prompt.ts +153 -0
- package/src/lib/schedules/nlp-parser.ts +357 -0
- package/src/lib/schedules/scheduler.ts +218 -3
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
- package/src/lib/settings/helpers.ts +6 -0
- package/src/lib/settings/routing.ts +24 -0
- package/src/lib/settings/runtime-setup.ts +28 -1
- package/src/lib/usage/ledger.ts +2 -1
- package/src/lib/validators/__tests__/settings.test.ts +9 -0
- package/src/lib/validators/profile.ts +39 -0
- package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
- package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
- package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
- package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
- 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={() =>
|
|
49
|
-
<
|
|
50
|
-
|
|
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
|
|
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={
|
|
183
|
+
onImported={refreshProfiles}
|
|
184
|
+
/>
|
|
185
|
+
<RepoImportWizard
|
|
186
|
+
open={showRepoImport}
|
|
187
|
+
onOpenChange={setShowRepoImport}
|
|
188
|
+
onImported={refreshProfiles}
|
|
88
189
|
/>
|
|
89
190
|
|
|
90
191
|
{filteredProfiles.length === 0 ? (
|