stagent 0.4.0 → 0.5.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 (53) hide show
  1. package/README.md +59 -23
  2. package/dist/cli.js +5 -0
  3. package/docs/.last-generated +1 -1
  4. package/docs/features/chat.md +54 -49
  5. package/docs/features/schedules.md +38 -32
  6. package/docs/features/settings.md +105 -50
  7. package/docs/manifest.json +8 -8
  8. package/docs/superpowers/specs/2026-03-27-chat-screenshot-display-design.md +303 -0
  9. package/package.json +3 -1
  10. package/src/app/api/chat/conversations/[id]/messages/route.ts +3 -2
  11. package/src/app/api/chat/entities/search/route.ts +97 -0
  12. package/src/app/api/documents/[id]/file/route.ts +4 -1
  13. package/src/app/api/projects/[id]/route.ts +119 -9
  14. package/src/app/api/projects/__tests__/delete-project.test.ts +170 -0
  15. package/src/app/api/settings/browser-tools/route.ts +68 -0
  16. package/src/app/settings/page.tsx +2 -0
  17. package/src/components/chat/chat-command-popover.tsx +277 -0
  18. package/src/components/chat/chat-input.tsx +85 -10
  19. package/src/components/chat/chat-message.tsx +9 -3
  20. package/src/components/chat/chat-shell.tsx +29 -5
  21. package/src/components/chat/screenshot-gallery.tsx +96 -0
  22. package/src/components/monitoring/log-entry.tsx +61 -27
  23. package/src/components/projects/project-detail.tsx +15 -2
  24. package/src/components/schedules/schedule-create-sheet.tsx +24 -330
  25. package/src/components/schedules/schedule-detail-sheet.tsx +37 -21
  26. package/src/components/schedules/schedule-edit-sheet.tsx +159 -0
  27. package/src/components/schedules/schedule-form.tsx +410 -0
  28. package/src/components/schedules/schedule-list.tsx +16 -0
  29. package/src/components/settings/browser-tools-section.tsx +247 -0
  30. package/src/components/settings/runtime-timeout-section.tsx +4 -4
  31. package/src/components/shared/command-palette.tsx +1 -30
  32. package/src/components/shared/screenshot-lightbox.tsx +151 -0
  33. package/src/hooks/use-caret-position.ts +104 -0
  34. package/src/hooks/use-chat-autocomplete.ts +290 -0
  35. package/src/lib/agents/__tests__/browser-mcp.test.ts +175 -0
  36. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  37. package/src/lib/agents/browser-mcp.ts +119 -0
  38. package/src/lib/agents/claude-agent.ts +66 -8
  39. package/src/lib/chat/command-data.ts +50 -0
  40. package/src/lib/chat/context-builder.ts +127 -3
  41. package/src/lib/chat/engine.ts +92 -11
  42. package/src/lib/chat/slash-commands.ts +191 -0
  43. package/src/lib/chat/tool-catalog.ts +185 -0
  44. package/src/lib/chat/tools/document-tools.ts +37 -0
  45. package/src/lib/chat/types.ts +11 -1
  46. package/src/lib/constants/settings.ts +4 -0
  47. package/src/lib/data/clear.ts +16 -4
  48. package/src/lib/db/bootstrap.ts +5 -0
  49. package/src/lib/db/migrations/0012_add_screenshot_columns.sql +5 -0
  50. package/src/lib/db/schema.ts +5 -0
  51. package/src/lib/screenshots/__tests__/persist.test.ts +104 -0
  52. package/src/lib/screenshots/persist.ts +114 -0
  53. package/src/lib/utils/stagent-paths.ts +4 -0
@@ -0,0 +1,97 @@
1
+ import { NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { projects, tasks, workflows, documents, schedules } from "@/lib/db/schema";
4
+ import { like, desc } from "drizzle-orm";
5
+ import { listProfiles } from "@/lib/agents/profiles/registry";
6
+
7
+ function formatBytes(bytes: number): string {
8
+ if (bytes < 1024) return `${bytes} B`;
9
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
10
+ return `${(bytes / 1048576).toFixed(1)} MB`;
11
+ }
12
+
13
+ interface EntityResult {
14
+ entityType: string;
15
+ entityId: string;
16
+ label: string;
17
+ status?: string;
18
+ description?: string;
19
+ }
20
+
21
+ export async function GET(request: Request) {
22
+ const url = new URL(request.url);
23
+ const query = url.searchParams.get("q") ?? "";
24
+ const limit = Math.min(parseInt(url.searchParams.get("limit") ?? "10", 10), 20);
25
+
26
+ if (!query.trim()) {
27
+ return NextResponse.json({ results: [] });
28
+ }
29
+
30
+ const pattern = `%${query}%`;
31
+ const perType = Math.max(2, Math.floor(limit / 5));
32
+
33
+ const results: EntityResult[] = [];
34
+
35
+ // Search in parallel across all entity types
36
+ const [projectRows, taskRows, workflowRows, documentRows, scheduleRows] =
37
+ await Promise.all([
38
+ db
39
+ .select({ id: projects.id, name: projects.name, status: projects.status, description: projects.description })
40
+ .from(projects)
41
+ .where(like(projects.name, pattern))
42
+ .orderBy(desc(projects.updatedAt))
43
+ .limit(perType),
44
+ db
45
+ .select({ id: tasks.id, title: tasks.title, status: tasks.status, description: tasks.description })
46
+ .from(tasks)
47
+ .where(like(tasks.title, pattern))
48
+ .orderBy(desc(tasks.updatedAt))
49
+ .limit(perType),
50
+ db
51
+ .select({ id: workflows.id, name: workflows.name, status: workflows.status })
52
+ .from(workflows)
53
+ .where(like(workflows.name, pattern))
54
+ .orderBy(desc(workflows.updatedAt))
55
+ .limit(perType),
56
+ db
57
+ .select({ id: documents.id, name: documents.originalName, status: documents.status, mimeType: documents.mimeType, size: documents.size })
58
+ .from(documents)
59
+ .where(like(documents.originalName, pattern))
60
+ .orderBy(desc(documents.createdAt))
61
+ .limit(perType),
62
+ db
63
+ .select({ id: schedules.id, name: schedules.name, status: schedules.status })
64
+ .from(schedules)
65
+ .where(like(schedules.name, pattern))
66
+ .orderBy(desc(schedules.updatedAt))
67
+ .limit(perType),
68
+ ]);
69
+
70
+ for (const p of projectRows) {
71
+ results.push({ entityType: "project", entityId: p.id, label: p.name, status: p.status, description: p.description?.slice(0, 120) || undefined });
72
+ }
73
+ for (const t of taskRows) {
74
+ results.push({ entityType: "task", entityId: t.id, label: t.title, status: t.status, description: t.description?.slice(0, 120) || undefined });
75
+ }
76
+ for (const w of workflowRows) {
77
+ results.push({ entityType: "workflow", entityId: w.id, label: w.name, status: w.status });
78
+ }
79
+ for (const d of documentRows) {
80
+ results.push({ entityType: "document", entityId: d.id, label: d.name, status: d.status, description: `${d.mimeType}, ${formatBytes(d.size)}` });
81
+ }
82
+ for (const s of scheduleRows) {
83
+ results.push({ entityType: "schedule", entityId: s.id, label: s.name, status: s.status });
84
+ }
85
+
86
+ // Search profiles in-memory (file-based registry)
87
+ const lowerQuery = query.toLowerCase();
88
+ const profileMatches = listProfiles()
89
+ .filter((p) => p.name.toLowerCase().includes(lowerQuery) || p.id.toLowerCase().includes(lowerQuery))
90
+ .slice(0, perType);
91
+
92
+ for (const p of profileMatches) {
93
+ results.push({ entityType: "profile", entityId: p.id, label: p.name });
94
+ }
95
+
96
+ return NextResponse.json({ results: results.slice(0, limit) });
97
+ }
@@ -11,12 +11,14 @@ export async function GET(
11
11
  ) {
12
12
  const { id } = await params;
13
13
  const inline = req.nextUrl.searchParams.get("inline") === "1";
14
+ const thumb = req.nextUrl.searchParams.get("thumb") === "1";
14
15
 
15
16
  const [doc] = await db
16
17
  .select({
17
18
  originalName: documents.originalName,
18
19
  mimeType: documents.mimeType,
19
20
  storagePath: documents.storagePath,
21
+ processedPath: documents.processedPath,
20
22
  })
21
23
  .from(documents)
22
24
  .where(eq(documents.id, id));
@@ -26,7 +28,8 @@ export async function GET(
26
28
  }
27
29
 
28
30
  try {
29
- const data = await readFile(doc.storagePath);
31
+ const filePath = (thumb && doc.processedPath) ? doc.processedPath : doc.storagePath;
32
+ const data = await readFile(filePath);
30
33
  const filename = basename(doc.originalName);
31
34
  const canInline =
32
35
  inline && (doc.mimeType.startsWith("image/") || doc.mimeType === "application/pdf");
@@ -1,7 +1,23 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
- import { projects, tasks } from "@/lib/db/schema";
4
- import { eq } from "drizzle-orm";
3
+ import {
4
+ projects,
5
+ tasks,
6
+ workflows,
7
+ documents,
8
+ schedules,
9
+ agentLogs,
10
+ notifications,
11
+ learnedContext,
12
+ usageLedger,
13
+ environmentSyncOps,
14
+ environmentCheckpoints,
15
+ environmentArtifacts,
16
+ environmentScans,
17
+ chatMessages,
18
+ conversations,
19
+ } from "@/lib/db/schema";
20
+ import { eq, inArray } from "drizzle-orm";
5
21
  import { updateProjectSchema } from "@/lib/validators/project";
6
22
 
7
23
  export async function GET(
@@ -61,12 +77,106 @@ export async function DELETE(
61
77
  return NextResponse.json({ error: "Not found" }, { status: 404 });
62
78
  }
63
79
 
64
- // Disassociate tasks before deleting project (avoids FK constraint failure)
65
- await db
66
- .update(tasks)
67
- .set({ projectId: null, updatedAt: new Date() })
68
- .where(eq(tasks.projectId, id));
80
+ try {
81
+ // Cascade-delete in FK-safe order (children before parents)
82
+ // Follows the same pattern as clear.ts and workflow DELETE
83
+
84
+ // 1. Collect child IDs for nested FK chains
85
+ const taskIds = db
86
+ .select({ id: tasks.id })
87
+ .from(tasks)
88
+ .where(eq(tasks.projectId, id))
89
+ .all()
90
+ .map((r) => r.id);
91
+
92
+ const workflowIds = db
93
+ .select({ id: workflows.id })
94
+ .from(workflows)
95
+ .where(eq(workflows.projectId, id))
96
+ .all()
97
+ .map((r) => r.id);
98
+
99
+ const conversationIds = db
100
+ .select({ id: conversations.id })
101
+ .from(conversations)
102
+ .where(eq(conversations.projectId, id))
103
+ .all()
104
+ .map((r) => r.id);
105
+
106
+ const scanIds = db
107
+ .select({ id: environmentScans.id })
108
+ .from(environmentScans)
109
+ .where(eq(environmentScans.projectId, id))
110
+ .all()
111
+ .map((r) => r.id);
112
+
113
+ const checkpointIds = db
114
+ .select({ id: environmentCheckpoints.id })
115
+ .from(environmentCheckpoints)
116
+ .where(eq(environmentCheckpoints.projectId, id))
117
+ .all()
118
+ .map((r) => r.id);
119
+
120
+ // 2. Environment tables (deepest children first)
121
+ if (checkpointIds.length > 0) {
122
+ db.delete(environmentSyncOps)
123
+ .where(inArray(environmentSyncOps.checkpointId, checkpointIds))
124
+ .run();
125
+ db.delete(environmentCheckpoints)
126
+ .where(inArray(environmentCheckpoints.id, checkpointIds))
127
+ .run();
128
+ }
129
+ if (scanIds.length > 0) {
130
+ db.delete(environmentArtifacts)
131
+ .where(inArray(environmentArtifacts.scanId, scanIds))
132
+ .run();
133
+ db.delete(environmentScans)
134
+ .where(inArray(environmentScans.id, scanIds))
135
+ .run();
136
+ }
69
137
 
70
- await db.delete(projects).where(eq(projects.id, id));
71
- return NextResponse.json({ success: true });
138
+ // 3. Chat tables (messages before conversations)
139
+ if (conversationIds.length > 0) {
140
+ db.delete(chatMessages)
141
+ .where(inArray(chatMessages.conversationId, conversationIds))
142
+ .run();
143
+ db.delete(conversations)
144
+ .where(inArray(conversations.id, conversationIds))
145
+ .run();
146
+ }
147
+
148
+ // 4. Usage ledger (references projectId, workflowId, taskId)
149
+ db.delete(usageLedger).where(eq(usageLedger.projectId, id)).run();
150
+
151
+ // 5. Task children (logs, notifications, documents, learned context)
152
+ if (taskIds.length > 0) {
153
+ db.delete(agentLogs).where(inArray(agentLogs.taskId, taskIds)).run();
154
+ db.delete(notifications)
155
+ .where(inArray(notifications.taskId, taskIds))
156
+ .run();
157
+ db.delete(documents).where(inArray(documents.taskId, taskIds)).run();
158
+ db.delete(learnedContext)
159
+ .where(inArray(learnedContext.sourceTaskId, taskIds))
160
+ .run();
161
+ }
162
+
163
+ // 6. Direct project children
164
+ db.delete(documents).where(eq(documents.projectId, id)).run();
165
+ db.delete(tasks).where(eq(tasks.projectId, id)).run();
166
+ if (workflowIds.length > 0) {
167
+ db.delete(workflows).where(inArray(workflows.id, workflowIds)).run();
168
+ }
169
+ db.delete(schedules).where(eq(schedules.projectId, id)).run();
170
+
171
+ // 7. Finally delete the project
172
+ db.delete(projects).where(eq(projects.id, id)).run();
173
+
174
+ return NextResponse.json({ success: true });
175
+ } catch (err) {
176
+ console.error("Project delete failed:", err);
177
+ return NextResponse.json(
178
+ { error: err instanceof Error ? err.message : "Delete failed" },
179
+ { status: 500 }
180
+ );
181
+ }
72
182
  }
@@ -0,0 +1,170 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import * as schema from "@/lib/db/schema";
5
+
6
+ /**
7
+ * Safety-net regression tests for project cascade deletion.
8
+ *
9
+ * These verify that the DELETE handler in projects/[id]/route.ts
10
+ * properly handles all FK relationships before deleting a project.
11
+ * This prevents the "Failed to delete project" FK constraint error
12
+ * that occurs when related records exist.
13
+ */
14
+ describe("project DELETE cascade coverage", () => {
15
+ const deleteRouteSource = readFileSync(
16
+ join(__dirname, "..", "[id]", "route.ts"),
17
+ "utf-8"
18
+ );
19
+
20
+ // Tables that have a direct projectId FK to projects
21
+ const TABLES_WITH_PROJECT_FK = [
22
+ "tasks",
23
+ "workflows",
24
+ "documents",
25
+ "schedules",
26
+ "usageLedger",
27
+ "environmentScans",
28
+ "environmentCheckpoints",
29
+ "conversations",
30
+ ];
31
+
32
+ // Tables that are indirect children (FK to a table that has projectId)
33
+ // These must be deleted before their parent tables
34
+ const INDIRECT_CHILDREN = [
35
+ { table: "agentLogs", parent: "tasks", via: "taskId" },
36
+ { table: "notifications", parent: "tasks", via: "taskId" },
37
+ { table: "learnedContext", parent: "tasks", via: "sourceTaskId" },
38
+ { table: "chatMessages", parent: "conversations", via: "conversationId" },
39
+ { table: "environmentArtifacts", parent: "environmentScans", via: "scanId" },
40
+ { table: "environmentSyncOps", parent: "environmentCheckpoints", via: "checkpointId" },
41
+ ];
42
+
43
+ it("handles all tables with direct projectId FK", () => {
44
+ const missing = TABLES_WITH_PROJECT_FK.filter(
45
+ (table) => !deleteRouteSource.includes(`db.delete(${table})`)
46
+ );
47
+
48
+ expect(
49
+ missing,
50
+ `Project DELETE route missing cascade for tables: ${missing.join(", ")}. ` +
51
+ `These have projectId FK and will cause constraint violations.`
52
+ ).toEqual([]);
53
+ });
54
+
55
+ it("handles all indirect child tables", () => {
56
+ const missing = INDIRECT_CHILDREN.filter(
57
+ ({ table }) => !deleteRouteSource.includes(`db.delete(${table})`)
58
+ );
59
+
60
+ expect(
61
+ missing.map((m) => `${m.table} (child of ${m.parent} via ${m.via})`),
62
+ `Project DELETE route missing cascade for indirect children. ` +
63
+ `These must be deleted before their parent tables.`
64
+ ).toEqual([]);
65
+ });
66
+
67
+ it("imports all required schema tables", () => {
68
+ const allTables = [
69
+ ...TABLES_WITH_PROJECT_FK,
70
+ ...INDIRECT_CHILDREN.map((c) => c.table),
71
+ ];
72
+
73
+ const missingImports = allTables.filter(
74
+ (table) => !deleteRouteSource.includes(table)
75
+ );
76
+
77
+ expect(
78
+ missingImports,
79
+ `Project DELETE route source does not reference these tables: ${missingImports.join(", ")}`
80
+ ).toEqual([]);
81
+ });
82
+
83
+ it("deletes children before parents (FK-safe order)", () => {
84
+ // Verify that child deletes appear BEFORE parent deletes in the source
85
+ const orderPairs = [
86
+ // chatMessages must come before conversations
87
+ { child: "chatMessages", parent: "conversations" },
88
+ // agentLogs, notifications, documents must come before tasks
89
+ { child: "agentLogs", parent: "tasks" },
90
+ { child: "notifications", parent: "tasks" },
91
+ // environmentArtifacts must come before environmentScans
92
+ { child: "environmentArtifacts", parent: "environmentScans" },
93
+ // environmentSyncOps must come before environmentCheckpoints
94
+ { child: "environmentSyncOps", parent: "environmentCheckpoints" },
95
+ // tasks, workflows, schedules must come before projects
96
+ { child: "tasks", parent: "projects" },
97
+ { child: "workflows", parent: "projects" },
98
+ { child: "schedules", parent: "projects" },
99
+ ];
100
+
101
+ const violations = orderPairs.filter(({ child, parent }) => {
102
+ // Find the LAST occurrence of db.delete(child) and FIRST occurrence of db.delete(parent)
103
+ // within the DELETE function (not the import section)
104
+ const deleteSection = deleteRouteSource.slice(
105
+ deleteRouteSource.indexOf("export async function DELETE")
106
+ );
107
+ const childPos = deleteSection.lastIndexOf(`db.delete(${child})`);
108
+ const parentPos = deleteSection.indexOf(`db.delete(${parent})`);
109
+ // child must appear before parent (lower index)
110
+ return childPos === -1 || parentPos === -1 || childPos > parentPos;
111
+ });
112
+
113
+ expect(
114
+ violations.map((v) => `${v.child} must be deleted before ${v.parent}`),
115
+ `FK-safe ordering violated — children must be deleted before parents`
116
+ ).toEqual([]);
117
+ });
118
+
119
+ it("wraps deletion in try/catch for error handling", () => {
120
+ const deleteSection = deleteRouteSource.slice(
121
+ deleteRouteSource.indexOf("export async function DELETE")
122
+ );
123
+ expect(deleteSection).toContain("try {");
124
+ expect(deleteSection).toContain("catch");
125
+ expect(deleteSection).toContain("status: 500");
126
+ });
127
+
128
+ it("verifies project exists before attempting delete", () => {
129
+ const deleteSection = deleteRouteSource.slice(
130
+ deleteRouteSource.indexOf("export async function DELETE")
131
+ );
132
+ expect(deleteSection).toContain("Not found");
133
+ expect(deleteSection).toContain("status: 404");
134
+ });
135
+
136
+ /**
137
+ * Meta-test: ensures all schema tables with a projectId column
138
+ * are accounted for in TABLES_WITH_PROJECT_FK above.
139
+ * If you add a new table with projectId to schema.ts, this test
140
+ * will fail and remind you to update both the delete handler
141
+ * and this test file.
142
+ */
143
+ it("test coverage includes all schema tables with projectId", () => {
144
+ const tablesWithProjectId = Object.entries(schema)
145
+ .filter(([, value]) => {
146
+ if (
147
+ value == null ||
148
+ typeof value !== "object" ||
149
+ !("getSQL" in (value as Record<string, unknown>))
150
+ ) {
151
+ return false;
152
+ }
153
+ // Check if the table object has a projectId column
154
+ const tableObj = value as Record<string, unknown>;
155
+ return "projectId" in tableObj;
156
+ })
157
+ .map(([name]) => name)
158
+ .filter((name) => name !== "projects"); // Exclude projects itself
159
+
160
+ const untested = tablesWithProjectId.filter(
161
+ (name) => !TABLES_WITH_PROJECT_FK.includes(name)
162
+ );
163
+
164
+ expect(
165
+ untested,
166
+ `New tables with projectId FK not covered in delete test: ${untested.join(", ")}. ` +
167
+ `Add them to TABLES_WITH_PROJECT_FK and update the DELETE handler.`
168
+ ).toEqual([]);
169
+ });
170
+ });
@@ -0,0 +1,68 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { getSetting, setSetting } from "@/lib/settings/helpers";
3
+ import { SETTINGS_KEYS } from "@/lib/constants/settings";
4
+
5
+ export async function GET() {
6
+ const [chromeEnabled, playwrightEnabled, chromeConfig, playwrightConfig] =
7
+ await Promise.all([
8
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
9
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
10
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_CONFIG),
11
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_CONFIG),
12
+ ]);
13
+
14
+ return NextResponse.json({
15
+ chromeDevtoolsEnabled: chromeEnabled === "true",
16
+ playwrightEnabled: playwrightEnabled === "true",
17
+ chromeDevtoolsConfig: chromeConfig ?? "",
18
+ playwrightConfig: playwrightConfig ?? "",
19
+ });
20
+ }
21
+
22
+ export async function POST(req: NextRequest) {
23
+ const body = await req.json();
24
+
25
+ if (body.chromeDevtoolsEnabled !== undefined) {
26
+ await setSetting(
27
+ SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED,
28
+ body.chromeDevtoolsEnabled ? "true" : "false"
29
+ );
30
+ }
31
+
32
+ if (body.playwrightEnabled !== undefined) {
33
+ await setSetting(
34
+ SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED,
35
+ body.playwrightEnabled ? "true" : "false"
36
+ );
37
+ }
38
+
39
+ if (body.chromeDevtoolsConfig !== undefined) {
40
+ await setSetting(
41
+ SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_CONFIG,
42
+ body.chromeDevtoolsConfig
43
+ );
44
+ }
45
+
46
+ if (body.playwrightConfig !== undefined) {
47
+ await setSetting(
48
+ SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_CONFIG,
49
+ body.playwrightConfig
50
+ );
51
+ }
52
+
53
+ // Return updated state
54
+ const [chromeEnabled, playwrightEnabled, chromeConfig, playwrightConfig] =
55
+ await Promise.all([
56
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_ENABLED),
57
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_ENABLED),
58
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_CHROME_DEVTOOLS_CONFIG),
59
+ getSetting(SETTINGS_KEYS.BROWSER_MCP_PLAYWRIGHT_CONFIG),
60
+ ]);
61
+
62
+ return NextResponse.json({
63
+ chromeDevtoolsEnabled: chromeEnabled === "true",
64
+ playwrightEnabled: playwrightEnabled === "true",
65
+ chromeDevtoolsConfig: chromeConfig ?? "",
66
+ playwrightConfig: playwrightConfig ?? "",
67
+ });
68
+ }
@@ -5,6 +5,7 @@ import { DataManagementSection } from "@/components/settings/data-management-sec
5
5
  import { BudgetGuardrailsSection } from "@/components/settings/budget-guardrails-section";
6
6
  import { ChatSettingsSection } from "@/components/settings/chat-settings-section";
7
7
  import { RuntimeTimeoutSection } from "@/components/settings/runtime-timeout-section";
8
+ import { BrowserToolsSection } from "@/components/settings/browser-tools-section";
8
9
  import { PageShell } from "@/components/shared/page-shell";
9
10
 
10
11
  export const dynamic = "force-dynamic";
@@ -17,6 +18,7 @@ export default function SettingsPage() {
17
18
  <OpenAIRuntimeSection />
18
19
  <ChatSettingsSection />
19
20
  <RuntimeTimeoutSection />
21
+ <BrowserToolsSection />
20
22
  <BudgetGuardrailsSection />
21
23
  <PermissionsSections />
22
24
  <DataManagementSection />