stagent 0.1.11 → 0.1.12

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 (81) hide show
  1. package/README.md +35 -4
  2. package/package.json +3 -2
  3. package/src/__tests__/e2e/blueprint.test.ts +63 -0
  4. package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
  5. package/src/__tests__/e2e/helpers.ts +286 -0
  6. package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
  7. package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
  8. package/src/__tests__/e2e/setup.ts +156 -0
  9. package/src/__tests__/e2e/single-task.test.ts +170 -0
  10. package/src/app/api/command-palette/recent/route.ts +41 -18
  11. package/src/app/api/context/batch/route.ts +44 -0
  12. package/src/app/api/permissions/presets/route.ts +80 -0
  13. package/src/app/api/playbook/status/route.ts +15 -0
  14. package/src/app/api/profiles/route.ts +23 -20
  15. package/src/app/api/settings/pricing/route.ts +15 -0
  16. package/src/app/costs/page.tsx +53 -43
  17. package/src/app/playbook/[slug]/page.tsx +76 -0
  18. package/src/app/playbook/page.tsx +54 -0
  19. package/src/app/profiles/page.tsx +7 -4
  20. package/src/app/settings/page.tsx +2 -2
  21. package/src/components/costs/cost-dashboard.tsx +226 -320
  22. package/src/components/dashboard/activity-feed.tsx +6 -2
  23. package/src/components/notifications/batch-proposal-review.tsx +150 -0
  24. package/src/components/notifications/notification-item.tsx +6 -3
  25. package/src/components/notifications/pending-approval-host.tsx +57 -11
  26. package/src/components/playbook/adoption-heatmap.tsx +69 -0
  27. package/src/components/playbook/journey-card.tsx +110 -0
  28. package/src/components/playbook/playbook-action-button.tsx +22 -0
  29. package/src/components/playbook/playbook-browser.tsx +143 -0
  30. package/src/components/playbook/playbook-card.tsx +102 -0
  31. package/src/components/playbook/playbook-detail-view.tsx +223 -0
  32. package/src/components/playbook/playbook-homepage.tsx +142 -0
  33. package/src/components/playbook/playbook-toc.tsx +90 -0
  34. package/src/components/playbook/playbook-updated-badge.tsx +23 -0
  35. package/src/components/playbook/related-docs.tsx +30 -0
  36. package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
  37. package/src/components/profiles/context-proposal-review.tsx +7 -3
  38. package/src/components/profiles/learned-context-panel.tsx +116 -8
  39. package/src/components/profiles/profile-detail-view.tsx +6 -3
  40. package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
  41. package/src/components/settings/api-key-form.tsx +5 -43
  42. package/src/components/settings/auth-config-section.tsx +10 -6
  43. package/src/components/settings/auth-status-badge.tsx +8 -0
  44. package/src/components/settings/budget-guardrails-section.tsx +403 -620
  45. package/src/components/settings/connection-test-control.tsx +63 -0
  46. package/src/components/settings/permissions-section.tsx +85 -75
  47. package/src/components/settings/permissions-sections.tsx +24 -0
  48. package/src/components/settings/presets-section.tsx +159 -0
  49. package/src/components/settings/pricing-registry-panel.tsx +164 -0
  50. package/src/components/shared/app-sidebar.tsx +2 -0
  51. package/src/components/shared/command-palette.tsx +30 -0
  52. package/src/components/shared/light-markdown.tsx +134 -0
  53. package/src/components/workflows/loop-status-view.tsx +8 -4
  54. package/src/components/workflows/workflow-status-view.tsx +16 -9
  55. package/src/lib/agents/learned-context.ts +27 -15
  56. package/src/lib/agents/learning-session.ts +234 -0
  57. package/src/lib/agents/pattern-extractor.ts +19 -0
  58. package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
  59. package/src/lib/agents/profiles/sort.ts +7 -0
  60. package/src/lib/constants/settings.ts +1 -0
  61. package/src/lib/db/schema.ts +3 -0
  62. package/src/lib/docs/adoption.ts +105 -0
  63. package/src/lib/docs/journey-tracker.ts +21 -0
  64. package/src/lib/docs/reader.ts +102 -0
  65. package/src/lib/docs/types.ts +54 -0
  66. package/src/lib/docs/usage-stage.ts +60 -0
  67. package/src/lib/notifications/actionable.ts +18 -10
  68. package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
  69. package/src/lib/settings/budget-guardrails.ts +213 -85
  70. package/src/lib/settings/permission-presets.ts +150 -0
  71. package/src/lib/settings/runtime-setup.ts +71 -0
  72. package/src/lib/usage/__tests__/ledger.test.ts +2 -2
  73. package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
  74. package/src/lib/usage/ledger.ts +1 -1
  75. package/src/lib/usage/pricing-registry.ts +570 -0
  76. package/src/lib/usage/pricing.ts +15 -95
  77. package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
  78. package/src/lib/utils/learned-context-history.ts +150 -0
  79. package/src/lib/validators/__tests__/settings.test.ts +23 -16
  80. package/src/lib/validators/settings.ts +3 -9
  81. package/src/lib/workflows/engine.ts +18 -0
@@ -0,0 +1,80 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import {
4
+ getPreset,
5
+ getActivePresets,
6
+ applyPreset,
7
+ removePreset,
8
+ PRESETS,
9
+ } from "@/lib/settings/permission-presets";
10
+
11
+ const presetSchema = z.object({
12
+ presetId: z.string().min(1),
13
+ });
14
+
15
+ /**
16
+ * GET /api/permissions/presets — list all presets with their active status.
17
+ */
18
+ export async function GET() {
19
+ const activeIds = await getActivePresets();
20
+ const activeSet = new Set(activeIds);
21
+
22
+ const presets = PRESETS.map((p) => ({
23
+ ...p,
24
+ active: activeSet.has(p.id),
25
+ }));
26
+
27
+ return NextResponse.json({ presets });
28
+ }
29
+
30
+ /**
31
+ * POST /api/permissions/presets — enable a preset (adds all its patterns).
32
+ */
33
+ export async function POST(req: NextRequest) {
34
+ const body = await req.json();
35
+ const parsed = presetSchema.safeParse(body);
36
+
37
+ if (!parsed.success) {
38
+ return NextResponse.json(
39
+ { error: "presetId (string) is required" },
40
+ { status: 400 }
41
+ );
42
+ }
43
+
44
+ const preset = getPreset(parsed.data.presetId);
45
+ if (!preset) {
46
+ return NextResponse.json(
47
+ { error: `Unknown preset: ${parsed.data.presetId}` },
48
+ { status: 404 }
49
+ );
50
+ }
51
+
52
+ await applyPreset(parsed.data.presetId);
53
+ return NextResponse.json({ success: true });
54
+ }
55
+
56
+ /**
57
+ * DELETE /api/permissions/presets — disable a preset (removes unique patterns).
58
+ */
59
+ export async function DELETE(req: NextRequest) {
60
+ const body = await req.json();
61
+ const parsed = presetSchema.safeParse(body);
62
+
63
+ if (!parsed.success) {
64
+ return NextResponse.json(
65
+ { error: "presetId (string) is required" },
66
+ { status: 400 }
67
+ );
68
+ }
69
+
70
+ const preset = getPreset(parsed.data.presetId);
71
+ if (!preset) {
72
+ return NextResponse.json(
73
+ { error: `Unknown preset: ${parsed.data.presetId}` },
74
+ { status: 404 }
75
+ );
76
+ }
77
+
78
+ await removePreset(parsed.data.presetId);
79
+ return NextResponse.json({ success: true });
80
+ }
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getDocsLastGenerated } from "@/lib/docs/reader";
3
+ import { getSetting } from "@/lib/settings/helpers";
4
+
5
+ export async function GET() {
6
+ const lastGenerated = getDocsLastGenerated();
7
+ const lastVisit = await getSetting("lastPlaybookVisit");
8
+
9
+ const hasUpdates =
10
+ lastGenerated != null &&
11
+ lastVisit != null &&
12
+ new Date(lastGenerated) > new Date(lastVisit);
13
+
14
+ return NextResponse.json({ hasUpdates });
15
+ }
@@ -1,28 +1,31 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { listProfiles, createProfile, isBuiltin } from "@/lib/agents/profiles/registry";
3
+ import { sortProfilesByName } from "@/lib/agents/profiles/sort";
3
4
  import { ProfileConfigSchema } from "@/lib/validators/profile";
4
5
 
5
6
  export async function GET() {
6
- const profiles = listProfiles().map((p) => ({
7
- id: p.id,
8
- name: p.name,
9
- description: p.description,
10
- domain: p.domain,
11
- tags: p.tags,
12
- skillMd: p.skillMd,
13
- allowedTools: p.allowedTools,
14
- mcpServers: p.mcpServers,
15
- canUseToolPolicy: p.canUseToolPolicy,
16
- maxTurns: p.maxTurns,
17
- outputFormat: p.outputFormat,
18
- version: p.version,
19
- author: p.author,
20
- source: p.source,
21
- tests: p.tests,
22
- supportedRuntimes: p.supportedRuntimes,
23
- runtimeOverrides: p.runtimeOverrides,
24
- isBuiltin: isBuiltin(p.id),
25
- }));
7
+ const profiles = sortProfilesByName(
8
+ listProfiles().map((p) => ({
9
+ id: p.id,
10
+ name: p.name,
11
+ description: p.description,
12
+ domain: p.domain,
13
+ tags: p.tags,
14
+ skillMd: p.skillMd,
15
+ allowedTools: p.allowedTools,
16
+ mcpServers: p.mcpServers,
17
+ canUseToolPolicy: p.canUseToolPolicy,
18
+ maxTurns: p.maxTurns,
19
+ outputFormat: p.outputFormat,
20
+ version: p.version,
21
+ author: p.author,
22
+ source: p.source,
23
+ tests: p.tests,
24
+ supportedRuntimes: p.supportedRuntimes,
25
+ runtimeOverrides: p.runtimeOverrides,
26
+ isBuiltin: isBuiltin(p.id),
27
+ }))
28
+ );
26
29
 
27
30
  return NextResponse.json(profiles);
28
31
  }
@@ -0,0 +1,15 @@
1
+ import { NextResponse } from "next/server";
2
+ import {
3
+ getPricingRegistrySnapshot,
4
+ refreshPricingRegistry,
5
+ } from "@/lib/usage/pricing-registry";
6
+
7
+ export async function GET() {
8
+ const snapshot = await getPricingRegistrySnapshot();
9
+ return NextResponse.json(snapshot);
10
+ }
11
+
12
+ export async function POST() {
13
+ const snapshot = await refreshPricingRegistry();
14
+ return NextResponse.json(snapshot);
15
+ }
@@ -1,4 +1,3 @@
1
- import { listRuntimeCatalog } from "@/lib/agents/runtime/catalog";
2
1
  import { CostDashboard } from "@/components/costs/cost-dashboard";
3
2
  import { getBudgetGuardrailSnapshot } from "@/lib/settings/budget-guardrails";
4
3
  import {
@@ -13,8 +12,6 @@ import {
13
12
 
14
13
  export const dynamic = "force-dynamic";
15
14
 
16
- const runtimeCatalog = listRuntimeCatalog();
17
- const validRuntimeIds = new Set<string>(runtimeCatalog.map((runtime) => runtime.id));
18
15
  const validDateRanges = new Set(["7d", "30d", "90d", "all"]);
19
16
  const validStatuses = new Set<UsageLedgerStatus>([
20
17
  "completed",
@@ -40,10 +37,6 @@ function resolveDateRange(value: string | undefined) {
40
37
  return value && validDateRanges.has(value) ? value : "30d";
41
38
  }
42
39
 
43
- function resolveRuntime(value: string | undefined) {
44
- return value && validRuntimeIds.has(value) ? value : "all";
45
- }
46
-
47
40
  function resolveStatus(value: string | undefined) {
48
41
  return value && validStatuses.has(value as UsageLedgerStatus) ? value : "all";
49
42
  }
@@ -102,25 +95,6 @@ function fillSeries<T extends { day: string }>(
102
95
  return keys.map((key) => values.get(key) ?? 0);
103
96
  }
104
97
 
105
- function findOverallSpend(
106
- statuses: Array<{
107
- scopeId: string;
108
- window: string;
109
- metric: string;
110
- currentValue: number;
111
- }>,
112
- window: "daily" | "monthly"
113
- ) {
114
- return (
115
- statuses.find(
116
- (status) =>
117
- status.scopeId === "overall" &&
118
- status.window === window &&
119
- status.metric === "spend"
120
- )?.currentValue ?? 0
121
- );
122
- }
123
-
124
98
  function buildRuntimeBreakdown(
125
99
  rows: ProviderModelBreakdownEntry[]
126
100
  ): Array<{
@@ -149,9 +123,7 @@ function buildRuntimeBreakdown(
149
123
  for (const row of rows) {
150
124
  const current = totals.get(row.runtimeId) ?? {
151
125
  runtimeId: row.runtimeId,
152
- label:
153
- runtimeCatalog.find((runtime) => runtime.id === row.runtimeId)?.label ??
154
- row.runtimeId,
126
+ label: row.runtimeId,
155
127
  providerId: row.providerId,
156
128
  costMicros: 0,
157
129
  totalTokens: 0,
@@ -191,22 +163,34 @@ export default async function CostsPage({
191
163
  }) {
192
164
  const params = await searchParams;
193
165
  const dateRange = resolveDateRange(toScalar(params.range));
194
- const runtimeId = resolveRuntime(toScalar(params.runtime));
195
166
  const status = resolveStatus(toScalar(params.status));
196
167
  const activityType = resolveActivityType(toScalar(params.activity));
197
-
198
168
  const rangeStart = getRangeStart(dateRange);
199
- const [spendRows30, tokenRows30, monthBreakdown, filteredBreakdown, auditEntries, budgetSnapshot] =
169
+
170
+ const budgetSnapshot = await getBudgetGuardrailSnapshot();
171
+ const configuredRuntimeIds = Object.values(budgetSnapshot.runtimeStates)
172
+ .filter((runtime) => runtime.configured)
173
+ .map((runtime) => runtime.runtimeId);
174
+ const requestedRuntime = toScalar(params.runtime);
175
+ const runtimeId =
176
+ requestedRuntime && configuredRuntimeIds.includes(requestedRuntime as never)
177
+ ? requestedRuntime
178
+ : "all";
179
+
180
+ const [spendRows30, tokenRows30, monthBreakdown, filteredBreakdown, auditEntries] =
200
181
  await Promise.all([
201
182
  getDailySpendTotals(30),
202
183
  getDailyTokenTotals(30),
203
184
  getProviderModelBreakdown({ startedAt: startOfCurrentMonth() }),
204
- getProviderModelBreakdown(
205
- rangeStart ? { startedAt: rangeStart } : undefined
206
- ),
185
+ getProviderModelBreakdown(rangeStart ? { startedAt: rangeStart } : undefined),
207
186
  listUsageAuditEntries({
208
187
  limit: 100,
209
- runtimeIds: runtimeId === "all" ? undefined : [runtimeId],
188
+ runtimeIds:
189
+ runtimeId === "all"
190
+ ? configuredRuntimeIds.length > 0
191
+ ? configuredRuntimeIds
192
+ : undefined
193
+ : [runtimeId],
210
194
  statuses: status === "all" ? undefined : [status as UsageLedgerStatus],
211
195
  activityTypes:
212
196
  activityType === "all"
@@ -214,17 +198,38 @@ export default async function CostsPage({
214
198
  : [activityType as UsageActivityType],
215
199
  startedAt: rangeStart,
216
200
  }),
217
- getBudgetGuardrailSnapshot(),
218
201
  ]);
219
202
 
203
+ const configuredBreakdown = filteredBreakdown.filter((row) =>
204
+ configuredRuntimeIds.length > 0
205
+ ? configuredRuntimeIds.includes(row.runtimeId as never)
206
+ : true
207
+ );
208
+ const configuredMonthBreakdown = monthBreakdown.filter((row) =>
209
+ configuredRuntimeIds.length > 0
210
+ ? configuredRuntimeIds.includes(row.runtimeId as never)
211
+ : true
212
+ );
213
+
220
214
  const spendSeries30 = fillSeries(30, spendRows30, (row) => row.costMicros);
221
215
  const tokenSeries30 = fillSeries(30, tokenRows30, (row) => row.totalTokens);
222
- const runtimeBreakdown = buildRuntimeBreakdown(filteredBreakdown);
223
- const monthTokens = monthBreakdown.reduce(
216
+ const runtimeBreakdown = buildRuntimeBreakdown(configuredBreakdown).map((row) => ({
217
+ ...row,
218
+ label: budgetSnapshot.runtimeStates[row.runtimeId as keyof typeof budgetSnapshot.runtimeStates]
219
+ ?.label ?? row.runtimeId,
220
+ }));
221
+ const monthTokens = configuredMonthBreakdown.reduce(
224
222
  (sum, row) => sum + row.totalTokens,
225
223
  0
226
224
  );
227
225
 
226
+ const overallDaily = budgetSnapshot.statuses.find(
227
+ (status) => status.scopeId === "overall" && status.window === "daily"
228
+ );
229
+ const overallMonthly = budgetSnapshot.statuses.find(
230
+ (status) => status.scopeId === "overall" && status.window === "monthly"
231
+ );
232
+
228
233
  return (
229
234
  <div className="gradient-neutral min-h-screen p-6">
230
235
  <CostDashboard
@@ -235,9 +240,12 @@ export default async function CostsPage({
235
240
  activityType,
236
241
  }}
237
242
  summary={{
238
- todaySpendMicros: findOverallSpend(budgetSnapshot.statuses, "daily"),
239
- monthSpendMicros: findOverallSpend(budgetSnapshot.statuses, "monthly"),
240
- todayTokens: tokenSeries30[tokenSeries30.length - 1] ?? 0,
243
+ monthSpendMicros: overallMonthly?.currentValue ?? 0,
244
+ derivedDailyBudgetMicros: overallDaily?.limitValue ?? 0,
245
+ remainingMonthlyHeadroomMicros: Math.max(
246
+ (overallMonthly?.limitValue ?? 0) - (overallMonthly?.currentValue ?? 0),
247
+ 0
248
+ ),
241
249
  monthTokens,
242
250
  }}
243
251
  trendSeries={{
@@ -247,8 +255,10 @@ export default async function CostsPage({
247
255
  tokens30: tokenSeries30,
248
256
  }}
249
257
  budgetStatuses={budgetSnapshot.statuses}
258
+ runtimeStates={budgetSnapshot.runtimeStates}
259
+ pricing={budgetSnapshot.pricing}
250
260
  runtimeBreakdown={runtimeBreakdown}
251
- modelBreakdown={filteredBreakdown}
261
+ modelBreakdown={configuredBreakdown}
252
262
  auditEntries={auditEntries}
253
263
  />
254
264
  </div>
@@ -0,0 +1,76 @@
1
+ import { notFound } from "next/navigation";
2
+ import { getDocBySlug, getManifest } from "@/lib/docs/reader";
3
+ import { getAdoptionMap } from "@/lib/docs/adoption";
4
+ import { PlaybookDetailView } from "@/components/playbook/playbook-detail-view";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ interface PlaybookDetailProps {
9
+ params: Promise<{ slug: string }>;
10
+ }
11
+
12
+ export async function generateMetadata({ params }: PlaybookDetailProps) {
13
+ const { slug } = await params;
14
+ const doc = getDocBySlug(slug);
15
+ return {
16
+ title: doc
17
+ ? `${(doc.frontmatter.title as string) || slug} | Playbook | Stagent`
18
+ : "Not Found | Playbook",
19
+ };
20
+ }
21
+
22
+ export default async function PlaybookDetailPage({
23
+ params,
24
+ }: PlaybookDetailProps) {
25
+ const { slug } = await params;
26
+ const [doc, manifest, adoptionMap] = await Promise.all([
27
+ getDocBySlug(slug),
28
+ getManifest(),
29
+ getAdoptionMap(),
30
+ ]);
31
+
32
+ if (!doc) notFound();
33
+
34
+ // Find related sections from manifest
35
+ const allSections = [...manifest.sections, ...manifest.journeys];
36
+ const currentSection = allSections.find((s) => s.slug === slug);
37
+
38
+ // Find related docs by shared tags
39
+ const currentTags = new Set(
40
+ (currentSection && "tags" in currentSection
41
+ ? (currentSection as { tags: string[] }).tags
42
+ : (doc.frontmatter.tags as string[]) || []
43
+ ).map((t) => t.toLowerCase())
44
+ );
45
+
46
+ const relatedSections = manifest.sections
47
+ .filter(
48
+ (s) =>
49
+ s.slug !== slug &&
50
+ s.tags.some((t) => currentTags.has(t.toLowerCase()))
51
+ )
52
+ .slice(0, 4);
53
+
54
+ const adoption = Object.fromEntries(adoptionMap);
55
+
56
+ // Collect all known doc slugs so markdown links resolve correctly
57
+ const allSlugs = [
58
+ ...manifest.sections.map((s) => s.slug),
59
+ ...manifest.journeys.map((j) => j.slug),
60
+ "getting-started",
61
+ "index",
62
+ ];
63
+
64
+ return (
65
+ <div className="gradient-twilight min-h-[100dvh] p-4 sm:p-6">
66
+ <div className="surface-page rounded-[28px] border border-border/60 p-6 shadow-[0_18px_48px_oklch(0.12_0.02_260_/_0.08)]">
67
+ <PlaybookDetailView
68
+ doc={doc}
69
+ relatedSections={relatedSections}
70
+ adoption={adoption}
71
+ allSlugs={allSlugs}
72
+ />
73
+ </div>
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,54 @@
1
+ import { getManifest, getDocsLastGenerated } from "@/lib/docs/reader";
2
+ import { getUsageStage } from "@/lib/docs/usage-stage";
3
+ import { getAdoptionMap } from "@/lib/docs/adoption";
4
+ import { getJourneyCompletions } from "@/lib/docs/journey-tracker";
5
+ import { getSetting, setSetting } from "@/lib/settings/helpers";
6
+ import { PlaybookHomepage } from "@/components/playbook/playbook-homepage";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ export const metadata = {
11
+ title: "Playbook | Stagent",
12
+ };
13
+
14
+ export default async function PlaybookPage() {
15
+ const [manifest, stage, adoptionMap, lastGenerated, lastVisit] =
16
+ await Promise.all([
17
+ getManifest(),
18
+ getUsageStage(),
19
+ getAdoptionMap(),
20
+ getDocsLastGenerated(),
21
+ getSetting("lastPlaybookVisit"),
22
+ ]);
23
+
24
+ const journeyCompletions = getJourneyCompletions(
25
+ manifest.journeys,
26
+ adoptionMap
27
+ );
28
+
29
+ // Update last visit timestamp
30
+ await setSetting("lastPlaybookVisit", new Date().toISOString());
31
+
32
+ // Serialize maps for client component
33
+ const adoption = Object.fromEntries(adoptionMap);
34
+ const completions = Object.fromEntries(journeyCompletions);
35
+
36
+ const hasUpdates =
37
+ lastGenerated != null &&
38
+ lastVisit != null &&
39
+ new Date(lastGenerated) > new Date(lastVisit);
40
+
41
+ return (
42
+ <div className="gradient-twilight min-h-[100dvh] p-4 sm:p-6">
43
+ <div className="surface-page rounded-[28px] border border-border/60 p-6 shadow-[0_18px_48px_oklch(0.12_0.02_260_/_0.08)]">
44
+ <PlaybookHomepage
45
+ manifest={manifest}
46
+ stage={stage}
47
+ adoption={adoption}
48
+ journeyCompletions={completions}
49
+ hasUpdates={hasUpdates}
50
+ />
51
+ </div>
52
+ </div>
53
+ );
54
+ }
@@ -1,13 +1,16 @@
1
1
  import { listProfiles, isBuiltin } from "@/lib/agents/profiles/registry";
2
+ import { sortProfilesByName } from "@/lib/agents/profiles/sort";
2
3
  import { ProfileBrowser } from "@/components/profiles/profile-browser";
3
4
 
4
5
  export const dynamic = "force-dynamic";
5
6
 
6
7
  export default async function ProfilesPage() {
7
- const profiles = listProfiles().map((p) => ({
8
- ...p,
9
- isBuiltin: isBuiltin(p.id),
10
- }));
8
+ const profiles = sortProfilesByName(
9
+ listProfiles().map((p) => ({
10
+ ...p,
11
+ isBuiltin: isBuiltin(p.id),
12
+ }))
13
+ );
11
14
 
12
15
  return (
13
16
  <div className="gradient-ocean-mist min-h-[100dvh] p-4 sm:p-6">
@@ -1,6 +1,6 @@
1
1
  import { AuthConfigSection } from "@/components/settings/auth-config-section";
2
2
  import { OpenAIRuntimeSection } from "@/components/settings/openai-runtime-section";
3
- import { PermissionsSection } from "@/components/settings/permissions-section";
3
+ import { PermissionsSections } from "@/components/settings/permissions-sections";
4
4
  import { DataManagementSection } from "@/components/settings/data-management-section";
5
5
  import { BudgetGuardrailsSection } from "@/components/settings/budget-guardrails-section";
6
6
 
@@ -19,7 +19,7 @@ export default function SettingsPage() {
19
19
  <AuthConfigSection />
20
20
  <OpenAIRuntimeSection />
21
21
  <BudgetGuardrailsSection />
22
- <PermissionsSection />
22
+ <PermissionsSections />
23
23
  <DataManagementSection />
24
24
  </div>
25
25
  </div>