Skip to content

Commit d1305a1

Browse files
authored
Merge pull request #95 from better-stack-ai/feat/media-plugin-backend
feat: media library plugin backend
2 parents 24c318b + df4c3ec commit d1305a1

25 files changed

+5075
-71
lines changed

e2e/tests/smoke.chat.spec.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,21 +144,29 @@ test.describe("AI Chat Plugin", () => {
144144
}) => {
145145
// This test verifies that multiple messages in a conversation stay together
146146
// and don't create separate history items (fixes the bug where every message
147-
// created a new conversation)
147+
// created a new conversation).
148+
//
149+
// A unique run ID is embedded in the first message so that conversations
150+
// created by previous retry attempts don't bleed into the sidebar count
151+
// assertion. The in-memory adapter persists state across Playwright retries
152+
// within the same server process, so a fixed message text would cause
153+
// multiple conversations with the same title to accumulate.
154+
const runId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
155+
const firstMessage = `First msg in conv ${runId}`;
148156

149157
await page.goto("/pages/chat");
150158

151159
// Send first message
152160
const input = page.getByPlaceholder("Type a message...");
153-
await input.fill("First message in conversation");
161+
await input.fill(firstMessage);
154162
await page.keyboard.press("Enter");
155163

156164
// Wait for first AI response
157165
await expect(
158166
page
159167
.locator('[data-testid="chat-interface"]')
160168
.locator('[aria-label="AI response"]'),
161-
).toBeVisible({ timeout: 30000 });
169+
).toBeVisible({ timeout: 45000 });
162170

163171
// Wait for navigation to new conversation URL
164172
await page.waitForURL(/\/pages\/chat\/[a-zA-Z0-9]+/, { timeout: 10000 });
@@ -179,21 +187,19 @@ test.describe("AI Chat Plugin", () => {
179187

180188
// Both messages should still be visible in the same conversation
181189
await expect(
182-
page
183-
.locator('[data-testid="chat-interface"]')
184-
.getByText("First message in conversation"),
190+
page.locator('[data-testid="chat-interface"]').getByText(firstMessage),
185191
).toBeVisible({ timeout: 10000 });
186192
await expect(
187193
page
188194
.locator('[data-testid="chat-interface"]')
189195
.getByText("Second message in same conversation"),
190196
).toBeVisible({ timeout: 10000 });
191197

192-
// There should only be ONE conversation in the sidebar with "First message"
193-
// (the title is based on the first message)
194-
const sidebarConversations = page.locator(
195-
'button:has-text("First message in conversation")',
196-
);
198+
// There should be exactly ONE conversation in the sidebar matching this
199+
// run's unique first message. Searching by the unique runId ensures
200+
// conversations from previous retry attempts (which used a different runId)
201+
// are not counted.
202+
const sidebarConversations = page.locator(`button:has-text("${runId}")`);
197203
await expect(sidebarConversations).toHaveCount(1, { timeout: 5000 });
198204
});
199205

packages/stack/build.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export default defineBuildConfig({
4444
"react/jsx-runtime",
4545
"@tanstack/react-query",
4646
"sonner",
47+
// optional peerDependencies (media plugin)
48+
"@vercel/blob",
49+
"@vercel/blob/server",
50+
"@aws-sdk/client-s3",
51+
"@aws-sdk/s3-request-presigner",
4752
// test/build-time deps kept external
4853
"vitest",
4954
"@vitest/runner",
@@ -110,6 +115,8 @@ export default defineBuildConfig({
110115
"./src/plugins/comments/client/components/index.tsx",
111116
"./src/plugins/comments/client/hooks/index.tsx",
112117
"./src/plugins/comments/query-keys.ts",
118+
// media plugin entries
119+
"./src/plugins/media/api/index.ts",
113120
"./src/components/auto-form/index.ts",
114121
"./src/components/stepped-auto-form/index.ts",
115122
"./src/components/multi-select/index.ts",

packages/stack/knip.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"src/plugins/kanban/client/components/index.tsx",
4141
"src/plugins/kanban/client/hooks/index.tsx",
4242
"src/plugins/kanban/query-keys.ts",
43+
"src/plugins/media/api/index.ts",
4344
"build.config.ts",
4445
"vitest.config.mts",
4546
"scripts/build-registry.ts",
@@ -63,6 +64,9 @@
6364
"remark-gfm",
6465
"remark-math",
6566
"tailwindcss",
66-
"@tailwindcss/typography"
67+
"@tailwindcss/typography",
68+
"@aws-sdk/client-s3",
69+
"@aws-sdk/s3-request-presigner",
70+
"@vercel/blob"
6771
]
6872
}

packages/stack/package.json

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,16 @@
414414
}
415415
},
416416
"./plugins/comments/css": "./dist/plugins/comments/style.css",
417+
"./plugins/media/api": {
418+
"import": {
419+
"types": "./dist/plugins/media/api/index.d.ts",
420+
"default": "./dist/plugins/media/api/index.mjs"
421+
},
422+
"require": {
423+
"types": "./dist/plugins/media/api/index.d.cts",
424+
"default": "./dist/plugins/media/api/index.cjs"
425+
}
426+
},
417427
"./plugins/route-docs/client": {
418428
"import": {
419429
"types": "./dist/plugins/route-docs/client/index.d.ts",
@@ -610,6 +620,9 @@
610620
"plugins/comments/query-keys": [
611621
"./dist/plugins/comments/query-keys.d.ts"
612622
],
623+
"plugins/media/api": [
624+
"./dist/plugins/media/api/index.d.ts"
625+
],
613626
"plugins/route-docs/client": [
614627
"./dist/plugins/route-docs/client/index.d.ts"
615628
],
@@ -646,13 +659,17 @@
646659
},
647660
"peerDependencies": {
648661
"@ai-sdk/react": ">=2.0.0",
662+
"@aws-sdk/client-s3": ">=3.0.0",
663+
"@aws-sdk/s3-request-presigner": ">=3.0.0",
649664
"@btst/yar": ">=1.2.0",
650665
"@hookform/resolvers": ">=5.0.0",
651666
"@radix-ui/react-dialog": ">=1.1.0",
652667
"@radix-ui/react-label": ">=2.1.0",
653668
"@radix-ui/react-slot": ">=1.1.0",
654669
"@radix-ui/react-switch": ">=1.1.0",
670+
"@tailwindcss/typography": ">=0.5.0",
655671
"@tanstack/react-query": "^5.0.0",
672+
"@vercel/blob": ">=0.14.0",
656673
"ai": ">=5.0.0",
657674
"better-call": ">=1.3.2",
658675
"class-variance-authority": ">=0.7.0",
@@ -675,25 +692,38 @@
675692
"sonner": ">=2.0.0",
676693
"tailwind-merge": ">=2.6.0",
677694
"tailwindcss": ">=3.0.0",
678-
"@tailwindcss/typography": ">=0.5.0",
679695
"zod": ">=4.2.0"
680696
},
697+
"peerDependenciesMeta": {
698+
"@vercel/blob": {
699+
"optional": true
700+
},
701+
"@aws-sdk/client-s3": {
702+
"optional": true
703+
},
704+
"@aws-sdk/s3-request-presigner": {
705+
"optional": true
706+
}
707+
},
681708
"devDependencies": {
682-
"tsx": "catalog:",
683709
"@ai-sdk/react": "^2.0.94",
710+
"@aws-sdk/client-s3": "^3.1011.0",
711+
"@aws-sdk/s3-request-presigner": "^3.1011.0",
684712
"@btst/adapter-memory": "2.1.1",
685713
"@btst/yar": "1.2.0",
686714
"@types/react": "^19.0.0",
687715
"@types/slug": "^5.0.9",
716+
"@vercel/blob": "^0.27.3",
688717
"@workspace/ui": "workspace:*",
689718
"ai": "^5.0.94",
690719
"better-call": "catalog:",
720+
"knip": "^5.61.2",
691721
"react": "^19.1.1",
692722
"react-dom": "^19.1.1",
693723
"react-error-boundary": "^4.1.2",
694-
"knip": "^5.61.2",
695724
"rollup-plugin-preserve-directives": "0.4.0",
696725
"rollup-plugin-visualizer": "^5.12.0",
726+
"tsx": "catalog:",
697727
"typescript": "catalog:",
698728
"unbuild": "catalog:",
699729
"vitest": "catalog:",
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/**
2+
* Stub for @vercel/blob/server — used in tests only.
3+
* The real module is an optional peer dependency.
4+
*/
5+
export async function handleUpload(_options: unknown): Promise<unknown> {
6+
throw new Error(
7+
"handleUpload is not available in the installed @vercel/blob version. Use a version that exports @vercel/blob/server.",
8+
);
9+
}

0 commit comments

Comments
 (0)