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,106 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
5
|
+
import { Label } from "@/components/ui/label";
|
|
6
|
+
import { TagInput } from "@/components/shared/tag-input";
|
|
7
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
8
|
+
|
|
9
|
+
export interface SmokeTestDraft {
|
|
10
|
+
task: string;
|
|
11
|
+
expectedKeywords: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SmokeTestEditorProps {
|
|
15
|
+
tests: SmokeTestDraft[];
|
|
16
|
+
onChange: (tests: SmokeTestDraft[]) => void;
|
|
17
|
+
keywordSuggestions?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SmokeTestEditor({
|
|
21
|
+
tests,
|
|
22
|
+
onChange,
|
|
23
|
+
keywordSuggestions = [],
|
|
24
|
+
}: SmokeTestEditorProps) {
|
|
25
|
+
const addTest = () => {
|
|
26
|
+
onChange([...tests, { task: "", expectedKeywords: "" }]);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const removeTest = (index: number) => {
|
|
30
|
+
onChange(tests.filter((_, i) => i !== index));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const updateTest = (index: number, field: keyof SmokeTestDraft, value: string) => {
|
|
34
|
+
const updated = tests.map((t, i) =>
|
|
35
|
+
i === index ? { ...t, [field]: value } : t
|
|
36
|
+
);
|
|
37
|
+
onChange(updated);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="space-y-3">
|
|
42
|
+
{tests.length === 0 && (
|
|
43
|
+
<p className="text-xs text-muted-foreground">
|
|
44
|
+
No tests defined. Add a test to verify this profile behaves correctly.
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{tests.map((test, i) => (
|
|
49
|
+
<div
|
|
50
|
+
key={i}
|
|
51
|
+
className="space-y-2 rounded-md border border-border/60 p-3"
|
|
52
|
+
>
|
|
53
|
+
<div className="flex items-center justify-between">
|
|
54
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
55
|
+
Test {i + 1}
|
|
56
|
+
</span>
|
|
57
|
+
<Button
|
|
58
|
+
type="button"
|
|
59
|
+
variant="ghost"
|
|
60
|
+
size="sm"
|
|
61
|
+
className="h-6 w-6 p-0 text-muted-foreground hover:text-destructive"
|
|
62
|
+
onClick={() => removeTest(i)}
|
|
63
|
+
>
|
|
64
|
+
<Trash2 className="h-3 w-3" />
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="space-y-1">
|
|
69
|
+
<Label className="text-xs">Task</Label>
|
|
70
|
+
<Textarea
|
|
71
|
+
value={test.task}
|
|
72
|
+
onChange={(e) => updateTest(i, "task", e.target.value)}
|
|
73
|
+
placeholder="Describe a task this agent should handle well..."
|
|
74
|
+
rows={2}
|
|
75
|
+
className="text-sm"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div className="space-y-1">
|
|
80
|
+
<Label className="text-xs">Expected Keywords</Label>
|
|
81
|
+
<TagInput
|
|
82
|
+
value={test.expectedKeywords}
|
|
83
|
+
onChange={(v) => updateTest(i, "expectedKeywords", v)}
|
|
84
|
+
suggestions={keywordSuggestions}
|
|
85
|
+
placeholder="keyword1, keyword2, keyword3"
|
|
86
|
+
/>
|
|
87
|
+
<p className="text-xs text-muted-foreground">
|
|
88
|
+
Response must contain these keywords to pass.
|
|
89
|
+
</p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
|
|
94
|
+
<Button
|
|
95
|
+
type="button"
|
|
96
|
+
variant="ghost"
|
|
97
|
+
size="sm"
|
|
98
|
+
onClick={addTest}
|
|
99
|
+
className="w-full border border-dashed border-border/60"
|
|
100
|
+
>
|
|
101
|
+
<Plus className="h-3 w-3 mr-1" />
|
|
102
|
+
Add Test
|
|
103
|
+
</Button>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -38,7 +38,7 @@ export function ScheduleCreateSheet({
|
|
|
38
38
|
headers: { "Content-Type": "application/json" },
|
|
39
39
|
body: JSON.stringify({
|
|
40
40
|
name: values.name,
|
|
41
|
-
prompt: values.prompt,
|
|
41
|
+
prompt: values.prompt || undefined,
|
|
42
42
|
interval: values.interval,
|
|
43
43
|
projectId: values.projectId || undefined,
|
|
44
44
|
assignedAgent: values.assignedAgent || undefined,
|
|
@@ -46,6 +46,14 @@ export function ScheduleCreateSheet({
|
|
|
46
46
|
recurs: values.recurs,
|
|
47
47
|
maxFirings: values.maxFirings || undefined,
|
|
48
48
|
expiresInHours: values.expiresInHours || undefined,
|
|
49
|
+
type: values.type,
|
|
50
|
+
...(values.type === "heartbeat" && {
|
|
51
|
+
heartbeatChecklist: values.heartbeatChecklist,
|
|
52
|
+
activeHoursStart: values.activeHoursStart || undefined,
|
|
53
|
+
activeHoursEnd: values.activeHoursEnd || undefined,
|
|
54
|
+
activeTimezone: values.activeTimezone || undefined,
|
|
55
|
+
heartbeatBudgetPerDay: values.heartbeatBudgetPerDay || undefined,
|
|
56
|
+
}),
|
|
49
57
|
}),
|
|
50
58
|
});
|
|
51
59
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from "react";
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
4
|
import { Input } from "@/components/ui/input";
|
|
5
5
|
import { Textarea } from "@/components/ui/textarea";
|
|
6
6
|
import { Label } from "@/components/ui/label";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
SelectValue,
|
|
14
14
|
} from "@/components/ui/select";
|
|
15
15
|
import { Switch } from "@/components/ui/switch";
|
|
16
|
-
import { Clock, Bot } from "lucide-react";
|
|
16
|
+
import { Clock, Bot, Heart, Plus, X, GripVertical, Sparkles, CheckCircle2, AlertCircle } from "lucide-react";
|
|
17
17
|
import {
|
|
18
18
|
type AgentRuntimeId,
|
|
19
19
|
DEFAULT_AGENT_RUNTIME,
|
|
@@ -37,6 +37,12 @@ export const INTERVAL_PRESETS = [
|
|
|
37
37
|
{ label: "Custom", value: "custom" },
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
+
export interface HeartbeatChecklistItem {
|
|
41
|
+
id: string;
|
|
42
|
+
instruction: string;
|
|
43
|
+
priority: "high" | "medium" | "low";
|
|
44
|
+
}
|
|
45
|
+
|
|
40
46
|
export interface ScheduleFormValues {
|
|
41
47
|
name: string;
|
|
42
48
|
prompt: string;
|
|
@@ -47,6 +53,12 @@ export interface ScheduleFormValues {
|
|
|
47
53
|
recurs: boolean;
|
|
48
54
|
maxFirings: number | "";
|
|
49
55
|
expiresInHours: number | "";
|
|
56
|
+
type: "scheduled" | "heartbeat";
|
|
57
|
+
heartbeatChecklist: HeartbeatChecklistItem[];
|
|
58
|
+
activeHoursStart: number | "";
|
|
59
|
+
activeHoursEnd: number | "";
|
|
60
|
+
activeTimezone: string;
|
|
61
|
+
heartbeatBudgetPerDay: number | "";
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
export interface ScheduleFormInitialValues {
|
|
@@ -126,6 +138,69 @@ export function ScheduleForm({
|
|
|
126
138
|
);
|
|
127
139
|
const [profiles, setProfiles] = useState<ProfileOption[]>([]);
|
|
128
140
|
|
|
141
|
+
// NL schedule input state
|
|
142
|
+
const [nlInput, setNlInput] = useState("");
|
|
143
|
+
const [nlParsing, setNlParsing] = useState(false);
|
|
144
|
+
const [nlResult, setNlResult] = useState<{
|
|
145
|
+
cronExpression: string;
|
|
146
|
+
description: string;
|
|
147
|
+
nextFireTimes: string[];
|
|
148
|
+
confidence: number;
|
|
149
|
+
} | null>(null);
|
|
150
|
+
const [nlError, setNlError] = useState<string | null>(null);
|
|
151
|
+
const nlTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
152
|
+
|
|
153
|
+
const parseNlExpression = useCallback(async (value: string) => {
|
|
154
|
+
if (!value.trim()) {
|
|
155
|
+
setNlResult(null);
|
|
156
|
+
setNlError(null);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
setNlParsing(true);
|
|
160
|
+
setNlError(null);
|
|
161
|
+
try {
|
|
162
|
+
const res = await fetch("/api/schedules/parse", {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { "Content-Type": "application/json" },
|
|
165
|
+
body: JSON.stringify({ expression: value.trim() }),
|
|
166
|
+
});
|
|
167
|
+
const data = await res.json();
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
setNlResult(null);
|
|
170
|
+
setNlError(data.error ?? "Could not parse");
|
|
171
|
+
} else {
|
|
172
|
+
setNlResult(data);
|
|
173
|
+
setNlError(null);
|
|
174
|
+
// Auto-fill if confidence >= 0.8
|
|
175
|
+
if (data.confidence >= 0.8) {
|
|
176
|
+
setIntervalPreset("custom");
|
|
177
|
+
setCustomInterval(data.cronExpression);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
setNlResult(null);
|
|
182
|
+
setNlError("Failed to reach parse API");
|
|
183
|
+
} finally {
|
|
184
|
+
setNlParsing(false);
|
|
185
|
+
}
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
function handleNlInputChange(value: string) {
|
|
189
|
+
setNlInput(value);
|
|
190
|
+
if (nlTimerRef.current) clearTimeout(nlTimerRef.current);
|
|
191
|
+
nlTimerRef.current = setTimeout(() => {
|
|
192
|
+
parseNlExpression(value);
|
|
193
|
+
}, 500);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Heartbeat state
|
|
197
|
+
const [scheduleType, setScheduleType] = useState<"scheduled" | "heartbeat">("scheduled");
|
|
198
|
+
const [heartbeatChecklist, setHeartbeatChecklist] = useState<HeartbeatChecklistItem[]>([]);
|
|
199
|
+
const [activeHoursStart, setActiveHoursStart] = useState<number | "">(9);
|
|
200
|
+
const [activeHoursEnd, setActiveHoursEnd] = useState<number | "">(17);
|
|
201
|
+
const [activeTimezone, setActiveTimezone] = useState("UTC");
|
|
202
|
+
const [heartbeatBudgetPerDay, setHeartbeatBudgetPerDay] = useState<number | "">("");
|
|
203
|
+
|
|
129
204
|
useEffect(() => {
|
|
130
205
|
fetch("/api/profiles")
|
|
131
206
|
.then((r) => r.json())
|
|
@@ -147,7 +222,16 @@ export function ScheduleForm({
|
|
|
147
222
|
|
|
148
223
|
async function handleSubmit(e: React.FormEvent) {
|
|
149
224
|
e.preventDefault();
|
|
150
|
-
if (!name.trim()
|
|
225
|
+
if (!name.trim()) return;
|
|
226
|
+
if (scheduleType === "scheduled" && !prompt.trim()) return;
|
|
227
|
+
if (scheduleType === "heartbeat" && heartbeatChecklist.length === 0) {
|
|
228
|
+
onError("Add at least one checklist item");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (scheduleType === "heartbeat" && heartbeatChecklist.some((item) => !item.instruction.trim())) {
|
|
232
|
+
onError("All checklist items must have instructions");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
151
235
|
|
|
152
236
|
const interval =
|
|
153
237
|
intervalPreset === "custom" ? customInterval : intervalPreset;
|
|
@@ -171,6 +255,12 @@ export function ScheduleForm({
|
|
|
171
255
|
recurs,
|
|
172
256
|
maxFirings,
|
|
173
257
|
expiresInHours,
|
|
258
|
+
type: scheduleType,
|
|
259
|
+
heartbeatChecklist,
|
|
260
|
+
activeHoursStart,
|
|
261
|
+
activeHoursEnd,
|
|
262
|
+
activeTimezone,
|
|
263
|
+
heartbeatBudgetPerDay,
|
|
174
264
|
});
|
|
175
265
|
}
|
|
176
266
|
|
|
@@ -191,19 +281,263 @@ export function ScheduleForm({
|
|
|
191
281
|
</p>
|
|
192
282
|
</div>
|
|
193
283
|
|
|
194
|
-
{/*
|
|
284
|
+
{/* Schedule Type */}
|
|
285
|
+
<div className="space-y-2">
|
|
286
|
+
<Label>Schedule Type</Label>
|
|
287
|
+
<div className="grid grid-cols-2 gap-2">
|
|
288
|
+
<button
|
|
289
|
+
type="button"
|
|
290
|
+
onClick={() => setScheduleType("scheduled")}
|
|
291
|
+
className={`flex items-center gap-2 rounded-lg border p-3 text-left text-sm transition-colors ${
|
|
292
|
+
scheduleType === "scheduled"
|
|
293
|
+
? "border-primary bg-primary/5"
|
|
294
|
+
: "border-border hover:border-muted-foreground/30"
|
|
295
|
+
}`}
|
|
296
|
+
>
|
|
297
|
+
<Clock className="h-4 w-4 text-muted-foreground" />
|
|
298
|
+
<div>
|
|
299
|
+
<div className="font-medium">Interval</div>
|
|
300
|
+
<div className="text-xs text-muted-foreground">
|
|
301
|
+
Fire on a schedule
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
</button>
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
onClick={() => setScheduleType("heartbeat")}
|
|
308
|
+
className={`flex items-center gap-2 rounded-lg border p-3 text-left text-sm transition-colors ${
|
|
309
|
+
scheduleType === "heartbeat"
|
|
310
|
+
? "border-primary bg-primary/5"
|
|
311
|
+
: "border-border hover:border-muted-foreground/30"
|
|
312
|
+
}`}
|
|
313
|
+
>
|
|
314
|
+
<Heart className="h-4 w-4 text-muted-foreground" />
|
|
315
|
+
<div>
|
|
316
|
+
<div className="font-medium">Heartbeat</div>
|
|
317
|
+
<div className="text-xs text-muted-foreground">
|
|
318
|
+
Check, then act if needed
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
</button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{/* Heartbeat Checklist (heartbeat only) */}
|
|
326
|
+
{scheduleType === "heartbeat" && (
|
|
327
|
+
<div className="space-y-2">
|
|
328
|
+
<Label className="flex items-center gap-1.5">
|
|
329
|
+
<Heart className="h-3.5 w-3.5 text-muted-foreground" />
|
|
330
|
+
Checklist
|
|
331
|
+
</Label>
|
|
332
|
+
<p className="text-xs text-muted-foreground">
|
|
333
|
+
Items the agent evaluates each heartbeat. Only acts if something needs attention.
|
|
334
|
+
</p>
|
|
335
|
+
<div className="space-y-2">
|
|
336
|
+
{heartbeatChecklist.map((item, idx) => (
|
|
337
|
+
<div key={item.id} className="flex items-start gap-2">
|
|
338
|
+
<GripVertical className="h-4 w-4 mt-2.5 text-muted-foreground/50 shrink-0" />
|
|
339
|
+
<div className="flex-1 space-y-1">
|
|
340
|
+
<Input
|
|
341
|
+
value={item.instruction}
|
|
342
|
+
onChange={(e) => {
|
|
343
|
+
const updated = [...heartbeatChecklist];
|
|
344
|
+
updated[idx] = { ...item, instruction: e.target.value };
|
|
345
|
+
setHeartbeatChecklist(updated);
|
|
346
|
+
}}
|
|
347
|
+
placeholder="e.g., Check if there are unread customer inquiries older than 2 hours"
|
|
348
|
+
/>
|
|
349
|
+
</div>
|
|
350
|
+
<Select
|
|
351
|
+
value={item.priority}
|
|
352
|
+
onValueChange={(v) => {
|
|
353
|
+
const updated = [...heartbeatChecklist];
|
|
354
|
+
updated[idx] = { ...item, priority: v as "high" | "medium" | "low" };
|
|
355
|
+
setHeartbeatChecklist(updated);
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
<SelectTrigger className="w-24 shrink-0">
|
|
359
|
+
<SelectValue />
|
|
360
|
+
</SelectTrigger>
|
|
361
|
+
<SelectContent>
|
|
362
|
+
<SelectItem value="high">High</SelectItem>
|
|
363
|
+
<SelectItem value="medium">Medium</SelectItem>
|
|
364
|
+
<SelectItem value="low">Low</SelectItem>
|
|
365
|
+
</SelectContent>
|
|
366
|
+
</Select>
|
|
367
|
+
<Button
|
|
368
|
+
type="button"
|
|
369
|
+
variant="ghost"
|
|
370
|
+
size="icon"
|
|
371
|
+
className="shrink-0"
|
|
372
|
+
onClick={() => {
|
|
373
|
+
setHeartbeatChecklist(heartbeatChecklist.filter((_, i) => i !== idx));
|
|
374
|
+
}}
|
|
375
|
+
>
|
|
376
|
+
<X className="h-4 w-4" />
|
|
377
|
+
</Button>
|
|
378
|
+
</div>
|
|
379
|
+
))}
|
|
380
|
+
<Button
|
|
381
|
+
type="button"
|
|
382
|
+
variant="outline"
|
|
383
|
+
size="sm"
|
|
384
|
+
onClick={() => {
|
|
385
|
+
setHeartbeatChecklist([
|
|
386
|
+
...heartbeatChecklist,
|
|
387
|
+
{
|
|
388
|
+
id: crypto.randomUUID().slice(0, 8),
|
|
389
|
+
instruction: "",
|
|
390
|
+
priority: "medium",
|
|
391
|
+
},
|
|
392
|
+
]);
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
396
|
+
Add item
|
|
397
|
+
</Button>
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
|
|
402
|
+
{/* Active Hours (heartbeat only) */}
|
|
403
|
+
{scheduleType === "heartbeat" && (
|
|
404
|
+
<div className="space-y-2">
|
|
405
|
+
<Label>Active Hours</Label>
|
|
406
|
+
<div className="grid grid-cols-3 gap-2">
|
|
407
|
+
<div className="space-y-1">
|
|
408
|
+
<Label htmlFor="active-start" className="text-xs text-muted-foreground">Start</Label>
|
|
409
|
+
<Input
|
|
410
|
+
id="active-start"
|
|
411
|
+
type="number"
|
|
412
|
+
min={0}
|
|
413
|
+
max={23}
|
|
414
|
+
value={activeHoursStart}
|
|
415
|
+
onChange={(e) => setActiveHoursStart(e.target.value ? Number(e.target.value) : "")}
|
|
416
|
+
placeholder="9"
|
|
417
|
+
/>
|
|
418
|
+
</div>
|
|
419
|
+
<div className="space-y-1">
|
|
420
|
+
<Label htmlFor="active-end" className="text-xs text-muted-foreground">End</Label>
|
|
421
|
+
<Input
|
|
422
|
+
id="active-end"
|
|
423
|
+
type="number"
|
|
424
|
+
min={0}
|
|
425
|
+
max={23}
|
|
426
|
+
value={activeHoursEnd}
|
|
427
|
+
onChange={(e) => setActiveHoursEnd(e.target.value ? Number(e.target.value) : "")}
|
|
428
|
+
placeholder="17"
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
431
|
+
<div className="space-y-1">
|
|
432
|
+
<Label htmlFor="active-tz" className="text-xs text-muted-foreground">Timezone</Label>
|
|
433
|
+
<Input
|
|
434
|
+
id="active-tz"
|
|
435
|
+
value={activeTimezone}
|
|
436
|
+
onChange={(e) => setActiveTimezone(e.target.value)}
|
|
437
|
+
placeholder="UTC"
|
|
438
|
+
/>
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
<p className="text-xs text-muted-foreground">
|
|
442
|
+
Heartbeats only fire within this window. Leave empty for 24/7.
|
|
443
|
+
</p>
|
|
444
|
+
</div>
|
|
445
|
+
)}
|
|
446
|
+
|
|
447
|
+
{/* Daily Budget (heartbeat only) */}
|
|
448
|
+
{scheduleType === "heartbeat" && (
|
|
449
|
+
<div className="space-y-2">
|
|
450
|
+
<Label htmlFor="hb-budget">Daily Budget ($)</Label>
|
|
451
|
+
<Input
|
|
452
|
+
id="hb-budget"
|
|
453
|
+
type="number"
|
|
454
|
+
min={0}
|
|
455
|
+
step={0.01}
|
|
456
|
+
value={heartbeatBudgetPerDay === "" ? "" : (heartbeatBudgetPerDay as number) / 1_000_000}
|
|
457
|
+
onChange={(e) =>
|
|
458
|
+
setHeartbeatBudgetPerDay(
|
|
459
|
+
e.target.value ? Math.round(Number(e.target.value) * 1_000_000) : ""
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
placeholder="Unlimited"
|
|
463
|
+
/>
|
|
464
|
+
<p className="text-xs text-muted-foreground">
|
|
465
|
+
Cap daily heartbeat spend. Leave empty for no limit.
|
|
466
|
+
</p>
|
|
467
|
+
</div>
|
|
468
|
+
)}
|
|
469
|
+
|
|
470
|
+
{/* Prompt (scheduled type) or optional context (heartbeat) */}
|
|
195
471
|
<div className="space-y-2">
|
|
196
|
-
<Label htmlFor="sched-prompt">
|
|
472
|
+
<Label htmlFor="sched-prompt">
|
|
473
|
+
{scheduleType === "heartbeat" ? "Additional Context (optional)" : "Prompt"}
|
|
474
|
+
</Label>
|
|
197
475
|
<Textarea
|
|
198
476
|
id="sched-prompt"
|
|
199
477
|
value={prompt}
|
|
200
478
|
onChange={(e) => setPrompt(e.target.value)}
|
|
201
|
-
placeholder=
|
|
479
|
+
placeholder={
|
|
480
|
+
scheduleType === "heartbeat"
|
|
481
|
+
? "Optional context for the agent when evaluating the checklist"
|
|
482
|
+
: "What the agent does each firing"
|
|
483
|
+
}
|
|
202
484
|
rows={3}
|
|
203
|
-
required
|
|
485
|
+
required={scheduleType === "scheduled"}
|
|
486
|
+
/>
|
|
487
|
+
<p className="text-xs text-muted-foreground">
|
|
488
|
+
{scheduleType === "heartbeat"
|
|
489
|
+
? "Extra instructions appended to the heartbeat evaluation"
|
|
490
|
+
: "Instructions for each execution"}
|
|
491
|
+
</p>
|
|
492
|
+
</div>
|
|
493
|
+
|
|
494
|
+
{/* Natural Language Schedule Input */}
|
|
495
|
+
<div className="space-y-2">
|
|
496
|
+
<Label className="flex items-center gap-1.5">
|
|
497
|
+
<Sparkles className="h-3.5 w-3.5 text-muted-foreground" />
|
|
498
|
+
Describe your schedule
|
|
499
|
+
</Label>
|
|
500
|
+
<Input
|
|
501
|
+
value={nlInput}
|
|
502
|
+
onChange={(e) => handleNlInputChange(e.target.value)}
|
|
503
|
+
placeholder="e.g., every weekday at 9am"
|
|
204
504
|
/>
|
|
505
|
+
{nlParsing && (
|
|
506
|
+
<p className="text-xs text-muted-foreground">Parsing...</p>
|
|
507
|
+
)}
|
|
508
|
+
{nlResult && !nlParsing && (
|
|
509
|
+
<div className="rounded-lg border p-3 space-y-1.5 bg-surface-2">
|
|
510
|
+
<div className="flex items-center gap-1.5">
|
|
511
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-600" />
|
|
512
|
+
<span className="text-sm font-medium">{nlResult.description}</span>
|
|
513
|
+
</div>
|
|
514
|
+
<p className="text-xs text-muted-foreground font-mono">
|
|
515
|
+
{nlResult.cronExpression}
|
|
516
|
+
</p>
|
|
517
|
+
{nlResult.nextFireTimes.length > 0 && (
|
|
518
|
+
<div className="text-xs text-muted-foreground space-y-0.5">
|
|
519
|
+
<p className="font-medium">Next fires:</p>
|
|
520
|
+
{nlResult.nextFireTimes.map((t, i) => (
|
|
521
|
+
<p key={i}>{new Date(t).toLocaleString()}</p>
|
|
522
|
+
))}
|
|
523
|
+
</div>
|
|
524
|
+
)}
|
|
525
|
+
{nlResult.confidence < 1.0 && (
|
|
526
|
+
<p className="text-xs text-amber-600">
|
|
527
|
+
Confidence: {Math.round(nlResult.confidence * 100)}%
|
|
528
|
+
{nlResult.confidence < 0.8 && " — not auto-filled, verify below"}
|
|
529
|
+
</p>
|
|
530
|
+
)}
|
|
531
|
+
</div>
|
|
532
|
+
)}
|
|
533
|
+
{nlError && !nlParsing && (
|
|
534
|
+
<div className="flex items-center gap-1.5 text-xs text-destructive">
|
|
535
|
+
<AlertCircle className="h-3.5 w-3.5" />
|
|
536
|
+
{nlError}
|
|
537
|
+
</div>
|
|
538
|
+
)}
|
|
205
539
|
<p className="text-xs text-muted-foreground">
|
|
206
|
-
|
|
540
|
+
Or use the preset/custom interval below
|
|
207
541
|
</p>
|
|
208
542
|
</div>
|
|
209
543
|
|
|
@@ -400,7 +734,12 @@ export function ScheduleForm({
|
|
|
400
734
|
|
|
401
735
|
<Button
|
|
402
736
|
type="submit"
|
|
403
|
-
disabled={
|
|
737
|
+
disabled={
|
|
738
|
+
loading ||
|
|
739
|
+
!name.trim() ||
|
|
740
|
+
(scheduleType === "scheduled" && !prompt.trim()) ||
|
|
741
|
+
(scheduleType === "heartbeat" && heartbeatChecklist.length === 0)
|
|
742
|
+
}
|
|
404
743
|
className="w-full"
|
|
405
744
|
>
|
|
406
745
|
{loading ? "Saving..." : submitLabel}
|
|
@@ -11,7 +11,7 @@ import { ScheduleStatusBadge } from "./schedule-status-badge";
|
|
|
11
11
|
import { ConfirmDialog } from "@/components/shared/confirm-dialog";
|
|
12
12
|
import { EmptyState } from "@/components/shared/empty-state";
|
|
13
13
|
import { describeCron } from "@/lib/schedules/interval-parser";
|
|
14
|
-
import { Clock, Pause, Play, Trash2 } from "lucide-react";
|
|
14
|
+
import { Clock, Heart, Pause, Play, Trash2 } from "lucide-react";
|
|
15
15
|
import { toast } from "sonner";
|
|
16
16
|
|
|
17
17
|
interface Schedule {
|
|
@@ -28,6 +28,8 @@ interface Schedule {
|
|
|
28
28
|
lastFiredAt: string | null;
|
|
29
29
|
nextFireAt: string | null;
|
|
30
30
|
createdAt: string;
|
|
31
|
+
type: "scheduled" | "heartbeat";
|
|
32
|
+
suppressionCount: number;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
interface ScheduleListProps {
|
|
@@ -155,7 +157,10 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
|
|
|
155
157
|
>
|
|
156
158
|
<CardHeader className="pb-2">
|
|
157
159
|
<div className="flex items-center justify-between gap-2 min-w-0">
|
|
158
|
-
<CardTitle className="min-w-0 truncate text-base font-medium">
|
|
160
|
+
<CardTitle className="min-w-0 truncate text-base font-medium flex items-center gap-1.5">
|
|
161
|
+
{sched.type === "heartbeat" && (
|
|
162
|
+
<Heart className="h-3.5 w-3.5 text-rose-500 shrink-0" />
|
|
163
|
+
)}
|
|
159
164
|
{sched.name}
|
|
160
165
|
</CardTitle>
|
|
161
166
|
<ScheduleStatusBadge status={sched.status} />
|
|
@@ -169,6 +174,14 @@ export function ScheduleList({ projects, initialSelectedId }: ScheduleListProps)
|
|
|
169
174
|
{sched.firingCount} firing
|
|
170
175
|
{sched.firingCount !== 1 ? "s" : ""}
|
|
171
176
|
</span>
|
|
177
|
+
{sched.type === "heartbeat" && sched.suppressionCount > 0 && (
|
|
178
|
+
<>
|
|
179
|
+
<span>·</span>
|
|
180
|
+
<span className="text-emerald-600">
|
|
181
|
+
{sched.suppressionCount} suppressed
|
|
182
|
+
</span>
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
172
185
|
{!sched.recurs && (
|
|
173
186
|
<>
|
|
174
187
|
<span>·</span>
|
|
@@ -7,6 +7,7 @@ import type { AuthMethod } from "@/lib/constants/settings";
|
|
|
7
7
|
interface AuthMethodSelectorProps {
|
|
8
8
|
value: AuthMethod;
|
|
9
9
|
onChange: (method: AuthMethod) => void;
|
|
10
|
+
recommendedMethod?: AuthMethod | null;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const methods = [
|
|
@@ -24,7 +25,7 @@ const methods = [
|
|
|
24
25
|
},
|
|
25
26
|
];
|
|
26
27
|
|
|
27
|
-
export function AuthMethodSelector({ value, onChange }: AuthMethodSelectorProps) {
|
|
28
|
+
export function AuthMethodSelector({ value, onChange, recommendedMethod }: AuthMethodSelectorProps) {
|
|
28
29
|
return (
|
|
29
30
|
<div className="space-y-2">
|
|
30
31
|
<p className="text-sm font-medium">Authentication Method</p>
|
|
@@ -58,6 +59,11 @@ export function AuthMethodSelector({ value, onChange }: AuthMethodSelectorProps)
|
|
|
58
59
|
<span className="text-xs text-muted-foreground">
|
|
59
60
|
{method.description}
|
|
60
61
|
</span>
|
|
62
|
+
{recommendedMethod === method.id && !isSelected && (
|
|
63
|
+
<span className="text-[10px] font-medium uppercase tracking-wider text-primary/70 mt-0.5">
|
|
64
|
+
Recommended
|
|
65
|
+
</span>
|
|
66
|
+
)}
|
|
61
67
|
</button>
|
|
62
68
|
);
|
|
63
69
|
})}
|