diff --git a/.gitignore b/.gitignore index 6bd9245c08..a8bee54501 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,10 @@ node_modules .worktrees .sst .env -.idea +.idea/* +!.idea/gradle.xml +!.idea/.run +.idea/*.iml .vscode/* !.vscode/extensions.json !.vscode/launch.json diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000000..f9fe116bbd --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.run/Run JetBrains Plugin.run.xml b/.run/Run JetBrains Plugin.run.xml new file mode 100644 index 0000000000..ac95938207 --- /dev/null +++ b/.run/Run JetBrains Plugin.run.xml @@ -0,0 +1,27 @@ + + + + + + + true + true + false + false + false + false + false + + + \ No newline at end of file diff --git a/README.md b/README.md index 9aee29c489..e77e3fbbf7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@

- VS Code Marketplace - X (Twitter) - Substack Blog - Discord - Reddit + VS Code Marketplace + X (Twitter) + Substack Blog + Discord + Reddit

# 🚀 Kilo @@ -41,7 +41,7 @@ 2. Create your account to access 500+ cutting-edge AI models including Gemini 3 Pro, Claude 4.5 Sonnet & Opus, and GPT-5 – with transparent pricing that matches provider rates exactly. 3. Start coding with AI that adapts to your workflow. Watch our quick-start guide to see Kilo in action: -[![Watch the video](https://img.youtube.com/vi/pqGfYXgrhig/maxresdefault.jpg)](https://youtu.be/pqGfYXgrhig) +Watch the video ## Get Started with the CLI diff --git a/bun.lock b/bun.lock index 0ee8e5ca95..2b6c228e4e 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ }, "packages/app": { "name": "@opencode-ai/app", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@kilocode/kilo-i18n": "workspace:*", "@kilocode/kilo-ui": "workspace:*", @@ -80,7 +80,7 @@ }, "packages/desktop": { "name": "@opencode-ai/desktop", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -113,7 +113,7 @@ }, "packages/desktop-electron": { "name": "@opencode-ai/desktop-electron", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@opencode-ai/app": "workspace:*", "@opencode-ai/ui": "workspace:*", @@ -144,7 +144,7 @@ }, "packages/kilo-docs": { "name": "@kilocode/kilo-docs", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@docsearch/css": "^4", "@docsearch/js": "^4", @@ -173,7 +173,7 @@ }, "packages/kilo-gateway": { "name": "@kilocode/kilo-gateway", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@ai-sdk/anthropic": "2.0.65", "@ai-sdk/openai": "2.0.101", @@ -208,7 +208,7 @@ }, "packages/kilo-i18n": { "name": "@kilocode/kilo-i18n", - "version": "7.1.11", + "version": "7.1.21", "devDependencies": { "@tsconfig/node22": "catalog:", "@types/bun": "catalog:", @@ -216,9 +216,12 @@ "typescript": "catalog:", }, }, + "packages/kilo-jetbrains": { + "name": "@kilocode/kilo-jetbrains", + }, "packages/kilo-telemetry": { "name": "@kilocode/kilo-telemetry", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@kilocode/kilo-gateway": "workspace:*", "@opentelemetry/api": "1.9.0", @@ -238,7 +241,7 @@ }, "packages/kilo-ui": { "name": "@kilocode/kilo-ui", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@kobalte/core": "0.13.11", "@opencode-ai/util": "workspace:*", @@ -273,7 +276,7 @@ }, "packages/kilo-vscode": { "name": "kilo-code", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@kilocode/kilo-i18n": "workspace:*", @@ -326,7 +329,7 @@ }, "packages/opencode": { "name": "@kilocode/cli", - "version": "7.1.11", + "version": "7.1.21", "bin": { "kilo": "./bin/kilo", "kilocode": "./bin/kilo", @@ -404,9 +407,11 @@ "opentui-spinner": "0.0.6", "partial-json": "0.1.7", "remeda": "catalog:", + "rotating-file-stream": "3.2.9", "semver": "^7.6.3", "simple-git": "3.31.1", "solid-js": "catalog:", + "stream-chat": "9.38.0", "strip-ansi": "7.1.2", "tree-sitter-bash": "0.25.0", "turndown": "7.2.0", @@ -452,7 +457,7 @@ }, "packages/plugin": { "name": "@kilocode/plugin", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@kilocode/sdk": "workspace:*", "zod": "catalog:", @@ -466,7 +471,7 @@ }, "packages/script": { "name": "@opencode-ai/script", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "semver": "^7.6.3", }, @@ -477,7 +482,7 @@ }, "packages/sdk/js": { "name": "@kilocode/sdk", - "version": "7.1.11", + "version": "7.1.21", "devDependencies": { "@hey-api/openapi-ts": "0.90.10", "@tsconfig/node22": "catalog:", @@ -488,7 +493,7 @@ }, "packages/storybook": { "name": "@opencode-ai/storybook", - "version": "7.1.11", + "version": "7.1.21", "devDependencies": { "@opencode-ai/ui": "workspace:*", "@solidjs/meta": "catalog:", @@ -511,7 +516,7 @@ }, "packages/ui": { "name": "@opencode-ai/ui", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "@kilocode/sdk": "workspace:*", "@kobalte/core": "catalog:", @@ -558,7 +563,7 @@ }, "packages/util": { "name": "@opencode-ai/util", - "version": "7.1.11", + "version": "7.1.21", "dependencies": { "zod": "catalog:", }, @@ -1224,6 +1229,8 @@ "@kilocode/kilo-i18n": ["@kilocode/kilo-i18n@workspace:packages/kilo-i18n"], + "@kilocode/kilo-jetbrains": ["@kilocode/kilo-jetbrains@workspace:packages/kilo-jetbrains"], + "@kilocode/kilo-telemetry": ["@kilocode/kilo-telemetry@workspace:packages/kilo-telemetry"], "@kilocode/kilo-ui": ["@kilocode/kilo-ui@workspace:packages/kilo-ui"], @@ -2008,6 +2015,8 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + "@types/katex": ["@types/katex@0.16.7", "", {}, "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="], "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], @@ -3296,6 +3305,8 @@ "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + "linkifyjs": ["linkifyjs@4.3.2", "", {}, "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA=="], + "load-json-file": ["load-json-file@7.0.1", "", {}, "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -3842,6 +3853,8 @@ "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + "rotating-file-stream": ["rotating-file-stream@3.2.9", "", {}, "sha512-i9i0KkHh12ryl4xtELg+0gyoFre2PJ9RcQQLzquWsiqygyYsrZLckrqqYrthhnJZGZb4g+KUHtcoWYVq34gaug=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -4022,6 +4035,8 @@ "storybook-solidjs-vite": ["storybook-solidjs-vite@10.0.9", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "^0.6.1", "@storybook/builder-vite": "^10.0.0", "@storybook/global": "^5.0.0", "vite-plugin-solid": "^2.11.8" }, "peerDependencies": { "solid-js": "^1.9.0", "storybook": "^0.0.0-0 || ^10.0.0", "typescript": ">= 4.9.x", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-n6MwWCL9mK/qIaUutE9vhGB0X1I1hVnKin2NL+iVC5oXfAiuaABVZlr/1oEeEypsgCdyDOcbEbhJmDWmaqGpPw=="], + "stream-chat": ["stream-chat@9.38.0", "", { "dependencies": { "@types/jsonwebtoken": "^9.0.8", "@types/ws": "^8.5.14", "axios": "^1.12.2", "base64-js": "^1.5.1", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "jsonwebtoken": "^9.0.3", "linkifyjs": "^4.3.2", "ws": "^8.18.1" } }, "sha512-nyTFKHnhGfk1Op/xuZzPKzM9uNTy4TBma69+ApwGj/UtrK2pT6rSaU0Qy/oAqub+Bh7jR2/5vlV/8FWJ2BObFg=="], + "streamx": ["streamx@2.23.0", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg=="], "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/flake.nix b/flake.nix index ed9a99e8a6..1fa4fae94e 100644 --- a/flake.nix +++ b/flake.nix @@ -150,28 +150,54 @@ ''; in pkgs.mkShell { - packages = with pkgs; [ - bun - nodejs_20 - pkg-config - openssl - git - gh - playwright-driver.browsers - vsce - unzip - gnutar - gzip - patchelf - ripgrep - kilo-dev - kilo-install-bin - kilo-bin - ]; + packages = + with pkgs; + [ + bun + nodejs_20 + pkg-config + openssl + git + gh + playwright-driver.browsers + vsce + unzip + gnutar + gzip + patchelf + ripgrep + jetbrains.jdk + jdk21 + kilo-dev + kilo-install-bin + kilo-bin + ] + ++ lib.optionals stdenv.isLinux [ + libX11 + libXext + libXrender + libXtst + libXi + fontconfig + freetype + ]; shellHook = '' export KILO_ROOT="$PWD" export PLAYWRIGHT_BROWSERS_PATH="${pkgs.playwright-driver.browsers}" export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true + '' + + pkgs.lib.optionalString pkgs.stdenv.isLinux '' + export LD_LIBRARY_PATH="${ + pkgs.lib.makeLibraryPath [ + pkgs.libX11 + pkgs.libXext + pkgs.libXrender + pkgs.libXtst + pkgs.libXi + pkgs.fontconfig + pkgs.freetype + ] + }:$LD_LIBRARY_PATH" ''; }; }); diff --git a/nix/hashes.json b/nix/hashes.json index 80015c1848..fffa19cd00 100644 --- a/nix/hashes.json +++ b/nix/hashes.json @@ -1,8 +1,8 @@ { "nodeModules": { - "x86_64-linux": "sha256-YjB3YfaA79nztpZoYEAlFBakS//G/Guprlqk4BYFgOA=", - "aarch64-linux": "sha256-ru470M8RVwyOPNGr6I4VfApQRHhYFFMPjOBneTRBJtc=", - "aarch64-darwin": "sha256-M6LIPBHnYH9AtIRAKyW44VN71Kb28nztzFteCkL0588=", - "x86_64-darwin": "sha256-HURoot79BFHYwoEp0XqVdI6cRdV7KGvmRRqOf+hUa9Q=" + "x86_64-linux": "sha256-4yd8H4UN7YIhdIQ607UoxSqwsQEKqUWkOifjQ2PEvn0=", + "aarch64-linux": "sha256-EuzzDc/j63eRV8QEFgDmQRIsiSq4ZPr9RJSdB9dH+kI=", + "aarch64-darwin": "sha256-BLCZubMC/n06HsUeO3FsvREwfsmjtiTBseSMDPPZNYM=", + "x86_64-darwin": "sha256-gU1uhExpYJhhVHbspCwCry/DwUiuddRvYl72RZqWzNo=" } } diff --git a/package.json b/package.json index bf3cc81357..f65be458b0 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,6 @@ "@openrouter/ai-sdk-provider@1.5.4": "patches/@openrouter%2Fai-sdk-provider@1.5.4.patch", "ghostty-web@0.3.0": "patches/ghostty-web@0.3.0.patch" }, - "version": "7.1.11", + "version": "7.1.21", "peerDependencies": {} } diff --git a/packages/app/package.json b/packages/app/package.json index a2511c60da..4f328f5e8e 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@opencode-ai/app", - "version": "7.1.11", + "version": "7.1.21", "description": "", "type": "module", "exports": { diff --git a/packages/desktop-electron/package.json b/packages/desktop-electron/package.json index f24b84b437..0479df63eb 100644 --- a/packages/desktop-electron/package.json +++ b/packages/desktop-electron/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop-electron", "private": true, - "version": "7.1.11", + "version": "7.1.21", "type": "module", "license": "MIT", "homepage": "https://opencode.ai", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 75eec5cdc7..ad500440bf 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@opencode-ai/desktop", "private": true, - "version": "7.1.11", + "version": "7.1.21", "type": "module", "license": "MIT", "scripts": { diff --git a/packages/extensions/zed/extension.toml b/packages/extensions/zed/extension.toml index 12b55abae7..4026a0d970 100644 --- a/packages/extensions/zed/extension.toml +++ b/packages/extensions/zed/extension.toml @@ -1,7 +1,7 @@ id = "kilo" name = "Kilo" description = "The open source coding agent." -version = "7.1.11" +version = "7.1.21" schema_version = 1 authors = ["Anomaly"] repository = "https://github.com/Kilo-Org/kilocode" @@ -11,26 +11,26 @@ name = "Kilo" icon = "./icons/opencode.svg" [agent_servers.opencode.targets.darwin-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.11/opencode-darwin-arm64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.21/opencode-darwin-arm64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.darwin-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.11/opencode-darwin-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.21/opencode-darwin-x64.zip" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-aarch64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.11/opencode-linux-arm64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.21/opencode-linux-arm64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.linux-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.11/opencode-linux-x64.tar.gz" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.21/opencode-linux-x64.tar.gz" cmd = "./opencode" args = ["acp"] [agent_servers.opencode.targets.windows-x86_64] -archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.11/opencode-windows-x64.zip" +archive = "https://github.com/Kilo-Org/kilocode/releases/download/v7.1.21/opencode-windows-x64.zip" cmd = "./opencode.exe" args = ["acp"] diff --git a/packages/kilo-docs/components/Tabs.tsx b/packages/kilo-docs/components/Tabs.tsx index 6cc69abda6..ecffe35dc6 100644 --- a/packages/kilo-docs/components/Tabs.tsx +++ b/packages/kilo-docs/components/Tabs.tsx @@ -1,5 +1,7 @@ import React, { useState, useEffect, Children, isValidElement, ReactNode, ReactElement } from "react" +const TAB_SYNC_EVENT = "kilo-tab-select" + interface TabProps { label: string children: ReactNode @@ -39,13 +41,26 @@ export function Tabs({ children }: TabsProps) { setActiveIndex(indexFromHash()) const onHashChange = () => setActiveIndex(indexFromHash()) window.addEventListener("hashchange", onHashChange) - return () => window.removeEventListener("hashchange", onHashChange) + + const onSync = (e: Event) => { + const label = (e as CustomEvent).detail + const found = tabs.findIndex((tab) => tab.props.label === label) + if (found >= 0) setActiveIndex(found) + } + window.addEventListener(TAB_SYNC_EVENT, onSync) + + return () => { + window.removeEventListener("hashchange", onHashChange) + window.removeEventListener(TAB_SYNC_EVENT, onSync) + } }, []) const selectTab = (index: number) => { setActiveIndex(index) - const slug = slugify(tabs[index].props.label) + const label = tabs[index].props.label + const slug = slugify(label) history.replaceState(null, "", `#${slug}`) + window.dispatchEvent(new CustomEvent(TAB_SYNC_EVENT, { detail: label })) } return ( diff --git a/packages/kilo-docs/components/TopNav.tsx b/packages/kilo-docs/components/TopNav.tsx index 3d458227c4..b0b2b15127 100644 --- a/packages/kilo-docs/components/TopNav.tsx +++ b/packages/kilo-docs/components/TopNav.tsx @@ -336,8 +336,9 @@ export function TopNav({ onMobileMenuToggle, isMobileMenuOpen = false, showMobil {/* Announcement banner */}

- We've replatformed our extensions on the new Kilo CLI. - Contribute at Kilo-Org/kilocode. + The all-new Kilo Code extension is here, rebuilt on the{" "} + Kilo CLI for speed, flexibility, and continued access to 500+ models via the Kilo Gateway + →

diff --git a/packages/kilo-docs/components/VideoEmbed.tsx b/packages/kilo-docs/components/VideoEmbed.tsx new file mode 100644 index 0000000000..8b507d6a09 --- /dev/null +++ b/packages/kilo-docs/components/VideoEmbed.tsx @@ -0,0 +1,92 @@ +import React from "react" + +interface VideoEmbedProps { + url: string + title?: string + caption?: string +} + +function extractVideoId(url: string): string | null { + const patterns = [ + /(?:youtube\.com\/watch\?v=)([^&\s]+)/, + /(?:youtube\.com\/embed\/)([^?\s]+)/, + /(?:youtu\.be\/)([^?\s]+)/, + /(?:youtube\.com\/v\/)([^?\s]+)/, + ] + + for (const pattern of patterns) { + const match = url.match(pattern) + if (match) { + return match[1] ?? null + } + } + + return null +} + +export function VideoEmbed({ url, title = "Video", caption }: VideoEmbedProps) { + const videoId = extractVideoId(url) + + if (!videoId) { + return ( +
+ Invalid YouTube URL: {url} +
+ ) + } + + return ( +
+
+