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
|
@@ -17,7 +17,9 @@ import { CheckpointList } from "./checkpoint-list";
|
|
|
17
17
|
import { TemplateList } from "./template-list";
|
|
18
18
|
import { HealthScoreCard } from "./health-score-card";
|
|
19
19
|
import { SuggestedProfiles } from "./suggested-profiles";
|
|
20
|
+
import { ProfileCreateDialog } from "./profile-create-dialog";
|
|
20
21
|
import type { HealthScore } from "@/lib/environment/health-scoring";
|
|
22
|
+
import type { ProfileSuggestion } from "@/lib/environment/profile-rules";
|
|
21
23
|
import { DiscoverWorkspaceDialog } from "@/components/workspace/discover-workspace-dialog";
|
|
22
24
|
|
|
23
25
|
interface EnvironmentDashboardProps {
|
|
@@ -28,6 +30,7 @@ interface EnvironmentDashboardProps {
|
|
|
28
30
|
checkpoints?: EnvironmentCheckpointRow[];
|
|
29
31
|
templates?: EnvironmentTemplateRow[];
|
|
30
32
|
healthScore?: HealthScore | null;
|
|
33
|
+
scanPath?: string;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export function EnvironmentDashboard({
|
|
@@ -38,6 +41,7 @@ export function EnvironmentDashboard({
|
|
|
38
41
|
checkpoints = [],
|
|
39
42
|
templates = [],
|
|
40
43
|
healthScore,
|
|
44
|
+
scanPath,
|
|
41
45
|
}: EnvironmentDashboardProps) {
|
|
42
46
|
const router = useRouter();
|
|
43
47
|
const [scanning, setScanning] = useState(false);
|
|
@@ -47,16 +51,46 @@ export function EnvironmentDashboard({
|
|
|
47
51
|
const [scopeFilter, setScopeFilter] = useState<string | null>(null);
|
|
48
52
|
const [searchQuery, setSearchQuery] = useState("");
|
|
49
53
|
const [discoverOpen, setDiscoverOpen] = useState(false);
|
|
54
|
+
const [createProfileSuggestion, setCreateProfileSuggestion] = useState<ProfileSuggestion | null>(null);
|
|
55
|
+
|
|
56
|
+
/** Convert an unlinked skill artifact into a quick suggestion for profile creation. */
|
|
57
|
+
function artifactToSuggestion(artifact: EnvironmentArtifactRow): ProfileSuggestion {
|
|
58
|
+
const name = artifact.name
|
|
59
|
+
.replace(/-/g, " ")
|
|
60
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
61
|
+
let description = `Discovered skill: ${artifact.name}`;
|
|
62
|
+
if (artifact.metadata) {
|
|
63
|
+
try {
|
|
64
|
+
const meta = JSON.parse(artifact.metadata);
|
|
65
|
+
if (meta.description) description = meta.description;
|
|
66
|
+
} catch { /* ignore */ }
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
ruleId: `discovered-${artifact.name}`,
|
|
70
|
+
name,
|
|
71
|
+
description,
|
|
72
|
+
confidence: 0.5,
|
|
73
|
+
tier: "discovered",
|
|
74
|
+
matchedArtifacts: [{ id: artifact.id, name: artifact.name, category: artifact.category }],
|
|
75
|
+
suggestedTools: ["Read", "Grep", "Glob", "Bash"],
|
|
76
|
+
systemPrompt: description,
|
|
77
|
+
tags: artifact.name.split("-").filter((t) => t.length > 2),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
50
80
|
|
|
51
81
|
const handleScan = useCallback(async () => {
|
|
52
82
|
setScanning(true);
|
|
53
83
|
try {
|
|
54
|
-
await fetch("/api/environment/scan", {
|
|
84
|
+
await fetch("/api/environment/scan", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({ projectDir: scanPath }),
|
|
88
|
+
});
|
|
55
89
|
router.refresh();
|
|
56
90
|
} finally {
|
|
57
91
|
setScanning(false);
|
|
58
92
|
}
|
|
59
|
-
}, [router]);
|
|
93
|
+
}, [router, scanPath]);
|
|
60
94
|
|
|
61
95
|
// Filter artifacts client-side
|
|
62
96
|
const filtered = artifacts.filter((a) => {
|
|
@@ -164,6 +198,11 @@ export function EnvironmentDashboard({
|
|
|
164
198
|
key={artifact.id}
|
|
165
199
|
artifact={artifact}
|
|
166
200
|
onClick={() => setSelectedArtifact(artifact)}
|
|
201
|
+
onCreateProfile={
|
|
202
|
+
artifact.category === "skill" && !artifact.linkedProfileId
|
|
203
|
+
? () => setCreateProfileSuggestion(artifactToSuggestion(artifact))
|
|
204
|
+
: undefined
|
|
205
|
+
}
|
|
167
206
|
/>
|
|
168
207
|
))}
|
|
169
208
|
</div>
|
|
@@ -195,6 +234,15 @@ export function EnvironmentDashboard({
|
|
|
195
234
|
onOpenChange={setDiscoverOpen}
|
|
196
235
|
onComplete={() => router.refresh()}
|
|
197
236
|
/>
|
|
237
|
+
|
|
238
|
+
{/* Profile creation from artifact card's "Create Profile" button */}
|
|
239
|
+
<ProfileCreateDialog
|
|
240
|
+
suggestion={createProfileSuggestion}
|
|
241
|
+
open={!!createProfileSuggestion}
|
|
242
|
+
onOpenChange={(open) => {
|
|
243
|
+
if (!open) setCreateProfileSuggestion(null);
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
198
246
|
</div>
|
|
199
247
|
);
|
|
200
248
|
}
|
|
@@ -36,12 +36,15 @@ export function EnvironmentSummaryCard({
|
|
|
36
36
|
const [scanning, setScanning] = useState(false);
|
|
37
37
|
|
|
38
38
|
useEffect(() => {
|
|
39
|
-
|
|
39
|
+
// Auto-scan on mount: pass workingDirectory so the server can rescan if stale
|
|
40
|
+
const params = new URLSearchParams({ projectId });
|
|
41
|
+
if (workingDirectory) params.set("projectDir", workingDirectory);
|
|
42
|
+
fetch(`/api/environment/scan?${params}`)
|
|
40
43
|
.then((res) => res.json())
|
|
41
44
|
.then((json) => setData(json))
|
|
42
45
|
.catch(() => setData(null))
|
|
43
46
|
.finally(() => setLoading(false));
|
|
44
|
-
}, [projectId]);
|
|
47
|
+
}, [projectId, workingDirectory]);
|
|
45
48
|
|
|
46
49
|
const handleScan = async () => {
|
|
47
50
|
if (!workingDirectory) return;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import { Bot, Sparkles } from "lucide-react";
|
|
4
|
+
import { Bot, Sparkles, ChevronDown, ChevronRight } from "lucide-react";
|
|
6
5
|
import { Card, CardContent } from "@/components/ui/card";
|
|
7
6
|
import { Badge } from "@/components/ui/badge";
|
|
8
7
|
import { Button } from "@/components/ui/button";
|
|
@@ -13,22 +12,91 @@ interface SuggestedProfilesProps {
|
|
|
13
12
|
scanId?: string;
|
|
14
13
|
}
|
|
15
14
|
|
|
15
|
+
function SuggestionCard({
|
|
16
|
+
suggestion,
|
|
17
|
+
onSelect,
|
|
18
|
+
}: {
|
|
19
|
+
suggestion: ProfileSuggestion;
|
|
20
|
+
onSelect: () => void;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<Card className="elevation-1 hover:border-primary/40 transition-colors">
|
|
24
|
+
<CardContent className="p-4 space-y-2.5">
|
|
25
|
+
<div className="flex items-start justify-between">
|
|
26
|
+
<div className="flex items-center gap-2">
|
|
27
|
+
<Bot className="h-4 w-4 text-primary" />
|
|
28
|
+
<span className="text-sm font-medium">{suggestion.name}</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div className="flex items-center gap-1.5 shrink-0">
|
|
31
|
+
{suggestion.tier === "discovered" && (
|
|
32
|
+
<Badge variant="secondary" className="text-[10px]">
|
|
33
|
+
Discovered
|
|
34
|
+
</Badge>
|
|
35
|
+
)}
|
|
36
|
+
<Badge variant="outline" className="text-[10px]">
|
|
37
|
+
{Math.round(suggestion.confidence * 100)}%
|
|
38
|
+
</Badge>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
43
|
+
{suggestion.description}
|
|
44
|
+
</p>
|
|
45
|
+
|
|
46
|
+
<div className="flex flex-wrap gap-1">
|
|
47
|
+
{suggestion.matchedArtifacts.map((a, i) => (
|
|
48
|
+
<Badge
|
|
49
|
+
key={`${a.id}-${i}`}
|
|
50
|
+
variant="secondary"
|
|
51
|
+
className="text-[10px] px-1.5 py-0"
|
|
52
|
+
>
|
|
53
|
+
{a.name}
|
|
54
|
+
</Badge>
|
|
55
|
+
))}
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<Button
|
|
59
|
+
variant="outline"
|
|
60
|
+
size="sm"
|
|
61
|
+
className="w-full"
|
|
62
|
+
onClick={onSelect}
|
|
63
|
+
>
|
|
64
|
+
Create Profile
|
|
65
|
+
</Button>
|
|
66
|
+
</CardContent>
|
|
67
|
+
</Card>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
16
71
|
export function SuggestedProfiles({ scanId }: SuggestedProfilesProps) {
|
|
17
|
-
const [
|
|
72
|
+
const [curated, setCurated] = useState<ProfileSuggestion[]>([]);
|
|
73
|
+
const [discovered, setDiscovered] = useState<ProfileSuggestion[]>([]);
|
|
18
74
|
const [loading, setLoading] = useState(true);
|
|
19
|
-
const [selectedSuggestion, setSelectedSuggestion] =
|
|
75
|
+
const [selectedSuggestion, setSelectedSuggestion] =
|
|
76
|
+
useState<ProfileSuggestion | null>(null);
|
|
77
|
+
const [discoveredExpanded, setDiscoveredExpanded] = useState(false);
|
|
20
78
|
|
|
21
79
|
useEffect(() => {
|
|
22
|
-
if (!scanId) {
|
|
80
|
+
if (!scanId) {
|
|
81
|
+
setLoading(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
23
84
|
|
|
24
|
-
fetch(`/api/environment/profiles/suggest?scanId=${scanId}`)
|
|
85
|
+
fetch(`/api/environment/profiles/suggest?scanId=${scanId}&tiered=true`)
|
|
25
86
|
.then((res) => res.json())
|
|
26
|
-
.then((data) =>
|
|
27
|
-
|
|
87
|
+
.then((data) => {
|
|
88
|
+
setCurated(data.curated || []);
|
|
89
|
+
setDiscovered(data.discovered || []);
|
|
90
|
+
})
|
|
91
|
+
.catch(() => {
|
|
92
|
+
setCurated([]);
|
|
93
|
+
setDiscovered([]);
|
|
94
|
+
})
|
|
28
95
|
.finally(() => setLoading(false));
|
|
29
96
|
}, [scanId]);
|
|
30
97
|
|
|
31
|
-
|
|
98
|
+
const totalCount = curated.length + discovered.length;
|
|
99
|
+
if (loading || totalCount === 0) return null;
|
|
32
100
|
|
|
33
101
|
return (
|
|
34
102
|
<div className="space-y-3">
|
|
@@ -36,54 +104,51 @@ export function SuggestedProfiles({ scanId }: SuggestedProfilesProps) {
|
|
|
36
104
|
<Sparkles className="h-4 w-4 text-primary" />
|
|
37
105
|
<h3 className="text-sm font-medium">Suggested Profiles</h3>
|
|
38
106
|
<Badge variant="secondary" className="text-[10px]">
|
|
39
|
-
{
|
|
107
|
+
{totalCount} suggestion{totalCount !== 1 ? "s" : ""}
|
|
40
108
|
</Badge>
|
|
41
109
|
</div>
|
|
42
110
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<Badge
|
|
56
|
-
variant="outline"
|
|
57
|
-
className="text-[10px] shrink-0"
|
|
58
|
-
>
|
|
59
|
-
{Math.round(suggestion.confidence * 100)}%
|
|
60
|
-
</Badge>
|
|
61
|
-
</div>
|
|
62
|
-
|
|
63
|
-
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
64
|
-
{suggestion.description}
|
|
65
|
-
</p>
|
|
111
|
+
{/* Tier 1: Curated suggestions */}
|
|
112
|
+
{curated.length > 0 && (
|
|
113
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
114
|
+
{curated.map((suggestion) => (
|
|
115
|
+
<SuggestionCard
|
|
116
|
+
key={suggestion.ruleId}
|
|
117
|
+
suggestion={suggestion}
|
|
118
|
+
onSelect={() => setSelectedSuggestion(suggestion)}
|
|
119
|
+
/>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
)}
|
|
66
123
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
124
|
+
{/* Tier 2: Discovered suggestions (collapsible) */}
|
|
125
|
+
{discovered.length > 0 && (
|
|
126
|
+
<div className="space-y-2">
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => setDiscoveredExpanded(!discoveredExpanded)}
|
|
129
|
+
className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
130
|
+
>
|
|
131
|
+
{discoveredExpanded ? (
|
|
132
|
+
<ChevronDown className="h-3 w-3" />
|
|
133
|
+
) : (
|
|
134
|
+
<ChevronRight className="h-3 w-3" />
|
|
135
|
+
)}
|
|
136
|
+
{discovered.length} discoverable skill{discovered.length !== 1 ? "s" : ""}
|
|
137
|
+
</button>
|
|
74
138
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
139
|
+
{discoveredExpanded && (
|
|
140
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
141
|
+
{discovered.map((suggestion) => (
|
|
142
|
+
<SuggestionCard
|
|
143
|
+
key={suggestion.ruleId}
|
|
144
|
+
suggestion={suggestion}
|
|
145
|
+
onSelect={() => setSelectedSuggestion(suggestion)}
|
|
146
|
+
/>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
87
152
|
|
|
88
153
|
<ProfileCreateDialog
|
|
89
154
|
suggestion={selectedSuggestion}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ArrowRight,
|
|
6
|
+
CheckCircle2,
|
|
7
|
+
XCircle,
|
|
8
|
+
Clock,
|
|
9
|
+
AlertTriangle,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import { toast } from "sonner";
|
|
12
|
+
import { Button } from "@/components/ui/button";
|
|
13
|
+
import { Badge } from "@/components/ui/badge";
|
|
14
|
+
|
|
15
|
+
interface HandoffApprovalCardProps {
|
|
16
|
+
id: string;
|
|
17
|
+
fromProfileId: string;
|
|
18
|
+
toProfileId: string;
|
|
19
|
+
subject: string;
|
|
20
|
+
body: string;
|
|
21
|
+
priority: number;
|
|
22
|
+
chainDepth: number;
|
|
23
|
+
status: string;
|
|
24
|
+
requiresApproval: boolean;
|
|
25
|
+
onActionComplete?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PRIORITY_LABELS: Record<number, string> = {
|
|
29
|
+
0: "Critical",
|
|
30
|
+
1: "High",
|
|
31
|
+
2: "Medium",
|
|
32
|
+
3: "Low",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const PRIORITY_VARIANTS: Record<number, "destructive" | "default" | "secondary" | "outline"> = {
|
|
36
|
+
0: "destructive",
|
|
37
|
+
1: "default",
|
|
38
|
+
2: "secondary",
|
|
39
|
+
3: "outline",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const STATUS_ICONS: Record<string, typeof Clock> = {
|
|
43
|
+
pending: Clock,
|
|
44
|
+
accepted: CheckCircle2,
|
|
45
|
+
in_progress: Clock,
|
|
46
|
+
completed: CheckCircle2,
|
|
47
|
+
rejected: XCircle,
|
|
48
|
+
expired: AlertTriangle,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const STATUS_VARIANTS: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
|
|
52
|
+
pending: "outline",
|
|
53
|
+
accepted: "default",
|
|
54
|
+
in_progress: "default",
|
|
55
|
+
completed: "secondary",
|
|
56
|
+
rejected: "destructive",
|
|
57
|
+
expired: "outline",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function HandoffApprovalCard({
|
|
61
|
+
id,
|
|
62
|
+
fromProfileId,
|
|
63
|
+
toProfileId,
|
|
64
|
+
subject,
|
|
65
|
+
body,
|
|
66
|
+
priority,
|
|
67
|
+
chainDepth,
|
|
68
|
+
status,
|
|
69
|
+
requiresApproval,
|
|
70
|
+
onActionComplete,
|
|
71
|
+
}: HandoffApprovalCardProps) {
|
|
72
|
+
const [acting, setActing] = useState(false);
|
|
73
|
+
|
|
74
|
+
const StatusIcon = STATUS_ICONS[status] ?? Clock;
|
|
75
|
+
|
|
76
|
+
const handleAction = async (action: "approve" | "reject") => {
|
|
77
|
+
setActing(true);
|
|
78
|
+
try {
|
|
79
|
+
const res = await fetch(`/api/handoffs/${id}`, {
|
|
80
|
+
method: "PATCH",
|
|
81
|
+
headers: { "Content-Type": "application/json" },
|
|
82
|
+
body: JSON.stringify({ action, approvedBy: "user" }),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (res.ok) {
|
|
86
|
+
toast.success(`Handoff ${action === "approve" ? "approved" : "rejected"}`);
|
|
87
|
+
onActionComplete?.();
|
|
88
|
+
} else {
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
toast.error(data.error ?? `Failed to ${action} handoff`);
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
toast.error(`Failed to ${action} handoff`);
|
|
94
|
+
} finally {
|
|
95
|
+
setActing(false);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="rounded-lg border p-4 space-y-3">
|
|
101
|
+
<div className="flex items-start justify-between">
|
|
102
|
+
<div className="space-y-1">
|
|
103
|
+
<h4 className="text-sm font-medium">{subject}</h4>
|
|
104
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
105
|
+
<span className="font-mono">{fromProfileId}</span>
|
|
106
|
+
<ArrowRight className="h-3 w-3" />
|
|
107
|
+
<span className="font-mono">{toProfileId}</span>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
<div className="flex items-center gap-2">
|
|
111
|
+
<Badge variant={PRIORITY_VARIANTS[priority] ?? "secondary"}>
|
|
112
|
+
{PRIORITY_LABELS[priority] ?? "Medium"}
|
|
113
|
+
</Badge>
|
|
114
|
+
<Badge variant={STATUS_VARIANTS[status] ?? "outline"}>
|
|
115
|
+
<StatusIcon className="mr-1 h-3 w-3" />
|
|
116
|
+
{status}
|
|
117
|
+
</Badge>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<p className="text-sm text-muted-foreground line-clamp-3">{body}</p>
|
|
122
|
+
|
|
123
|
+
<div className="flex items-center justify-between">
|
|
124
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
125
|
+
{chainDepth > 0 && (
|
|
126
|
+
<span>Chain depth: {chainDepth}</span>
|
|
127
|
+
)}
|
|
128
|
+
{requiresApproval && status === "pending" && (
|
|
129
|
+
<Badge variant="outline" className="text-xs">
|
|
130
|
+
Awaiting approval
|
|
131
|
+
</Badge>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{status === "pending" && requiresApproval && (
|
|
136
|
+
<div className="flex items-center gap-2">
|
|
137
|
+
<Button
|
|
138
|
+
variant="outline"
|
|
139
|
+
size="sm"
|
|
140
|
+
onClick={() => handleAction("reject")}
|
|
141
|
+
disabled={acting}
|
|
142
|
+
>
|
|
143
|
+
<XCircle className="mr-1 h-3.5 w-3.5" />
|
|
144
|
+
Reject
|
|
145
|
+
</Button>
|
|
146
|
+
<Button
|
|
147
|
+
size="sm"
|
|
148
|
+
onClick={() => handleAction("approve")}
|
|
149
|
+
disabled={acting}
|
|
150
|
+
>
|
|
151
|
+
<CheckCircle2 className="mr-1 h-3.5 w-3.5" />
|
|
152
|
+
Approve
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|