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.
- package/README.md +59 -23
- package/dist/cli.js +5 -0
- package/docs/.last-generated +1 -1
- package/docs/features/chat.md +54 -49
- package/docs/features/schedules.md +38 -32
- package/docs/features/settings.md +105 -50
- package/docs/manifest.json +8 -8
- package/docs/superpowers/specs/2026-03-27-chat-screenshot-display-design.md +303 -0
- package/package.json +3 -1
- package/src/app/api/chat/conversations/[id]/messages/route.ts +3 -2
- package/src/app/api/chat/entities/search/route.ts +97 -0
- package/src/app/api/documents/[id]/file/route.ts +4 -1
- package/src/app/api/projects/[id]/route.ts +119 -9
- package/src/app/api/projects/__tests__/delete-project.test.ts +170 -0
- package/src/app/api/settings/browser-tools/route.ts +68 -0
- package/src/app/settings/page.tsx +2 -0
- package/src/components/chat/chat-command-popover.tsx +277 -0
- package/src/components/chat/chat-input.tsx +85 -10
- package/src/components/chat/chat-message.tsx +9 -3
- package/src/components/chat/chat-shell.tsx +29 -5
- package/src/components/chat/screenshot-gallery.tsx +96 -0
- package/src/components/monitoring/log-entry.tsx +61 -27
- package/src/components/projects/project-detail.tsx +15 -2
- package/src/components/schedules/schedule-create-sheet.tsx +24 -330
- package/src/components/schedules/schedule-detail-sheet.tsx +37 -21
- package/src/components/schedules/schedule-edit-sheet.tsx +159 -0
- package/src/components/schedules/schedule-form.tsx +410 -0
- package/src/components/schedules/schedule-list.tsx +16 -0
- package/src/components/settings/browser-tools-section.tsx +247 -0
- package/src/components/settings/runtime-timeout-section.tsx +4 -4
- package/src/components/shared/command-palette.tsx +1 -30
- package/src/components/shared/screenshot-lightbox.tsx +151 -0
- package/src/hooks/use-caret-position.ts +104 -0
- package/src/hooks/use-chat-autocomplete.ts +290 -0
- package/src/lib/agents/__tests__/browser-mcp.test.ts +175 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
- package/src/lib/agents/browser-mcp.ts +119 -0
- package/src/lib/agents/claude-agent.ts +66 -8
- package/src/lib/chat/command-data.ts +50 -0
- package/src/lib/chat/context-builder.ts +127 -3
- package/src/lib/chat/engine.ts +92 -11
- package/src/lib/chat/slash-commands.ts +191 -0
- package/src/lib/chat/tool-catalog.ts +185 -0
- package/src/lib/chat/tools/document-tools.ts +37 -0
- package/src/lib/chat/types.ts +11 -1
- package/src/lib/constants/settings.ts +4 -0
- package/src/lib/data/clear.ts +16 -4
- package/src/lib/db/bootstrap.ts +5 -0
- package/src/lib/db/migrations/0012_add_screenshot_columns.sql +5 -0
- package/src/lib/db/schema.ts +5 -0
- package/src/lib/screenshots/__tests__/persist.test.ts +104 -0
- package/src/lib/screenshots/persist.ts +114 -0
- 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
|
|
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 {
|
|
4
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
|
|
68
|
-
.
|
|
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
|
-
|
|
71
|
-
|
|
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 />
|