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,124 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import { Brain } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Slider } from "@/components/ui/slider";
14
+ import { FormSectionCard } from "@/components/shared/form-section-card";
15
+ import {
16
+ Tooltip,
17
+ TooltipContent,
18
+ TooltipProvider,
19
+ TooltipTrigger,
20
+ } from "@/components/ui/tooltip";
21
+
22
+ const DEFAULT_LIMIT = 8000;
23
+
24
+ export function LearningContextSection() {
25
+ const [contextLimit, setContextLimit] = useState(DEFAULT_LIMIT);
26
+ const [saving, setSaving] = useState(false);
27
+
28
+ const fetchSettings = useCallback(async () => {
29
+ try {
30
+ const res = await fetch("/api/settings/learning");
31
+ if (res.ok) {
32
+ const data = await res.json();
33
+ if (data.contextCharLimit)
34
+ setContextLimit(parseInt(data.contextCharLimit, 10));
35
+ }
36
+ } catch {
37
+ // Use defaults
38
+ }
39
+ }, []);
40
+
41
+ useEffect(() => {
42
+ fetchSettings();
43
+ }, [fetchSettings]);
44
+
45
+ const handleSave = async (value: number) => {
46
+ setSaving(true);
47
+ try {
48
+ await fetch("/api/settings/learning", {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ contextCharLimit: String(value) }),
52
+ });
53
+ toast.success("Context memory limit updated");
54
+ } catch {
55
+ toast.error("Failed to save setting");
56
+ } finally {
57
+ setSaving(false);
58
+ }
59
+ };
60
+
61
+ return (
62
+ <Card>
63
+ <CardHeader>
64
+ <CardTitle>Self-Learning</CardTitle>
65
+ <CardDescription>
66
+ Configure how agents accumulate and manage learned context from task
67
+ executions.
68
+ </CardDescription>
69
+ </CardHeader>
70
+ <CardContent>
71
+ <TooltipProvider>
72
+ <FormSectionCard
73
+ icon={Brain}
74
+ title="Context Memory Limit"
75
+ hint="Maximum characters of learned context stored per agent profile."
76
+ >
77
+ <div className="space-y-3 w-full">
78
+ <div className="flex items-center justify-between text-sm">
79
+ <span className="text-muted-foreground">Focused</span>
80
+ <Tooltip>
81
+ <TooltipTrigger asChild>
82
+ <span className="font-medium tabular-nums cursor-help">
83
+ {(contextLimit / 1000).toFixed(0)}K characters
84
+ </span>
85
+ </TooltipTrigger>
86
+ <TooltipContent side="top" className="max-w-xs">
87
+ Lower limits keep context tight and focused on recent
88
+ patterns. Higher limits preserve more historical knowledge
89
+ but use more of the AI&apos;s context window.
90
+ Auto-summarization kicks in at 75% capacity.
91
+ </TooltipContent>
92
+ </Tooltip>
93
+ <span className="text-muted-foreground">Comprehensive</span>
94
+ </div>
95
+ <div className="relative">
96
+ <div
97
+ className="absolute top-1/2 -translate-y-1/2 h-1.5 rounded-full bg-primary/10"
98
+ style={{
99
+ left: `${((4000 - 2000) / (32000 - 2000)) * 100}%`,
100
+ width: `${((16000 - 4000) / (32000 - 2000)) * 100}%`,
101
+ }}
102
+ />
103
+ <Slider
104
+ value={[contextLimit]}
105
+ min={2000}
106
+ max={32000}
107
+ step={1000}
108
+ disabled={saving}
109
+ onValueChange={(value) => setContextLimit(value[0])}
110
+ onValueCommit={(value) => handleSave(value[0])}
111
+ />
112
+ </div>
113
+ <div className="flex justify-center">
114
+ <span className="text-xs text-muted-foreground">
115
+ Recommended: 4K–16K characters
116
+ </span>
117
+ </div>
118
+ </div>
119
+ </FormSectionCard>
120
+ </TooltipProvider>
121
+ </CardContent>
122
+ </Card>
123
+ );
124
+ }
@@ -0,0 +1,270 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+ import { Download, Loader2, Server } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Button } from "@/components/ui/button";
14
+ import { Input } from "@/components/ui/input";
15
+ import { Label } from "@/components/ui/label";
16
+
17
+ // ── Types ───────────────────────────────────────────────────────────
18
+
19
+ interface OllamaModel {
20
+ name: string;
21
+ size: number;
22
+ modified_at: string;
23
+ }
24
+
25
+ type ConnectionStatus = "idle" | "testing" | "connected" | "failed";
26
+
27
+ // ── Component ───────────────────────────────────────────────────────
28
+
29
+ export function OllamaSection() {
30
+ const [baseUrl, setBaseUrl] = useState("http://localhost:11434");
31
+ const [models, setModels] = useState<OllamaModel[]>([]);
32
+ const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>("idle");
33
+ const [connectionError, setConnectionError] = useState<string | null>(null);
34
+ const [pullModel, setPullModel] = useState("");
35
+ const [pulling, setPulling] = useState(false);
36
+ const [savingUrl, setSavingUrl] = useState(false);
37
+
38
+ // ── Load settings ───────────────────────────────────────────────
39
+
40
+ useEffect(() => {
41
+ async function loadSettings() {
42
+ try {
43
+ const res = await fetch("/api/settings/ollama");
44
+ if (res.ok) {
45
+ const data = await res.json();
46
+ if (data.baseUrl) setBaseUrl(data.baseUrl);
47
+ }
48
+ } catch {
49
+ // Settings not yet saved, use defaults
50
+ }
51
+ }
52
+ loadSettings();
53
+ }, []);
54
+
55
+ // ── Fetch models ──────────────────────────────────────────────
56
+
57
+ const fetchModels = useCallback(async () => {
58
+ try {
59
+ const res = await fetch("/api/runtimes/ollama");
60
+ if (res.ok) {
61
+ const data = await res.json();
62
+ setModels(data.models ?? []);
63
+ setConnectionStatus("connected");
64
+ setConnectionError(null);
65
+ } else {
66
+ const data = await res.json().catch(() => ({}));
67
+ setConnectionStatus("failed");
68
+ setConnectionError(data.error ?? "Failed to connect");
69
+ setModels([]);
70
+ }
71
+ } catch {
72
+ setConnectionStatus("failed");
73
+ setConnectionError("Cannot reach Ollama API");
74
+ setModels([]);
75
+ }
76
+ }, []);
77
+
78
+ // ── Test connection ───────────────────────────────────────────
79
+
80
+ async function handleTestConnection() {
81
+ setConnectionStatus("testing");
82
+ setConnectionError(null);
83
+ await fetchModels();
84
+ }
85
+
86
+ // ── Save base URL ─────────────────────────────────────────────
87
+
88
+ async function handleSaveUrl() {
89
+ setSavingUrl(true);
90
+ try {
91
+ const res = await fetch("/api/settings/ollama", {
92
+ method: "POST",
93
+ headers: { "Content-Type": "application/json" },
94
+ body: JSON.stringify({ baseUrl }),
95
+ });
96
+ if (res.ok) {
97
+ toast.success("Ollama base URL saved");
98
+ // Re-test connection with new URL
99
+ await fetchModels();
100
+ } else {
101
+ toast.error("Failed to save base URL");
102
+ }
103
+ } finally {
104
+ setSavingUrl(false);
105
+ }
106
+ }
107
+
108
+ // ── Pull model ────────────────────────────────────────────────
109
+
110
+ async function handlePullModel() {
111
+ if (!pullModel.trim()) return;
112
+ setPulling(true);
113
+ try {
114
+ const res = await fetch("/api/runtimes/ollama", {
115
+ method: "POST",
116
+ headers: { "Content-Type": "application/json" },
117
+ body: JSON.stringify({ action: "pull", model: pullModel.trim() }),
118
+ });
119
+ if (res.ok) {
120
+ toast.success(`Model "${pullModel.trim()}" pulled successfully`);
121
+ setPullModel("");
122
+ await fetchModels();
123
+ } else {
124
+ const data = await res.json().catch(() => ({}));
125
+ toast.error(data.error ?? "Failed to pull model");
126
+ }
127
+ } finally {
128
+ setPulling(false);
129
+ }
130
+ }
131
+
132
+ // ── Format file size ──────────────────────────────────────────
133
+
134
+ function formatSize(bytes: number): string {
135
+ if (bytes < 1e9) return `${(bytes / 1e6).toFixed(0)} MB`;
136
+ return `${(bytes / 1e9).toFixed(1)} GB`;
137
+ }
138
+
139
+ // ── Connection status dot ─────────────────────────────────────
140
+
141
+ const statusDot =
142
+ connectionStatus === "connected"
143
+ ? "bg-success"
144
+ : connectionStatus === "failed"
145
+ ? "bg-destructive"
146
+ : connectionStatus === "testing"
147
+ ? "bg-warning animate-pulse"
148
+ : "border-2 border-muted-foreground/40";
149
+
150
+ return (
151
+ <Card className="surface-card">
152
+ <CardHeader>
153
+ <CardTitle className="flex items-center gap-2">
154
+ <Server className="h-5 w-5" />
155
+ Ollama (Local Models)
156
+ </CardTitle>
157
+ <CardDescription>
158
+ Run models locally with Ollama — free, private, no API key required.
159
+ </CardDescription>
160
+ </CardHeader>
161
+
162
+ <CardContent className="space-y-4">
163
+ {/* Base URL */}
164
+ <div className="space-y-2">
165
+ <Label htmlFor="ollama-url">Base URL</Label>
166
+ <div className="flex gap-2">
167
+ <Input
168
+ id="ollama-url"
169
+ value={baseUrl}
170
+ onChange={(e) => setBaseUrl(e.target.value)}
171
+ placeholder="http://localhost:11434"
172
+ className="flex-1"
173
+ />
174
+ <Button
175
+ variant="outline"
176
+ size="sm"
177
+ onClick={handleSaveUrl}
178
+ disabled={savingUrl}
179
+ >
180
+ {savingUrl ? <Loader2 className="h-4 w-4 animate-spin" /> : "Save"}
181
+ </Button>
182
+ </div>
183
+ </div>
184
+
185
+ {/* Connection test */}
186
+ <div className="flex items-center gap-3">
187
+ <Button
188
+ variant="outline"
189
+ size="sm"
190
+ onClick={handleTestConnection}
191
+ disabled={connectionStatus === "testing"}
192
+ >
193
+ {connectionStatus === "testing" ? (
194
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
195
+ ) : null}
196
+ Test Connection
197
+ </Button>
198
+
199
+ <div className="flex items-center gap-2">
200
+ <div className={`h-2.5 w-2.5 rounded-full ${statusDot}`} />
201
+ <span className="text-sm text-muted-foreground">
202
+ {connectionStatus === "connected" && `Connected — ${models.length} model${models.length !== 1 ? "s" : ""} available`}
203
+ {connectionStatus === "failed" && (connectionError ?? "Not connected")}
204
+ {connectionStatus === "testing" && "Testing..."}
205
+ {connectionStatus === "idle" && "Not tested"}
206
+ </span>
207
+ </div>
208
+ </div>
209
+
210
+ {/* Available models list */}
211
+ {models.length > 0 && (
212
+ <div className="space-y-1">
213
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
214
+ Available Models
215
+ </p>
216
+ <div className="rounded-xl border border-border/60 divide-y divide-border/40">
217
+ {models.map((m) => (
218
+ <div
219
+ key={m.name}
220
+ className="flex items-center justify-between px-3 py-2"
221
+ >
222
+ <span className="text-sm font-medium">{m.name}</span>
223
+ <span className="text-xs text-muted-foreground">
224
+ {formatSize(m.size)}
225
+ </span>
226
+ </div>
227
+ ))}
228
+ </div>
229
+ <p className="text-xs text-muted-foreground">
230
+ To set a default Ollama model for chat, use the Chat section above.
231
+ </p>
232
+ </div>
233
+ )}
234
+
235
+ {/* Pull model */}
236
+ <div className="space-y-2">
237
+ <Label htmlFor="ollama-pull">Pull a Model</Label>
238
+ <div className="flex gap-2">
239
+ <Input
240
+ id="ollama-pull"
241
+ value={pullModel}
242
+ onChange={(e) => setPullModel(e.target.value)}
243
+ placeholder="e.g., llama3.2, mistral, codellama"
244
+ className="flex-1"
245
+ onKeyDown={(e) => {
246
+ if (e.key === "Enter") handlePullModel();
247
+ }}
248
+ />
249
+ <Button
250
+ variant="outline"
251
+ size="sm"
252
+ onClick={handlePullModel}
253
+ disabled={pulling || !pullModel.trim()}
254
+ >
255
+ {pulling ? (
256
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
257
+ ) : (
258
+ <Download className="mr-2 h-4 w-4" />
259
+ )}
260
+ Pull
261
+ </Button>
262
+ </div>
263
+ <p className="text-xs text-muted-foreground">
264
+ Download models from the Ollama library. This may take several minutes for large models.
265
+ </p>
266
+ </div>
267
+ </CardContent>
268
+ </Card>
269
+ );
270
+ }