stagent 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/README.md +8 -8
  2. package/dist/cli.js +146 -2
  3. package/docs/.coverage-gaps.json +21 -0
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +36 -14
  6. package/docs/features/chat.md +33 -56
  7. package/docs/features/cost-usage.md +14 -10
  8. package/docs/features/dashboard-kanban.md +30 -13
  9. package/docs/features/delivery-channels.md +198 -0
  10. package/docs/features/design-system.md +10 -10
  11. package/docs/features/documents.md +8 -8
  12. package/docs/features/home-workspace.md +20 -15
  13. package/docs/features/inbox-notifications.md +22 -10
  14. package/docs/features/keyboard-navigation.md +11 -11
  15. package/docs/features/monitoring.md +1 -1
  16. package/docs/features/playbook.md +30 -32
  17. package/docs/features/profiles.md +33 -11
  18. package/docs/features/projects.md +2 -2
  19. package/docs/features/provider-runtimes.md +58 -14
  20. package/docs/features/schedules.md +70 -40
  21. package/docs/features/settings.md +74 -46
  22. package/docs/features/shared-components.md +7 -15
  23. package/docs/features/tool-permissions.md +9 -9
  24. package/docs/features/workflows.md +32 -21
  25. package/docs/getting-started.md +33 -9
  26. package/docs/index.md +25 -16
  27. package/docs/journeys/developer.md +124 -207
  28. package/docs/journeys/personal-use.md +70 -79
  29. package/docs/journeys/power-user.md +107 -151
  30. package/docs/journeys/work-use.md +81 -113
  31. package/docs/manifest.json +77 -45
  32. package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
  33. package/docs/use-cases/agency-operator.md +84 -0
  34. package/docs/use-cases/solo-founder.md +75 -0
  35. package/docs/why-stagent.md +59 -0
  36. package/package.json +10 -3
  37. package/src/app/api/channels/[id]/route.ts +103 -0
  38. package/src/app/api/channels/[id]/test/route.ts +52 -0
  39. package/src/app/api/channels/inbound/slack/route.ts +109 -0
  40. package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
  41. package/src/app/api/channels/inbound/telegram/route.ts +76 -0
  42. package/src/app/api/channels/route.ts +71 -0
  43. package/src/app/api/chat/conversations/route.ts +15 -0
  44. package/src/app/api/chat/entities/search/route.ts +46 -31
  45. package/src/app/api/environment/profiles/suggest/route.ts +19 -3
  46. package/src/app/api/environment/scan/route.ts +8 -1
  47. package/src/app/api/handoffs/[id]/route.ts +76 -0
  48. package/src/app/api/handoffs/route.ts +89 -0
  49. package/src/app/api/memory/route.ts +181 -0
  50. package/src/app/api/profiles/[id]/route.ts +16 -1
  51. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  52. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  53. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  54. package/src/app/api/profiles/assist/route.ts +35 -0
  55. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  56. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  57. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  58. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  59. package/src/app/api/profiles/import-repo/route.ts +29 -0
  60. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  61. package/src/app/api/profiles/route.ts +73 -22
  62. package/src/app/api/runtimes/ollama/route.ts +86 -0
  63. package/src/app/api/runtimes/suggest/route.ts +29 -0
  64. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  65. package/src/app/api/schedules/[id]/route.ts +41 -3
  66. package/src/app/api/schedules/parse/route.ts +66 -0
  67. package/src/app/api/schedules/route.ts +71 -12
  68. package/src/app/api/settings/author-default/route.ts +7 -0
  69. package/src/app/api/settings/learning/route.ts +41 -0
  70. package/src/app/api/settings/ollama/route.ts +34 -0
  71. package/src/app/api/settings/providers/route.ts +57 -0
  72. package/src/app/api/settings/routing/route.ts +24 -0
  73. package/src/app/api/settings/web-search/route.ts +28 -0
  74. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  75. package/src/app/documents/page.tsx +3 -0
  76. package/src/app/environment/page.tsx +8 -1
  77. package/src/app/settings/page.tsx +10 -4
  78. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  79. package/src/app/workflows/new/page.tsx +2 -0
  80. package/src/components/chat/chat-command-popover.tsx +22 -19
  81. package/src/components/chat/chat-input.tsx +5 -0
  82. package/src/components/chat/chat-model-selector.tsx +42 -1
  83. package/src/components/chat/chat-shell.tsx +2 -0
  84. package/src/components/dashboard/welcome-landing.tsx +9 -9
  85. package/src/components/environment/artifact-card.tsx +27 -1
  86. package/src/components/environment/environment-dashboard.tsx +50 -2
  87. package/src/components/environment/environment-summary-card.tsx +5 -2
  88. package/src/components/environment/suggested-profiles.tsx +117 -52
  89. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  90. package/src/components/memory/memory-browser.tsx +315 -0
  91. package/src/components/profiles/learned-context-panel.tsx +4 -4
  92. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  93. package/src/components/profiles/profile-browser.tsx +109 -8
  94. package/src/components/profiles/profile-card.tsx +29 -1
  95. package/src/components/profiles/profile-detail-view.tsx +200 -28
  96. package/src/components/profiles/profile-form-view.tsx +220 -82
  97. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  98. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  99. package/src/components/schedules/schedule-create-sheet.tsx +9 -1
  100. package/src/components/schedules/schedule-form.tsx +348 -9
  101. package/src/components/schedules/schedule-list.tsx +15 -2
  102. package/src/components/settings/auth-method-selector.tsx +7 -1
  103. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  104. package/src/components/settings/channels-section.tsx +526 -0
  105. package/src/components/settings/chat-settings-section.tsx +27 -1
  106. package/src/components/settings/data-management-section.tsx +8 -6
  107. package/src/components/settings/learning-context-section.tsx +124 -0
  108. package/src/components/settings/ollama-section.tsx +270 -0
  109. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  110. package/src/components/settings/web-search-section.tsx +101 -0
  111. package/src/components/shared/tag-input.tsx +156 -0
  112. package/src/components/tasks/kanban-board.tsx +32 -0
  113. package/src/components/tasks/kanban-column.tsx +4 -2
  114. package/src/components/tasks/task-card.tsx +1 -0
  115. package/src/components/tasks/task-chip-bar.tsx +6 -1
  116. package/src/components/tasks/task-create-panel.tsx +55 -5
  117. package/src/components/workflows/workflow-form-view.tsx +38 -3
  118. package/src/hooks/use-chat-autocomplete.ts +24 -26
  119. package/src/hooks/use-project-skills.ts +66 -0
  120. package/src/hooks/use-tag-suggestions.ts +31 -0
  121. package/src/instrumentation.ts +4 -1
  122. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  123. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  124. package/src/lib/agents/agentic-loop.ts +235 -0
  125. package/src/lib/agents/browser-mcp.ts +59 -4
  126. package/src/lib/agents/claude-agent.ts +26 -199
  127. package/src/lib/agents/handoff/bus.ts +164 -0
  128. package/src/lib/agents/handoff/governance.ts +47 -0
  129. package/src/lib/agents/handoff/types.ts +16 -0
  130. package/src/lib/agents/learned-context.ts +27 -7
  131. package/src/lib/agents/memory/decay.ts +61 -0
  132. package/src/lib/agents/memory/extractor.ts +181 -0
  133. package/src/lib/agents/memory/retrieval.ts +96 -0
  134. package/src/lib/agents/memory/types.ts +6 -0
  135. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  136. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  137. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  138. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  139. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  140. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  141. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  142. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  143. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  144. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  145. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  146. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  147. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  148. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  149. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  150. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  151. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  152. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  153. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  154. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  155. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  156. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  157. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  158. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  159. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  160. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  162. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  163. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  164. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  165. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  166. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  168. package/src/lib/agents/profiles/registry.ts +130 -6
  169. package/src/lib/agents/profiles/types.ts +28 -0
  170. package/src/lib/agents/router.ts +174 -2
  171. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  172. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  173. package/src/lib/agents/runtime/catalog.ts +57 -2
  174. package/src/lib/agents/runtime/claude.ts +205 -1
  175. package/src/lib/agents/runtime/index.ts +22 -0
  176. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  177. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  178. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  179. package/src/lib/agents/runtime/types.ts +2 -0
  180. package/src/lib/agents/tool-permissions.ts +203 -0
  181. package/src/lib/channels/gateway.ts +321 -0
  182. package/src/lib/channels/poller.ts +268 -0
  183. package/src/lib/channels/registry.ts +90 -0
  184. package/src/lib/channels/slack-adapter.ts +188 -0
  185. package/src/lib/channels/telegram-adapter.ts +218 -0
  186. package/src/lib/channels/types.ts +43 -0
  187. package/src/lib/channels/webhook-adapter.ts +74 -0
  188. package/src/lib/chat/context-builder.ts +22 -2
  189. package/src/lib/chat/engine.ts +95 -13
  190. package/src/lib/chat/ollama-engine.ts +198 -0
  191. package/src/lib/chat/stagent-tools.ts +106 -20
  192. package/src/lib/chat/tool-catalog.ts +24 -0
  193. package/src/lib/chat/tool-registry.ts +90 -0
  194. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  195. package/src/lib/chat/tools/document-tools.ts +7 -7
  196. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  197. package/src/lib/chat/tools/notification-tools.ts +4 -4
  198. package/src/lib/chat/tools/profile-tools.ts +3 -3
  199. package/src/lib/chat/tools/project-tools.ts +3 -3
  200. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  201. package/src/lib/chat/tools/settings-tools.ts +2 -2
  202. package/src/lib/chat/tools/task-tools.ts +66 -11
  203. package/src/lib/chat/tools/usage-tools.ts +2 -2
  204. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  205. package/src/lib/chat/types.ts +11 -5
  206. package/src/lib/constants/known-tools.ts +19 -0
  207. package/src/lib/constants/prose-styles.ts +1 -1
  208. package/src/lib/constants/settings.ts +7 -0
  209. package/src/lib/data/channel-bindings.ts +85 -0
  210. package/src/lib/data/clear.ts +22 -0
  211. package/src/lib/data/profile-test-results.ts +48 -0
  212. package/src/lib/data/seed-data/conversations.ts +196 -0
  213. package/src/lib/data/seed-data/learned-context.ts +99 -0
  214. package/src/lib/data/seed-data/notifications.ts +54 -1
  215. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  216. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  217. package/src/lib/data/seed-data/views.ts +60 -0
  218. package/src/lib/data/seed.ts +51 -0
  219. package/src/lib/db/bootstrap.ts +162 -0
  220. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  221. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  222. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  223. package/src/lib/db/schema.ts +187 -1
  224. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  225. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  226. package/src/lib/environment/auto-scan.ts +48 -0
  227. package/src/lib/environment/data.ts +25 -0
  228. package/src/lib/environment/profile-generator.ts +40 -10
  229. package/src/lib/environment/profile-linker.ts +143 -0
  230. package/src/lib/environment/profile-rules.ts +96 -0
  231. package/src/lib/import/dedup.ts +149 -0
  232. package/src/lib/import/format-adapter.ts +631 -0
  233. package/src/lib/import/github-api.ts +219 -0
  234. package/src/lib/import/repo-scanner.ts +251 -0
  235. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  236. package/src/lib/schedules/active-hours.ts +120 -0
  237. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  238. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  239. package/src/lib/schedules/nlp-parser.ts +357 -0
  240. package/src/lib/schedules/scheduler.ts +218 -3
  241. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  242. package/src/lib/settings/helpers.ts +6 -0
  243. package/src/lib/settings/routing.ts +24 -0
  244. package/src/lib/settings/runtime-setup.ts +28 -1
  245. package/src/lib/usage/ledger.ts +2 -1
  246. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  247. package/src/lib/validators/profile.ts +39 -0
  248. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  249. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  250. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  251. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  252. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -0,0 +1,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() || !prompt.trim()) return;
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
- {/* Prompt */}
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">Prompt</Label>
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="What the agent does each firing"
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
- Instructions for each execution
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={loading || !name.trim() || !prompt.trim()}
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
  })}