stagent 0.1.11 → 0.1.13

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 (145) hide show
  1. package/README.md +74 -49
  2. package/package.json +3 -2
  3. package/public/readme/cost-usage-list.png +0 -0
  4. package/public/readme/dashboard-bulk-select.png +0 -0
  5. package/public/readme/dashboard-card-edit.png +0 -0
  6. package/public/readme/dashboard-create-form-ai-applied.png +0 -0
  7. package/public/readme/dashboard-create-form-ai-assist.png +0 -0
  8. package/public/readme/dashboard-create-form-empty.png +0 -0
  9. package/public/readme/dashboard-create-form-filled.png +0 -0
  10. package/public/readme/dashboard-filtered.png +0 -0
  11. package/public/readme/dashboard-list.png +0 -0
  12. package/public/readme/dashboard-workflow-confirm.png +0 -0
  13. package/public/readme/home-below-fold.png +0 -0
  14. package/public/readme/home-list.png +0 -0
  15. package/public/readme/inbox-list.png +0 -0
  16. package/public/readme/playbook-list.png +0 -0
  17. package/public/readme/profiles-list.png +0 -0
  18. package/public/readme/settings-list.png +0 -0
  19. package/public/readme/workflows-list.png +0 -0
  20. package/src/__tests__/e2e/blueprint.test.ts +63 -0
  21. package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
  22. package/src/__tests__/e2e/helpers.ts +286 -0
  23. package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
  24. package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
  25. package/src/__tests__/e2e/setup.ts +156 -0
  26. package/src/__tests__/e2e/single-task.test.ts +170 -0
  27. package/src/app/api/command-palette/recent/route.ts +41 -18
  28. package/src/app/api/context/batch/route.ts +44 -0
  29. package/src/app/api/permissions/presets/route.ts +80 -0
  30. package/src/app/api/playbook/status/route.ts +15 -0
  31. package/src/app/api/profiles/route.ts +23 -20
  32. package/src/app/api/settings/pricing/route.ts +15 -0
  33. package/src/app/api/tasks/[id]/route.ts +54 -3
  34. package/src/app/api/workflows/[id]/route.ts +43 -4
  35. package/src/app/api/workflows/[id]/status/route.ts +70 -2
  36. package/src/app/api/workflows/from-assist/route.ts +6 -32
  37. package/src/app/costs/page.tsx +53 -43
  38. package/src/app/dashboard/page.tsx +59 -21
  39. package/src/app/documents/[id]/page.tsx +10 -8
  40. package/src/app/globals.css +11 -0
  41. package/src/app/page.tsx +60 -3
  42. package/src/app/playbook/[slug]/page.tsx +76 -0
  43. package/src/app/playbook/page.tsx +54 -0
  44. package/src/app/profiles/page.tsx +7 -4
  45. package/src/app/settings/page.tsx +2 -2
  46. package/src/app/tasks/[id]/page.tsx +22 -2
  47. package/src/components/costs/cost-dashboard.tsx +226 -320
  48. package/src/components/dashboard/activity-feed.tsx +6 -2
  49. package/src/components/dashboard/greeting.tsx +3 -1
  50. package/src/components/dashboard/priority-queue.tsx +58 -9
  51. package/src/components/dashboard/stats-cards.tsx +16 -2
  52. package/src/components/documents/document-chip-bar.tsx +183 -0
  53. package/src/components/documents/document-content-renderer.tsx +146 -0
  54. package/src/components/documents/document-detail-view.tsx +16 -239
  55. package/src/components/documents/image-zoom-view.tsx +60 -0
  56. package/src/components/documents/smart-extracted-text.tsx +47 -0
  57. package/src/components/documents/utils.ts +70 -0
  58. package/src/components/notifications/batch-proposal-review.tsx +150 -0
  59. package/src/components/notifications/inbox-list.tsx +4 -5
  60. package/src/components/notifications/notification-item.tsx +73 -6
  61. package/src/components/notifications/pending-approval-host.tsx +63 -14
  62. package/src/components/playbook/adoption-heatmap.tsx +69 -0
  63. package/src/components/playbook/journey-card.tsx +110 -0
  64. package/src/components/playbook/playbook-action-button.tsx +22 -0
  65. package/src/components/playbook/playbook-browser.tsx +143 -0
  66. package/src/components/playbook/playbook-card.tsx +102 -0
  67. package/src/components/playbook/playbook-detail-view.tsx +225 -0
  68. package/src/components/playbook/playbook-homepage.tsx +142 -0
  69. package/src/components/playbook/playbook-toc.tsx +90 -0
  70. package/src/components/playbook/playbook-updated-badge.tsx +23 -0
  71. package/src/components/playbook/related-docs.tsx +30 -0
  72. package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
  73. package/src/components/profiles/context-proposal-review.tsx +7 -3
  74. package/src/components/profiles/learned-context-panel.tsx +116 -8
  75. package/src/components/profiles/profile-browser.tsx +1 -0
  76. package/src/components/profiles/profile-card.tsx +16 -8
  77. package/src/components/profiles/profile-detail-view.tsx +12 -4
  78. package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
  79. package/src/components/settings/api-key-form.tsx +5 -43
  80. package/src/components/settings/auth-config-section.tsx +10 -6
  81. package/src/components/settings/auth-status-badge.tsx +8 -0
  82. package/src/components/settings/budget-guardrails-section.tsx +403 -620
  83. package/src/components/settings/connection-test-control.tsx +63 -0
  84. package/src/components/settings/permissions-section.tsx +85 -75
  85. package/src/components/settings/permissions-sections.tsx +24 -0
  86. package/src/components/settings/presets-section.tsx +159 -0
  87. package/src/components/settings/pricing-registry-panel.tsx +164 -0
  88. package/src/components/shared/app-sidebar.tsx +4 -2
  89. package/src/components/shared/command-palette.tsx +30 -0
  90. package/src/components/shared/light-markdown.tsx +134 -0
  91. package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +1 -1
  92. package/src/components/tasks/ai-assist-panel.tsx +108 -78
  93. package/src/components/tasks/content-preview.tsx +2 -1
  94. package/src/components/tasks/kanban-board.tsx +57 -5
  95. package/src/components/tasks/kanban-column.tsx +34 -23
  96. package/src/components/tasks/task-bento-cell.tsx +50 -0
  97. package/src/components/tasks/task-bento-grid.tsx +155 -0
  98. package/src/components/tasks/task-card.tsx +14 -16
  99. package/src/components/tasks/task-chip-bar.tsx +207 -0
  100. package/src/components/tasks/task-detail-view.tsx +42 -190
  101. package/src/components/tasks/task-result-renderer.tsx +33 -0
  102. package/src/components/workflows/blueprint-gallery.tsx +19 -12
  103. package/src/components/workflows/blueprint-preview.tsx +8 -1
  104. package/src/components/workflows/loop-status-view.tsx +2 -4
  105. package/src/components/workflows/swarm-dashboard.tsx +2 -3
  106. package/src/components/workflows/workflow-confirmation-view.tsx +2 -7
  107. package/src/components/workflows/workflow-full-output.tsx +80 -0
  108. package/src/components/workflows/workflow-kanban-card.tsx +121 -0
  109. package/src/components/workflows/workflow-list.tsx +47 -42
  110. package/src/components/workflows/workflow-status-view.tsx +163 -16
  111. package/src/lib/agents/learned-context.ts +27 -15
  112. package/src/lib/agents/learning-session.ts +354 -0
  113. package/src/lib/agents/pattern-extractor.ts +19 -0
  114. package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
  115. package/src/lib/agents/profiles/sort.ts +7 -0
  116. package/src/lib/constants/card-icons.tsx +202 -0
  117. package/src/lib/constants/prose-styles.ts +7 -0
  118. package/src/lib/constants/settings.ts +1 -0
  119. package/src/lib/constants/task-status.ts +3 -0
  120. package/src/lib/db/schema.ts +3 -0
  121. package/src/lib/docs/adoption.ts +105 -0
  122. package/src/lib/docs/journey-tracker.ts +21 -0
  123. package/src/lib/docs/reader.ts +107 -0
  124. package/src/lib/docs/types.ts +54 -0
  125. package/src/lib/docs/usage-stage.ts +60 -0
  126. package/src/lib/documents/context-builder.ts +41 -0
  127. package/src/lib/notifications/actionable.ts +18 -10
  128. package/src/lib/queries/chart-data.ts +20 -1
  129. package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
  130. package/src/lib/settings/budget-guardrails.ts +213 -85
  131. package/src/lib/settings/permission-presets.ts +150 -0
  132. package/src/lib/settings/runtime-setup.ts +71 -0
  133. package/src/lib/usage/__tests__/ledger.test.ts +2 -2
  134. package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
  135. package/src/lib/usage/ledger.ts +1 -1
  136. package/src/lib/usage/pricing-registry.ts +570 -0
  137. package/src/lib/usage/pricing.ts +15 -95
  138. package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
  139. package/src/lib/utils/learned-context-history.ts +150 -0
  140. package/src/lib/validators/__tests__/settings.test.ts +23 -16
  141. package/src/lib/validators/settings.ts +3 -9
  142. package/src/lib/workflows/engine.ts +75 -61
  143. package/src/lib/workflows/types.ts +2 -0
  144. package/tsconfig.json +2 -1
  145. package/src/components/documents/document-preview.tsx +0 -68
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { CheckCircle2, Loader2, XCircle } from "lucide-react";
5
+
6
+ import { Button } from "@/components/ui/button";
7
+
8
+ export interface ConnectionTestResult {
9
+ connected: boolean;
10
+ error?: string;
11
+ }
12
+
13
+ interface ConnectionTestControlProps {
14
+ onTest: () => Promise<ConnectionTestResult>;
15
+ buttonLabel?: string;
16
+ }
17
+
18
+ export function ConnectionTestControl({
19
+ onTest,
20
+ buttonLabel = "Test Connection",
21
+ }: ConnectionTestControlProps) {
22
+ const [testing, setTesting] = useState(false);
23
+ const [testResult, setTestResult] = useState<ConnectionTestResult | null>(null);
24
+
25
+ async function handleTest() {
26
+ setTesting(true);
27
+ setTestResult(null);
28
+
29
+ try {
30
+ const result = await onTest();
31
+ setTestResult(result);
32
+ } finally {
33
+ setTesting(false);
34
+ }
35
+ }
36
+
37
+ return (
38
+ <div className="flex items-center gap-3">
39
+ <Button variant="outline" size="sm" onClick={handleTest} disabled={testing}>
40
+ {testing && <Loader2 className="mr-1 h-3 w-3 animate-spin" />}
41
+ {buttonLabel}
42
+ </Button>
43
+
44
+ {testResult && (
45
+ <span className="flex items-center gap-1.5 text-sm">
46
+ {testResult.connected ? (
47
+ <>
48
+ <CheckCircle2 className="h-4 w-4 text-success" />
49
+ <span className="text-success">Connected</span>
50
+ </>
51
+ ) : (
52
+ <>
53
+ <XCircle className="h-4 w-4 text-status-failed" />
54
+ <span className="text-status-failed">
55
+ {testResult.error || "Connection failed"}
56
+ </span>
57
+ </>
58
+ )}
59
+ </span>
60
+ )}
61
+ </div>
62
+ );
63
+ }
@@ -1,91 +1,101 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useState } from "react";
3
+ import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
4
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
5
  import { Button } from "@/components/ui/button";
6
6
  import { Badge } from "@/components/ui/badge";
7
7
  import { ShieldCheck, X } from "lucide-react";
8
8
 
9
- export function PermissionsSection() {
10
- const [permissions, setPermissions] = useState<string[]>([]);
11
- const [loading, setLoading] = useState(true);
12
- const [revoking, setRevoking] = useState<string | null>(null);
9
+ export interface PermissionsSectionHandle {
10
+ refresh: () => void;
11
+ }
13
12
 
14
- useEffect(() => {
15
- fetchPermissions();
16
- }, []);
13
+ export const PermissionsSection = forwardRef<PermissionsSectionHandle>(
14
+ function PermissionsSection(_props, ref) {
15
+ const [permissions, setPermissions] = useState<string[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+ const [revoking, setRevoking] = useState<string | null>(null);
17
18
 
18
- async function fetchPermissions() {
19
- try {
20
- const res = await fetch("/api/permissions");
21
- if (res.ok) {
22
- const data = await res.json();
23
- setPermissions(data.permissions ?? []);
19
+ async function fetchPermissions() {
20
+ try {
21
+ const res = await fetch("/api/permissions");
22
+ if (res.ok) {
23
+ const data = await res.json();
24
+ setPermissions(data.permissions ?? []);
25
+ }
26
+ } finally {
27
+ setLoading(false);
24
28
  }
25
- } finally {
26
- setLoading(false);
27
29
  }
28
- }
29
30
 
30
- async function handleRevoke(pattern: string) {
31
- setRevoking(pattern);
32
- try {
33
- const res = await fetch("/api/permissions", {
34
- method: "DELETE",
35
- headers: { "Content-Type": "application/json" },
36
- body: JSON.stringify({ pattern }),
37
- });
38
- if (res.ok) {
39
- setPermissions((prev) => prev.filter((p) => p !== pattern));
31
+ useEffect(() => {
32
+ fetchPermissions();
33
+ }, []);
34
+
35
+ useImperativeHandle(ref, () => ({
36
+ refresh: fetchPermissions,
37
+ }));
38
+
39
+ async function handleRevoke(pattern: string) {
40
+ setRevoking(pattern);
41
+ try {
42
+ const res = await fetch("/api/permissions", {
43
+ method: "DELETE",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify({ pattern }),
46
+ });
47
+ if (res.ok) {
48
+ setPermissions((prev) => prev.filter((p) => p !== pattern));
49
+ }
50
+ } finally {
51
+ setRevoking(null);
40
52
  }
41
- } finally {
42
- setRevoking(null);
43
53
  }
44
- }
45
54
 
46
- return (
47
- <Card className="surface-card">
48
- <CardHeader>
49
- <CardTitle className="flex items-center gap-2">
50
- <ShieldCheck className="h-5 w-5" />
51
- Tool Permissions
52
- </CardTitle>
53
- <CardDescription>
54
- Tools matching these patterns are automatically approved when agents request permission.
55
- Revoke a pattern to require manual approval again.
56
- </CardDescription>
57
- </CardHeader>
58
- <CardContent>
59
- {loading ? (
60
- <p className="text-sm text-muted-foreground">Loading...</p>
61
- ) : permissions.length === 0 ? (
62
- <p className="text-sm text-muted-foreground">
63
- No saved permissions. Click &quot;Always Allow&quot; on a tool permission request in the Inbox to add one.
64
- </p>
65
- ) : (
66
- <div className="flex flex-wrap gap-2">
67
- {permissions.map((pattern) => (
68
- <Badge
69
- key={pattern}
70
- variant="secondary"
71
- className="font-mono text-xs px-3 py-1.5 gap-1.5"
72
- >
73
- {pattern}
74
- <Button
75
- variant="ghost"
76
- size="icon"
77
- className="h-4 w-4 p-0 hover:bg-transparent"
78
- onClick={() => handleRevoke(pattern)}
79
- disabled={revoking === pattern}
80
- aria-label={`Revoke ${pattern}`}
55
+ return (
56
+ <Card className="surface-card">
57
+ <CardHeader>
58
+ <CardTitle className="flex items-center gap-2">
59
+ <ShieldCheck className="h-5 w-5" />
60
+ Tool Permissions
61
+ </CardTitle>
62
+ <CardDescription>
63
+ Tools matching these patterns are automatically approved when agents request permission.
64
+ Revoke a pattern to require manual approval again.
65
+ </CardDescription>
66
+ </CardHeader>
67
+ <CardContent>
68
+ {loading ? (
69
+ <p className="text-sm text-muted-foreground">Loading...</p>
70
+ ) : permissions.length === 0 ? (
71
+ <p className="text-sm text-muted-foreground">
72
+ No saved permissions. Click &quot;Always Allow&quot; on a tool permission request in the Inbox to add one.
73
+ </p>
74
+ ) : (
75
+ <div className="flex flex-wrap gap-2">
76
+ {permissions.map((pattern) => (
77
+ <Badge
78
+ key={pattern}
79
+ variant="secondary"
80
+ className="font-mono text-xs px-3 py-1.5 gap-1.5"
81
81
  >
82
- <X className="h-3 w-3" />
83
- </Button>
84
- </Badge>
85
- ))}
86
- </div>
87
- )}
88
- </CardContent>
89
- </Card>
90
- );
91
- }
82
+ {pattern}
83
+ <Button
84
+ variant="ghost"
85
+ size="icon"
86
+ className="h-4 w-4 p-0 hover:bg-transparent"
87
+ onClick={() => handleRevoke(pattern)}
88
+ disabled={revoking === pattern}
89
+ aria-label={`Revoke ${pattern}`}
90
+ >
91
+ <X className="h-3 w-3" />
92
+ </Button>
93
+ </Badge>
94
+ ))}
95
+ </div>
96
+ )}
97
+ </CardContent>
98
+ </Card>
99
+ );
100
+ }
101
+ );
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import { useCallback, useRef } from "react";
4
+ import { PresetsSection } from "./presets-section";
5
+ import { PermissionsSection } from "./permissions-section";
6
+
7
+ /**
8
+ * Wrapper that coordinates presets and individual permissions.
9
+ * When a preset is toggled, the individual permissions list refreshes.
10
+ */
11
+ export function PermissionsSections() {
12
+ const permissionsRef = useRef<{ refresh: () => void }>(null);
13
+
14
+ const handlePresetChange = useCallback(() => {
15
+ permissionsRef.current?.refresh();
16
+ }, []);
17
+
18
+ return (
19
+ <>
20
+ <PresetsSection onPresetChange={handlePresetChange} />
21
+ <PermissionsSection ref={permissionsRef} />
22
+ </>
23
+ );
24
+ }
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardDescription,
8
+ CardHeader,
9
+ CardTitle,
10
+ } from "@/components/ui/card";
11
+ import { Button } from "@/components/ui/button";
12
+ import { Badge } from "@/components/ui/badge";
13
+ import { Shield, ShieldAlert, ShieldCheck, Zap } from "lucide-react";
14
+
15
+ interface PresetInfo {
16
+ id: string;
17
+ name: string;
18
+ description: string;
19
+ risk: "low" | "medium" | "high";
20
+ patterns: string[];
21
+ active: boolean;
22
+ }
23
+
24
+ const RISK_CONFIG = {
25
+ low: {
26
+ icon: ShieldCheck,
27
+ variant: "success" as const,
28
+ label: "Low Risk",
29
+ },
30
+ medium: {
31
+ icon: Shield,
32
+ variant: "default" as const,
33
+ label: "Medium Risk",
34
+ },
35
+ high: {
36
+ icon: ShieldAlert,
37
+ variant: "destructive" as const,
38
+ label: "High Risk",
39
+ },
40
+ };
41
+
42
+ export function PresetsSection({ onPresetChange }: { onPresetChange?: () => void }) {
43
+ const [presets, setPresets] = useState<PresetInfo[]>([]);
44
+ const [loading, setLoading] = useState(true);
45
+ const [toggling, setToggling] = useState<string | null>(null);
46
+
47
+ useEffect(() => {
48
+ fetchPresets();
49
+ }, []);
50
+
51
+ async function fetchPresets() {
52
+ try {
53
+ const res = await fetch("/api/permissions/presets");
54
+ if (res.ok) {
55
+ const data = await res.json();
56
+ setPresets(data.presets ?? []);
57
+ }
58
+ } finally {
59
+ setLoading(false);
60
+ }
61
+ }
62
+
63
+ async function handleToggle(presetId: string, currentlyActive: boolean) {
64
+ setToggling(presetId);
65
+ try {
66
+ const res = await fetch("/api/permissions/presets", {
67
+ method: currentlyActive ? "DELETE" : "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({ presetId }),
70
+ });
71
+ if (res.ok) {
72
+ await fetchPresets();
73
+ onPresetChange?.();
74
+ }
75
+ } finally {
76
+ setToggling(null);
77
+ }
78
+ }
79
+
80
+ return (
81
+ <Card className="surface-card">
82
+ <CardHeader>
83
+ <CardTitle className="flex items-center gap-2">
84
+ <Zap className="h-5 w-5" />
85
+ Permission Presets
86
+ </CardTitle>
87
+ <CardDescription>
88
+ One-click permission bundles for common use cases. Presets are additive
89
+ — enabling a higher-risk preset includes all lower-risk permissions.
90
+ </CardDescription>
91
+ </CardHeader>
92
+ <CardContent>
93
+ {loading ? (
94
+ <p className="text-sm text-muted-foreground">Loading...</p>
95
+ ) : (
96
+ <div className="grid gap-3 sm:grid-cols-3">
97
+ {presets.map((preset) => {
98
+ const config = RISK_CONFIG[preset.risk];
99
+ const Icon = config.icon;
100
+ const isToggling = toggling === preset.id;
101
+
102
+ return (
103
+ <div
104
+ key={preset.id}
105
+ className={`rounded-lg border p-4 space-y-3 transition-colors ${
106
+ preset.active
107
+ ? "border-primary/50 bg-primary/5"
108
+ : "border-border"
109
+ }`}
110
+ >
111
+ <div className="flex items-start justify-between gap-2">
112
+ <div className="space-y-1">
113
+ <h3 className="font-medium text-sm flex items-center gap-1.5">
114
+ <Icon className="h-4 w-4" />
115
+ {preset.name}
116
+ </h3>
117
+ <Badge variant={config.variant} className="text-[10px]">
118
+ {config.label}
119
+ </Badge>
120
+ </div>
121
+ </div>
122
+
123
+ <p className="text-xs text-muted-foreground leading-relaxed">
124
+ {preset.description}
125
+ </p>
126
+
127
+ <div className="flex flex-wrap gap-1">
128
+ {preset.patterns.map((p) => (
129
+ <span
130
+ key={p}
131
+ className="inline-block font-mono text-[10px] bg-muted px-1.5 py-0.5 rounded"
132
+ >
133
+ {p}
134
+ </span>
135
+ ))}
136
+ </div>
137
+
138
+ <Button
139
+ variant={preset.active ? "outline" : "default"}
140
+ size="sm"
141
+ className="w-full"
142
+ disabled={isToggling}
143
+ onClick={() => handleToggle(preset.id, preset.active)}
144
+ >
145
+ {isToggling
146
+ ? "Updating..."
147
+ : preset.active
148
+ ? "Disable"
149
+ : "Enable"}
150
+ </Button>
151
+ </div>
152
+ );
153
+ })}
154
+ </div>
155
+ )}
156
+ </CardContent>
157
+ </Card>
158
+ );
159
+ }
@@ -0,0 +1,164 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { CalendarClock, RefreshCcw } from "lucide-react";
5
+ import { Badge } from "@/components/ui/badge";
6
+ import { Button } from "@/components/ui/button";
7
+ import type { PricingRegistrySnapshot, PricingRow } from "@/lib/usage/pricing-registry";
8
+ import { toast } from "sonner";
9
+
10
+ interface PricingRegistryPanelProps {
11
+ initialSnapshot: PricingRegistrySnapshot;
12
+ showClaudePlans?: boolean;
13
+ }
14
+
15
+ function formatCurrencyUsd(value: number) {
16
+ return new Intl.NumberFormat("en-US", {
17
+ style: "currency",
18
+ currency: "USD",
19
+ minimumFractionDigits: value >= 100 ? 0 : 2,
20
+ maximumFractionDigits: value >= 100 ? 0 : 2,
21
+ }).format(value);
22
+ }
23
+
24
+ function formatMicrosPerMillion(value: number) {
25
+ return `${formatCurrencyUsd(value / 1_000_000)} / 1M`;
26
+ }
27
+
28
+ function formatUpdatedAt(value: string | null) {
29
+ if (!value) {
30
+ return "Not refreshed yet";
31
+ }
32
+
33
+ return new Date(value).toLocaleString(undefined, {
34
+ dateStyle: "medium",
35
+ timeStyle: "short",
36
+ });
37
+ }
38
+
39
+ function providerRows(
40
+ rows: PricingRow[],
41
+ showClaudePlans: boolean | undefined
42
+ ) {
43
+ return rows.filter((row) =>
44
+ row.visible &&
45
+ (showClaudePlans ? true : row.kind === "api_model")
46
+ );
47
+ }
48
+
49
+ export function PricingRegistryPanel({
50
+ initialSnapshot,
51
+ showClaudePlans = false,
52
+ }: PricingRegistryPanelProps) {
53
+ const [snapshot, setSnapshot] = useState(initialSnapshot);
54
+ const [refreshing, setRefreshing] = useState(false);
55
+
56
+ async function handleRefresh() {
57
+ setRefreshing(true);
58
+ try {
59
+ const response = await fetch("/api/settings/pricing", { method: "POST" });
60
+ const data = (await response.json()) as PricingRegistrySnapshot;
61
+ setSnapshot(data);
62
+ toast.success("Pricing refreshed");
63
+ } catch (error) {
64
+ toast.error(error instanceof Error ? error.message : "Failed to refresh pricing");
65
+ } finally {
66
+ setRefreshing(false);
67
+ }
68
+ }
69
+
70
+ const providerCards = [
71
+ snapshot.providers.anthropic,
72
+ snapshot.providers.openai,
73
+ ];
74
+
75
+ return (
76
+ <div className="surface-card-muted rounded-2xl p-4">
77
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
78
+ <div className="space-y-1">
79
+ <div className="flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
80
+ <CalendarClock className="h-3.5 w-3.5" />
81
+ <span>Latest Pricing</span>
82
+ </div>
83
+ <p className="text-sm text-muted-foreground">
84
+ Official provider pricing with manual refresh and the last known update time.
85
+ </p>
86
+ </div>
87
+ <div className="flex items-center gap-2">
88
+ {snapshot.stale ? (
89
+ <Badge
90
+ variant="outline"
91
+ className="border-status-warning/30 bg-status-warning/10 text-status-warning"
92
+ >
93
+ Stale
94
+ </Badge>
95
+ ) : null}
96
+ <Button
97
+ type="button"
98
+ variant="outline"
99
+ size="sm"
100
+ onClick={handleRefresh}
101
+ disabled={refreshing}
102
+ >
103
+ <RefreshCcw className={`h-3.5 w-3.5 ${refreshing ? "animate-spin" : ""}`} />
104
+ Refresh pricing
105
+ </Button>
106
+ </div>
107
+ </div>
108
+
109
+ <div className="mt-4 grid gap-3 lg:grid-cols-2">
110
+ {providerCards.map((provider) => (
111
+ <div key={provider.providerId} className="surface-panel rounded-xl p-3">
112
+ <div className="flex items-start justify-between gap-3">
113
+ <div>
114
+ <p className="text-sm font-semibold capitalize">{provider.providerId}</p>
115
+ <p className="text-xs text-muted-foreground">
116
+ Updated {formatUpdatedAt(provider.fetchedAtIso)}
117
+ </p>
118
+ </div>
119
+ <Badge variant="outline">{provider.sourceLabel}</Badge>
120
+ </div>
121
+
122
+ <div className="mt-3 space-y-2">
123
+ {providerRows(provider.rows, showClaudePlans && provider.providerId === "anthropic").map(
124
+ (row) => (
125
+ <div
126
+ key={row.key}
127
+ className="flex items-start justify-between gap-3 rounded-lg border border-border/60 bg-background/40 px-3 py-2"
128
+ >
129
+ <div>
130
+ <p className="text-sm font-medium">{row.label}</p>
131
+ <p className="text-xs text-muted-foreground">
132
+ {row.kind === "subscription_plan" ? "Monthly plan" : "Input / output"}
133
+ </p>
134
+ </div>
135
+ <div className="text-right text-sm">
136
+ {row.kind === "subscription_plan" ? (
137
+ <p className="font-medium">
138
+ {formatCurrencyUsd(row.monthlyPriceUsd ?? 0)}
139
+ </p>
140
+ ) : (
141
+ <>
142
+ <p className="font-medium">
143
+ {formatMicrosPerMillion(row.inputCostPerMillionMicros ?? 0)}
144
+ </p>
145
+ <p className="text-xs text-muted-foreground">
146
+ {formatMicrosPerMillion(row.outputCostPerMillionMicros ?? 0)}
147
+ </p>
148
+ </>
149
+ )}
150
+ </div>
151
+ </div>
152
+ )
153
+ )}
154
+ </div>
155
+
156
+ {provider.refreshError ? (
157
+ <p className="mt-3 text-xs text-status-warning">{provider.refreshError}</p>
158
+ ) : null}
159
+ </div>
160
+ ))}
161
+ </div>
162
+ </div>
163
+ );
164
+ }
@@ -8,11 +8,12 @@ import {
8
8
  Inbox,
9
9
  Activity,
10
10
  FolderKanban,
11
- GitBranch,
11
+ Workflow,
12
12
  FileText,
13
13
  Bot,
14
14
  Clock,
15
15
  Wallet,
16
+ BookMarked,
16
17
  Settings,
17
18
  } from "lucide-react";
18
19
  import {
@@ -39,11 +40,12 @@ const navItems = [
39
40
  { title: "Inbox", href: "/inbox", icon: Inbox, badge: true },
40
41
  { title: "Monitor", href: "/monitor", icon: Activity, badge: false },
41
42
  { title: "Projects", href: "/projects", icon: FolderKanban, badge: false },
42
- { title: "Workflows", href: "/workflows", icon: GitBranch, badge: false },
43
+ { title: "Workflows", href: "/workflows", icon: Workflow, badge: false },
43
44
  { title: "Documents", href: "/documents", icon: FileText, badge: false },
44
45
  { title: "Profiles", href: "/profiles", icon: Bot, badge: false },
45
46
  { title: "Schedules", href: "/schedules", icon: Clock, badge: false },
46
47
  { title: "Cost & Usage", href: "/costs", icon: Wallet, badge: false },
48
+ { title: "Playbook", href: "/playbook", icon: BookMarked, badge: false },
47
49
  { title: "Settings", href: "/settings", icon: Settings, badge: false },
48
50
  ];
49
51
 
@@ -29,6 +29,7 @@ import {
29
29
  Moon,
30
30
  CheckCheck,
31
31
  Loader2,
32
+ BookOpen,
32
33
  } from "lucide-react";
33
34
 
34
35
  interface RecentProject {
@@ -43,6 +44,12 @@ interface RecentTask {
43
44
  status: string;
44
45
  }
45
46
 
47
+ interface PlaybookItem {
48
+ slug: string;
49
+ title: string;
50
+ tags: string[];
51
+ }
52
+
46
53
  const navigationItems = [
47
54
  { title: "Home", href: "/", icon: Home, keywords: "landing welcome" },
48
55
  { title: "Dashboard", href: "/dashboard", icon: LayoutDashboard, keywords: "tasks kanban board" },
@@ -54,6 +61,7 @@ const navigationItems = [
54
61
  { title: "Profiles", href: "/profiles", icon: Bot, keywords: "agents configuration" },
55
62
  { title: "Schedules", href: "/schedules", icon: Clock, keywords: "cron recurring timer" },
56
63
  { title: "Cost & Usage", href: "/costs", icon: Wallet, keywords: "spend tokens metering budget analytics" },
64
+ { title: "Playbook", href: "/playbook", icon: BookOpen, keywords: "docs guide documentation help" },
57
65
  { title: "Settings", href: "/settings", icon: Settings, keywords: "preferences configuration" },
58
66
  ];
59
67
 
@@ -81,6 +89,7 @@ export function CommandPalette() {
81
89
  const [open, setOpen] = useState(false);
82
90
  const [recentProjects, setRecentProjects] = useState<RecentProject[]>([]);
83
91
  const [recentTasks, setRecentTasks] = useState<RecentTask[]>([]);
92
+ const [playbookItems, setPlaybookItems] = useState<PlaybookItem[]>([]);
84
93
  const [loadingRecent, setLoadingRecent] = useState(false);
85
94
  const abortRef = useRef<AbortController | null>(null);
86
95
  const router = useRouter();
@@ -114,6 +123,7 @@ export function CommandPalette() {
114
123
  if (data) {
115
124
  setRecentProjects(data.projects);
116
125
  setRecentTasks(data.tasks);
126
+ setPlaybookItems(data.playbook ?? []);
117
127
  }
118
128
  })
119
129
  .catch(() => {
@@ -213,6 +223,26 @@ export function CommandPalette() {
213
223
 
214
224
  <CommandSeparator />
215
225
 
226
+ {/* Playbook */}
227
+ {playbookItems.length > 0 && (
228
+ <>
229
+ <CommandGroup heading="Playbook">
230
+ {playbookItems.map((item) => (
231
+ <CommandItem
232
+ key={`playbook-${item.slug}`}
233
+ value={`playbook-${item.title}`}
234
+ onSelect={() => navigate(`/playbook/${item.slug}`)}
235
+ keywords={["docs", "guide", ...item.tags]}
236
+ >
237
+ <BookOpen className="h-4 w-4" />
238
+ <span className="flex-1 truncate">{item.title}</span>
239
+ </CommandItem>
240
+ ))}
241
+ </CommandGroup>
242
+ <CommandSeparator />
243
+ </>
244
+ )}
245
+
216
246
  {/* Create */}
217
247
  <CommandGroup heading="Create">
218
248
  {createItems.map((item) => (