diff --git a/bun.lock b/bun.lock
index ec75cee..023a317 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,10 +6,11 @@
"name": "commandly",
"dependencies": {
"@ai-sdk/anthropic": "^3.0.69",
- "@ai-sdk/google": "^3.0.62",
+ "@ai-sdk/google": "^3.0.63",
"@ai-sdk/groq": "^3.0.35",
"@ai-sdk/mistral": "^3.0.30",
- "@ai-sdk/openai": "^3.0.52",
+ "@ai-sdk/openai": "^3.0.53",
+ "@ai-sdk/react": "3.0.163",
"@ai-sdk/xai": "^3.0.82",
"@base-ui/react": "^1.4.0",
"@dnd-kit/core": "^6.3.1",
@@ -27,12 +28,13 @@
"@tanstack/react-query": "^5.99.0",
"@tanstack/react-router": "1.168.19",
"@tanstack/react-router-with-query": "1.130.17",
- "@tanstack/react-start": "^1.167.34",
- "ai": "^6.0.158",
+ "@tanstack/react-start": "^1.167.41",
+ "ai": "^6.0.162",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
+ "jsonpath-plus": "^10.4.0",
"lucide-react": "^1.8.0",
"motion": "^12.38.0",
"next-themes": "^0.4.6",
@@ -46,6 +48,7 @@
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"tailwindcss-animate": "^1.0.7",
+ "tokenlens": "^1.3.1",
"tw-animate-css": "^1.4.0",
"use-stick-to-bottom": "^1.1.3",
"zod": "^4.3.6",
@@ -63,10 +66,11 @@
"@types/json-schema": "^7.0.15",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
- "@typescript/native-preview": "^7.0.0-dev.20260408.1",
+ "@typescript/native-preview": "^7.0.0-dev.20260415.1",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "4.1.4",
"ajv": "^8.18.0",
+ "fake-indexeddb": "^6.2.5",
"jsdom": "^29.0.2",
"oxfmt": "^0.45.0",
"oxlint": "^1.60.0",
@@ -88,15 +92,15 @@
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.69", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LshR7X3pFugY0o41G2VKTmg1XoGpSl7uoYWfzk6zjVZLhCfeFiwgpOga+eTV4XY1VVpZwKVqRnkDbIL7K2eH5g=="],
- "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.95", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZmUNNbZl3V42xwQzPaNUi+s8eqR2lnrxf0bvB6YbLXpLjHYv0k2Y78t12cNOfY0bxGeuVVTLyk856uLuQIuXEQ=="],
+ "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.99", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-8/UuzFY8p+T8j4XP/9m841pUb5bhnFt8cecSnJpd2zhBttNZ6GbfjZTmsqnvM/RwJOvzIsdFULZrU+E9QFREsQ=="],
- "@ai-sdk/google": ["@ai-sdk/google@3.0.62", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cC9HAjR5WZxjqGyEJrJqFTlVqyPE9UOFmmGdf5dINaimgfPmzqXYN1qTYEJ+1knbyTVsNMub0KAF5SOqqtO8IQ=="],
+ "@ai-sdk/google": ["@ai-sdk/google@3.0.63", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RfOZWVMYSPu2sPRfGajrauWAZ9BSaRopSn+AszkKWQ1MFj8nhaXvCqRHB5pBQUaHTfZKagvOmMpNfa/s3gPLgQ=="],
"@ai-sdk/groq": ["@ai-sdk/groq@3.0.35", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LXoPwSKaqXst9LyLN2J7gK8n7RldQLbP2zsnBYxXcOsXKrtceksqtbsmGXujvab2TM9FisquAw/ZG2hTbD5vnQ=="],
"@ai-sdk/mistral": ["@ai-sdk/mistral@3.0.30", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-+j4IXRSk9E661cFSafmIr+XHOzwjFagawwzMOlSqwL6U4Sq4PCFLDF+oHbX5NUqNjUL7FD1zi/9lBIfa41pUvw=="],
- "@ai-sdk/openai": ["@ai-sdk/openai@3.0.52", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-4Rr8NCGmfWTz6DCUvixn9UmyZcMatiHn0zWoMzI3JCUe9R1P/vsPOpCBALKoSzVYOjyJnhtnVIbfUKujcS39uw=="],
+ "@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="],
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.41", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kNAGINk71AlOXx10Dq/PXw4t/9XjdK8uxfpVElRwtSFMdeSiLVt58p9TPx4/FJD+hxZuVhvxYj9r42osxWq79g=="],
@@ -104,15 +108,17 @@
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.23", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg=="],
+ "@ai-sdk/react": ["@ai-sdk/react@3.0.163", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.23", "ai": "6.0.161", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" } }, "sha512-UM8BwNx4YFcG1XIBSTepIGx48RXk974qVSplVZc2JPiY86tC4Qpb8trquh5MdtSKzlS6yrUX46n8gS2WZaUIXQ=="],
+
"@ai-sdk/xai": ["@ai-sdk/xai@3.0.82", "", { "dependencies": { "@ai-sdk/openai-compatible": "2.0.41", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-A0VFMufnVf4wODcT3SPQUUzvYXiIO1VhFuXj9r6z/vP4rlo+QRDPw3WSTchcz93ROQWSfBE3I6Szqz342OHi5w=="],
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
- "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.8", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-OISPR9c2uPo23rUdvfEQiLPjoMLOpEeLNnP5iGkxr6tDDxJd3NjD+6fxY0mdaMbIPUjFGL4HFOJqLvow5q4aqQ=="],
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.10", "", { "dependencies": { "@csstools/css-calc": "^3.1.1", "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-02OhhkKtgNRuicQ/nF3TRnGsxL9wp0r3Y7VlKWyOHHGmGyvXv03y+PnymU8FKFJMTjIr1Bk8U2g1HWSLrpAHww=="],
- "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.8", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-erMO6FgtM02dC24NGm0xufMzWz5OF0wXKR7BpvGD973bq/GbmR8/DbxNZbj0YevQ5hlToJaWSVK/G9/NDgGEVw=="],
+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.0.9", "", { "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-r3ElRr7y8ucyN2KdICwGsmj19RoN13CLCa/pvGydghWK6ZzeKQ+TcDjVdtEZz2ElpndM5jXw//B9CEee0mWnVg=="],
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
@@ -198,13 +204,13 @@
"@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
- "@csstools/css-calc": ["@csstools/css-calc@3.1.1", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ=="],
+ "@csstools/css-calc": ["@csstools/css-calc@3.2.0", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w=="],
- "@csstools/css-color-parser": ["@csstools/css-color-parser@4.0.2", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.1.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw=="],
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.0", "", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.0" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ=="],
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
- "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.2", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA=="],
+ "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.3", "", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg=="],
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
@@ -330,6 +336,10 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+ "@jsep-plugin/assignment": ["@jsep-plugin/assignment@1.3.0", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ=="],
+
+ "@jsep-plugin/regex": ["@jsep-plugin/regex@1.0.4", "", { "peerDependencies": { "jsep": "^0.4.0||^1.0.0" } }, "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg=="],
+
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@mdx-js/rollup": ["@mdx-js/rollup@3.1.1", "", { "dependencies": { "@mdx-js/mdx": "^3.0.0", "@rollup/pluginutils": "^5.0.0", "source-map": "^0.7.0", "vfile": "^6.0.0" }, "peerDependencies": { "rollup": ">=2" } }, "sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw=="],
@@ -744,17 +754,17 @@
"@tanstack/react-router-with-query": ["@tanstack/react-router-with-query@1.130.17", "", { "peerDependencies": { "@tanstack/react-query": ">=5.49.2", "@tanstack/react-router": ">=1.43.2", "@tanstack/router-core": ">=1.114.7", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-TNaSocW20KuPwUojEm130DLWTr9M5hsSzxiu4QqS2jNCnrGLuDrwMHyP+6fq13lG3YuU4u9O1qajxfJIGomZCg=="],
- "@tanstack/react-start": ["@tanstack/react-start@1.167.34", "", { "dependencies": { "@tanstack/react-router": "1.168.19", "@tanstack/react-start-client": "1.166.36", "@tanstack/react-start-rsc": "0.0.14", "@tanstack/react-start-server": "1.166.37", "@tanstack/router-utils": "^1.161.6", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-plugin-core": "1.167.31", "@tanstack/start-server-core": "1.167.18", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-gBT7PLCtN1LE4fo6S85P3WX7gQRDH6qCuBGVWOZeHwH/tzkp5HQWUWliHZFp3UyhY+pBdsLuiCBqdJ6pQk3bxQ=="],
+ "@tanstack/react-start": ["@tanstack/react-start@1.167.41", "", { "dependencies": { "@tanstack/react-router": "1.168.22", "@tanstack/react-start-client": "1.166.39", "@tanstack/react-start-rsc": "0.0.20", "@tanstack/react-start-server": "1.166.40", "@tanstack/router-utils": "^1.161.6", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-plugin-core": "1.167.35", "@tanstack/start-server-core": "1.167.19", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-w51yu/VQHNecIgN3ku+EmCxPjVbBftbnucDZBnxns43zPAhL5T5bnaBi0Fx4yJ3sDAmiceWEJD2F6IUaXTlNFA=="],
- "@tanstack/react-start-client": ["@tanstack/react-start-client@1.166.36", "", { "dependencies": { "@tanstack/react-router": "1.168.19", "@tanstack/router-core": "1.168.14", "@tanstack/start-client-core": "1.167.16" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-gfvevr2UB5B7Arv9pJl8VB0loeK7QgVOX1xiz1Wf1I52kbv1efaEJie0zzOWO9msR6brmFplMojAe5PNz9Gbug=="],
+ "@tanstack/react-start-client": ["@tanstack/react-start-client@1.166.39", "", { "dependencies": { "@tanstack/react-router": "1.168.22", "@tanstack/router-core": "1.168.15", "@tanstack/start-client-core": "1.167.17" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-NDao1nwwM4hXiVwQjVO8ZPlL9gZ0yeIl7w0PV+75jz+V9bhXr2nGhBuBw4zBuG7px4fEVmO5E8b7kpjWgCDJtA=="],
- "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.0.14", "", { "dependencies": { "@tanstack/react-router": "1.168.19", "@tanstack/react-start-server": "1.166.37", "@tanstack/router-core": "1.168.14", "@tanstack/router-utils": "1.161.6", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-fn-stubs": "1.161.6", "@tanstack/start-plugin-core": "1.167.31", "@tanstack/start-server-core": "1.167.18", "@tanstack/start-storage-context": "1.166.28", "pathe": "^2.0.3" }, "peerDependencies": { "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@vitejs/plugin-rsc"] }, "sha512-CgpRZ8u7egckKtaTkHuwiWJHeZjD6YbYXF6GmD+mukuA7NBBdR0NgX+M4IlR9w4xS95vVtVLruPk3B9GXY6+Nw=="],
+ "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.0.20", "", { "dependencies": { "@tanstack/react-router": "1.168.22", "@tanstack/react-start-server": "1.166.40", "@tanstack/router-core": "1.168.15", "@tanstack/router-utils": "1.161.6", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-fn-stubs": "1.161.6", "@tanstack/start-plugin-core": "1.167.35", "@tanstack/start-server-core": "1.167.19", "@tanstack/start-storage-context": "1.166.29", "pathe": "^2.0.3" }, "peerDependencies": { "@vitejs/plugin-rsc": ">=0.5.20", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@vitejs/plugin-rsc"] }, "sha512-KPsaq/asQxu/scMduMuQlwURIXwu+qZ7SO/8xbY/PQ/DJzfp9q1ZEHs9JN7dBEOHcme/YPeHta+UxBapB5Syyw=="],
- "@tanstack/react-start-server": ["@tanstack/react-start-server@1.166.37", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-router": "1.168.19", "@tanstack/router-core": "1.168.14", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-server-core": "1.167.18" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Ow9s9AvhnNpYaXvuOy6HLBRbev7lLDRK3GLeqF9IFDfynRYAdR8OZWluTvRTPQPBp4eQqHNylqJ9rQ6BQnZMJA=="],
+ "@tanstack/react-start-server": ["@tanstack/react-start-server@1.166.40", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-router": "1.168.22", "@tanstack/router-core": "1.168.15", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-server-core": "1.167.19" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-g5OU/eHmT1MleDbebdhNybpYgDu3QeNlemMCyXxEJNewOFM4iBlfoUBdMPhONJDlHQrOBXwMk/RUEFemY94yRw=="],
"@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="],
- "@tanstack/router-core": ["@tanstack/router-core@1.168.9", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2" }, "bin": { "intent": "bin/intent.js" } }, "sha512-18oeEwEDyXOIuO1VBP9ACaK7tYHZUjynGDCoUh/5c/BNhia9vCJCp9O0LfhZXOorDc/PmLSgvmweFhVmIxF10g=="],
+ "@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.167.3", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16" }, "peerDependencies": { "@tanstack/router-core": "^1.168.11", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-fJ1VMhyQgnoashTrP763c2HRc9kofgF61L7Jb3F6eTHAmCKtGVx8BRtiFt37sr3U0P0jmaaiiSPGP6nT5JtVNg=="],
@@ -764,15 +774,15 @@
"@tanstack/router-utils": ["@tanstack/router-utils@1.161.6", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-nRcYw+w2OEgK6VfjirYvGyPLOK+tZQz1jkYcmH5AjMamQ9PycnlxZF2aEZtPpNoUsaceX2bHptn6Ub5hGXqNvw=="],
- "@tanstack/start-client-core": ["@tanstack/start-client-core@1.167.16", "", { "dependencies": { "@tanstack/router-core": "1.168.14", "@tanstack/start-fn-stubs": "1.161.6", "@tanstack/start-storage-context": "1.166.28", "seroval": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-ftQ+f5SB2gLqU24QeofmbkfXiSSPS/VlLRdNC6DmshUIrDp8SQFb8gP/7hbkORs7jx4YULGA4z2xQrz/RmC+Kg=="],
+ "@tanstack/start-client-core": ["@tanstack/start-client-core@1.167.17", "", { "dependencies": { "@tanstack/router-core": "1.168.15", "@tanstack/start-fn-stubs": "1.161.6", "@tanstack/start-storage-context": "1.166.29", "seroval": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-3ZnpQ0LPnhrm/GX+HT7XfRxTcqnmBE1KJd7LtaJNuN13NH0C4ZOWchKLPEed2/gluhgsT6UgWm+Ec0kEFtxSaw=="],
"@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.161.6", "", {}, "sha512-Y6QSlGiLga8cHfvxGGaonXIlt2bIUTVdH6AMjmpMp7+ANNCp+N96GQbjjhLye3JkaxDfP68x5iZA8NK4imgRig=="],
- "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.167.31", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.168.14", "@tanstack/router-generator": "1.166.30", "@tanstack/router-plugin": "1.167.20", "@tanstack/router-utils": "1.161.6", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-server-core": "1.167.18", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.0", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-aOxHOgLx8OdkcSxYNDdL09t+VhdWg/rYB5iBJAIKzXE/fp2WBezPcIcyLYgzWnv4KCLHc1VcSNQ6ASLuwrLjtg=="],
+ "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.167.35", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.168.15", "@tanstack/router-generator": "1.166.32", "@tanstack/router-plugin": "1.167.22", "@tanstack/router-utils": "1.161.6", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-server-core": "1.167.19", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.5.0", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-Ww511KfsXd7TbPYzjiUDCMUI5VbO0chmrTgFi1oOUT0jmk5U0Xh9WVIun1cvRmaq+KBZwvWGvmeIn0UwO3mHEA=="],
- "@tanstack/start-server-core": ["@tanstack/start-server-core@1.167.18", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/router-core": "1.168.14", "@tanstack/start-client-core": "1.167.16", "@tanstack/start-storage-context": "1.166.28", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-JiV0Eqyj5SfEjGZrm/u3nBICeLZRQyeYNI7ine4PjTh0TVL7IklfN5Xfka+p80a/DuUvpHWqKvE4s3tdFZyc1A=="],
+ "@tanstack/start-server-core": ["@tanstack/start-server-core@1.167.19", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/router-core": "1.168.15", "@tanstack/start-client-core": "1.167.17", "@tanstack/start-storage-context": "1.166.29", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-wzOdfzLsK91CnjoywnEjXSlVlaRVK99HJhyVijNU1TECBI2JEKvW9S6d14YfS4gD4fFH4V86tFYhkcLPe6nzWg=="],
- "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.166.28", "", { "dependencies": { "@tanstack/router-core": "1.168.14" } }, "sha512-CUQMd6YtJ7hejKXDqT1R4N5gQ9PYyxUCv/ERNJ6/c/8ohNuhMPlOGSFVvqy2BLYNTFSj9GWjahMazeQpQomPgw=="],
+ "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.166.29", "", { "dependencies": { "@tanstack/router-core": "1.168.15" } }, "sha512-KrJYudc1nbnTY43jdN+hQFMYkhz7+3T+hkgBoGnIP1OspSe6vGQaYGDB4EUXYnkLfyQp+iUuKubgS8hSKeJ0ng=="],
"@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="],
@@ -786,6 +796,14 @@
"@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
+ "@tokenlens/core": ["@tokenlens/core@1.3.0", "", {}, "sha512-d8YNHNC+q10bVpi95fELJwJyPVf1HfvBEI18eFQxRSZTdByXrP+f/ZtlhSzkx0Jl0aEmYVeBA5tPeeYRioLViQ=="],
+
+ "@tokenlens/fetch": ["@tokenlens/fetch@1.3.0", "", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-RONDRmETYly9xO8XMKblmrZjKSwCva4s5ebJwQNfNlChZoA5kplPoCgnWceHnn1J1iRjLVlrCNB43ichfmGBKQ=="],
+
+ "@tokenlens/helpers": ["@tokenlens/helpers@1.3.1", "", { "dependencies": { "@tokenlens/core": "1.3.0", "@tokenlens/fetch": "1.3.0" } }, "sha512-t6yL8N6ES8337E6eVSeH4hCKnPdWkZRFpupy9w5E66Q9IeqQ9IO7XQ6gh12JKjvWiRHuyyJ8MBP5I549Cr41EQ=="],
+
+ "@tokenlens/models": ["@tokenlens/models@1.3.0", "", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-9mx7ZGeewW4ndXAiD7AT1bbCk4OpJeortbjHHyNkgap+pMPPn1chY6R5zqe1ggXIUzZ2l8VOAKfPqOvpcrisJw=="],
+
"@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="],
"@tsconfig/node10": ["@tsconfig/node10@1.0.12", "", {}, "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ=="],
@@ -914,21 +932,21 @@
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
- "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260408.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260408.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260408.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260408.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260408.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260408.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260408.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260408.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-N0MZLEUnAoP/aRVk7MY119LDsESkbtEwIw+YeXi/jjx2XCqf7ni3GxIVsUYtf/troyuSedq3V/OUrkoCh5A9gA=="],
+ "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260415.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260415.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260415.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260415.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260415.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260415.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260415.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260415.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-kRQ0x4DgXZBI0bNTck65EUaj48+hbMlCHiJKfc0Se5ZVUG0SKRC6JBPLwIBCX5TfljKsm8SstuJ3qn6uw1IWpA=="],
- "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260408.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YcPczNLfPDB13eUBYHkTOkL7HyWqqqEhho4eSxhAvigZuxvtHQ1uyILIvLVAwipEVzhJ8QciKmLdLucpfi4XyA=="],
+ "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20260415.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yGyyDb9bP3XfaIm8VUiaq7xkKwFSxLQ44XGYV78lrne12GhXgZ7Smbf2BVnT5MrTgT5uooMzww85P3I3XNVZng=="],
- "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260408.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cHqkDg53xxxz21MThLBf4vx1kyIpRPEYNdEiQlvu9O35Tth49+aub6F+/YEMd9MG4TYZmxh1bEjkjErTUIElpA=="],
+ "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20260415.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-vzac8BSbSkGPV/FMKyY/3cNZN+FgvjT1E+NNR8xWO8DfvSz4hYqbxvAL+zWPUno6R8afNFLZeJTfuIge0tJJ1g=="],
- "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260408.1", "", { "os": "linux", "cpu": "arm" }, "sha512-w26Gv9yq9LIYIhxjkQC+i0wBPDdQdX+H06ZhyVRL5grKWTIsk9Xwjp9mDRB/dGlXBKcvnM25JH16OyAA0rFH3A=="],
+ "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20260415.1", "", { "os": "linux", "cpu": "arm" }, "sha512-1CI2nLfsoEibEAt6ApMVEr5M/v9EeXHmn9iD2nyyomO34ky3zqBtEPHakvgXD4QmpZg9O4WxRKc6u6EIy0Imxg=="],
- "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260408.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-iHG0FEXq/QFsn+qlTPllxdcbvfQ9aRYggy4lc1z0+f11Nyk4YDNCSiR8WW7pbnOTx/VreGbbXhlpuJXTidqL8g=="],
+ "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20260415.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jiXu+SrpCL/6J3LwuUSxU8scYs5H0wBkqu3CopdSTcJxQuzUDe6QAEoEW3O81cdrsq5qrIlfFxWH3bdohsvhJA=="],
- "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260408.1", "", { "os": "linux", "cpu": "x64" }, "sha512-hMcUlUIzYbvbdq6j/B4RPL+kZR917NGnE9AgPZ7dJ92yamH/7LGT1Mnlc6McUx31yqTFBFHdTc7Cfx+ynua7Iw=="],
+ "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20260415.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+4r4UqGE5ccy9vJNOhjXTqbZPkVGlqdiEgTJbdz8+EpUNQaLGMBhDj1cBMdrdK1YRuj/3C+pLfE3PD9kEsxf6Q=="],
- "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260408.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-avJWIEKSx4rdBLZD1FOOTuxTU51dQfYb3jZvZMaXD4thJjq+6eSwfzu2elwL36AZDlnaxggGjB5nBxp0t54iOA=="],
+ "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20260415.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-QsCAG7la4GE4Dp7i9MUz7Qv+HbKVDdmwlmn+8sOo0M3aSC6WssCU5gKVBUjp43WPtml/JticwzSz1qXLfdxHFA=="],
- "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260408.1", "", { "os": "win32", "cpu": "x64" }, "sha512-gpvEHkF/WoxkA3711c4uWNCZO9WAuwrq49COdNwxgOTzYHnMc1yCj8CpkCUJwU0f/Ydwp2s6/efn6gTMvtckPg=="],
+ "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260415.1", "", { "os": "win32", "cpu": "x64" }, "sha512-Z+rclKC7FqkUsqQ+ErgWJmf5J55LEl/rooFq71prC6V0vCBa5yLMmLBmFxZLLj2BsCBuwbN2O7aWUWrpCUEkmw=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -964,7 +982,7 @@
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
- "ai": ["ai@6.0.158", "", { "dependencies": { "@ai-sdk/gateway": "3.0.95", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-gLTp1UXFtMqKUi3XHs33K7UFglbvojkxF/aq337TxnLGOhHIW9+GyP2jwW4hYX87f1es+wId3VQoPRRu9zEStQ=="],
+ "ai": ["ai@6.0.162", "", { "dependencies": { "@ai-sdk/gateway": "3.0.99", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1PSvNEK1PEbpUXahnFrcey6l7DJXMVWmg0ibQ8h8oMSe9V1Vx5d+R3xNu0hzBtwqfxYj21ddZo+EUYVs6GOEyA=="],
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
@@ -1000,7 +1018,7 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
- "baseline-browser-mapping": ["baseline-browser-mapping@2.10.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA=="],
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A=="],
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
@@ -1010,7 +1028,7 @@
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
- "brace-expansion": ["brace-expansion@1.1.13", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w=="],
+ "brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
@@ -1094,7 +1112,7 @@
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
- "cookie-es": ["cookie-es@2.0.1", "", {}, "sha512-aVf4A4hI2w70LnF7GG+7xDQUkliwiXWXFvTjkip4+b64ygDQ2sJPRSKFDHbxn8o0xu9QzPkMuuiWIXyFSE2slA=="],
+ "cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
@@ -1244,7 +1262,7 @@
"domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
- "dotenv": ["dotenv@17.4.1", "", {}, "sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw=="],
+ "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -1252,7 +1270,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.334", "", {}, "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.335", "", {}, "sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
@@ -1324,6 +1342,8 @@
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+ "fake-indexeddb": ["fake-indexeddb@6.2.5", "", {}, "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w=="],
+
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
@@ -1542,6 +1562,8 @@
"jsdom": ["jsdom@29.0.2", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.5", "@asamuzakjp/dom-selector": "^7.0.6", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.1", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.2.7", "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w=="],
+ "jsep": ["jsep@1.4.0", "", {}, "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw=="],
+
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
@@ -1556,6 +1578,8 @@
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
+ "jsonpath-plus": ["jsonpath-plus@10.4.0", "", { "dependencies": { "@jsep-plugin/assignment": "^1.3.0", "@jsep-plugin/regex": "^1.0.4", "jsep": "^1.4.0" }, "bin": { "jsonpath": "bin/jsonpath-cli.js", "jsonpath-plus": "bin/jsonpath-cli.js" } }, "sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA=="],
+
"katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="],
"khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
@@ -1866,7 +1890,7 @@
"powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="],
- "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
+ "prettier": ["prettier@3.8.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q=="],
"pretty-format": ["pretty-format@30.3.0", "", { "dependencies": { "@jest/schemas": "30.0.5", "ansi-styles": "^5.2.0", "react-is": "^18.3.1" } }, "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ=="],
@@ -2072,6 +2096,8 @@
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+ "swr": ["swr@2.4.1", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA=="],
+
"symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
@@ -2084,6 +2110,8 @@
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
+ "throttleit": ["throttleit@2.1.0", "", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
+
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
@@ -2104,6 +2132,8 @@
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+ "tokenlens": ["tokenlens@1.3.1", "", { "dependencies": { "@tokenlens/core": "1.3.0", "@tokenlens/fetch": "1.3.0", "@tokenlens/helpers": "1.3.1", "@tokenlens/models": "1.3.0" } }, "sha512-7oxmsS5PNCX3z+b+z07hL5vCzlgHKkCGrEQjQmWl5l+v5cUrtL7S1cuST4XThaL1XyjbTX8J5hfP0cjDJRkaLA=="],
+
"tough-cookie": ["tough-cookie@6.0.1", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
"tr46": ["tr46@6.0.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
@@ -2268,6 +2298,8 @@
"@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+ "@ai-sdk/react/ai": ["ai@6.0.161", "", { "dependencies": { "@ai-sdk/gateway": "3.0.98", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ufhmijmx2YyWTPAicGgtpLOB/xD7mG8zKs1pT1Trj+JL/3r1rS8fkMi/cHZoChSAQSGB4pgmcWVxDrVTUvK2IQ=="],
+
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
@@ -2316,37 +2348,41 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
- "@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/react-start/@tanstack/react-router": ["@tanstack/react-router@1.168.22", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q=="],
- "@tanstack/react-start-client/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/react-start-client/@tanstack/react-router": ["@tanstack/react-router@1.168.22", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q=="],
- "@tanstack/react-start-rsc/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/react-start-client/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="],
- "@tanstack/react-start-server/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/react-start-rsc/@tanstack/react-router": ["@tanstack/react-router@1.168.22", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q=="],
- "@tanstack/router-devtools-core/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/react-start-rsc/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="],
- "@tanstack/router-generator/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/react-start-server/@tanstack/react-router": ["@tanstack/react-router@1.168.22", "", { "dependencies": { "@tanstack/history": "1.161.6", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.168.15", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-W2LyfkfJtDCf//jOjZeUBWwOVl8iDRVTECpGHa2M28MT3T5/VVnjgicYNHR/ax0Filk1iU67MRjcjHheTYvK1Q=="],
- "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+ "@tanstack/react-start-server/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="],
- "@tanstack/router-plugin/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@tanstack/router-plugin/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
- "@tanstack/start-client-core/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/start-client-core/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="],
"@tanstack/start-plugin-core/@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="],
- "@tanstack/start-plugin-core/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/start-plugin-core/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="],
+
+ "@tanstack/start-plugin-core/@tanstack/router-generator": ["@tanstack/router-generator@1.166.32", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.15", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "magic-string": "^0.30.21", "prettier": "^3.5.0", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-VuusKwEXcgKq+myq1JQfZogY8scTXIIeFls50dJ/UXgCXWp5n14iFreYNlg41wURcak2oA3M+t2TVfD0xUUD6g=="],
+
+ "@tanstack/start-plugin-core/@tanstack/router-plugin": ["@tanstack/router-plugin@1.167.22", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.168.15", "@tanstack/router-generator": "1.166.32", "@tanstack/router-utils": "1.161.6", "@tanstack/virtual-file-routes": "1.161.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.168.21", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"], "bin": { "intent": "bin/intent.js" } }, "sha512-wYPzIvBK8bcmXVUpZfSgGBXOrfBAdF4odKevz6rejio5rEd947NtKDF5R7eYdwlAOmRqYpLJnJ1QHkc5t8bY4w=="],
"@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
- "@tanstack/start-server-core/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/start-server-core/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="],
- "@tanstack/start-storage-context/@tanstack/router-core": ["@tanstack/router-core@1.168.14", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-UhCJtjNrd5wcTmhgB2HyUP0+Rj1M7BD4dS11YsF9x6VC2KH/eqxzs/vK+nN5f+cOhPOLZdmLkWMW+WGmacZ8HA=="],
+ "@tanstack/start-storage-context/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="],
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
@@ -2368,6 +2404,8 @@
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+ "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
"cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
@@ -2454,6 +2492,8 @@
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+ "@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.98", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Ol+nP8PIlj8FjN8qKlxhE89N0woqAaGi9CUBGp1boe3RafpphJ7WMuq/RErSvxtwTqje03TP+zIdzP113krxRg=="],
+
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
@@ -2474,30 +2514,10 @@
"@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
- "@tanstack/react-router/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
- "@tanstack/react-start-client/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
- "@tanstack/react-start-rsc/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
- "@tanstack/react-start-server/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
- "@tanstack/router-devtools-core/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
- "@tanstack/router-generator/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
- "@tanstack/router-plugin/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
- "@tanstack/start-client-core/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
+ "@tanstack/react-start/@tanstack/react-router/@tanstack/router-core": ["@tanstack/router-core@1.168.15", "", { "dependencies": { "@tanstack/history": "1.161.6", "cookie-es": "^3.0.0", "seroval": "^1.5.0", "seroval-plugins": "^1.5.0" }, "bin": { "intent": "bin/intent.js" } }, "sha512-Wr0424NDtD8fT/uALobMZ9DdcfsTyXtW5IPR++7zvW8/7RaIOeaqXpVDId8ywaGtqPWLWOfaUg2zUtYtukoXYA=="],
"@tanstack/start-plugin-core/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
- "@tanstack/start-plugin-core/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
- "@tanstack/start-server-core/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
- "@tanstack/start-storage-context/@tanstack/router-core/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
-
"@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"@ts-morph/common/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
diff --git a/package.json b/package.json
index 4b6c5a2..7c1f8dd 100644
--- a/package.json
+++ b/package.json
@@ -23,10 +23,11 @@
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.69",
- "@ai-sdk/google": "^3.0.62",
+ "@ai-sdk/google": "^3.0.63",
"@ai-sdk/groq": "^3.0.35",
"@ai-sdk/mistral": "^3.0.30",
- "@ai-sdk/openai": "^3.0.52",
+ "@ai-sdk/openai": "^3.0.53",
+ "@ai-sdk/react": "3.0.163",
"@ai-sdk/xai": "^3.0.82",
"@base-ui/react": "^1.4.0",
"@dnd-kit/core": "^6.3.1",
@@ -44,12 +45,13 @@
"@tanstack/react-query": "^5.99.0",
"@tanstack/react-router": "1.168.19",
"@tanstack/react-router-with-query": "1.130.17",
- "@tanstack/react-start": "^1.167.34",
- "ai": "^6.0.158",
+ "@tanstack/react-start": "^1.167.41",
+ "ai": "^6.0.162",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
+ "jsonpath-plus": "^10.4.0",
"lucide-react": "^1.8.0",
"motion": "^12.38.0",
"next-themes": "^0.4.6",
@@ -63,6 +65,7 @@
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.2",
"tailwindcss-animate": "^1.0.7",
+ "tokenlens": "^1.3.1",
"tw-animate-css": "^1.4.0",
"use-stick-to-bottom": "^1.1.3",
"zod": "^4.3.6"
@@ -80,10 +83,11 @@
"@types/json-schema": "^7.0.15",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
- "@typescript/native-preview": "^7.0.0-dev.20260408.1",
+ "@typescript/native-preview": "^7.0.0-dev.20260415.1",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/coverage-v8": "4.1.4",
"ajv": "^8.18.0",
+ "fake-indexeddb": "^6.2.5",
"jsdom": "^29.0.2",
"oxfmt": "^0.45.0",
"oxlint": "^1.60.0",
diff --git a/public/r/generated-command.json b/public/r/generated-command.json
index 3bcf4bf..3cee7cd 100644
--- a/public/r/generated-command.json
+++ b/public/r/generated-command.json
@@ -20,25 +20,25 @@
},
{
"path": "registry/commandly/types/flat.ts",
- "content": "export interface ToolInfo {\n description?: string;\n version?: string;\n url?: string;\n}\n\nexport interface Command {\n key: string;\n parentCommandKey?: string;\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n values: ParameterEnumValue[];\n allowMultiple?: boolean;\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n key: string;\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n key: string;\n parameterKey: string;\n dependsOnParameterKey: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n key: string;\n name: string;\n commandKey?: string;\n description?: string;\n group?: string;\n metadata?: ParameterMetadata;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: ParameterValidation[];\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n key?: string;\n commandKey?: string;\n name: string;\n exclusionType: ExclusionType;\n parameterKeys: string[];\n}\n\nexport interface Tool {\n name: string;\n displayName: string;\n info?: ToolInfo;\n commands: Command[];\n parameters: Parameter[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: ToolMetadata;\n}\n",
+ "content": "export interface ToolInfo {\n /** A brief human-readable description of what the tool does. */\n description?: string;\n /** The version string of the tool (e.g. \"1.0.0\"). */\n version?: string;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n}\n\nexport interface Command {\n /** Unique identifier for this command within the tool. */\n key: string;\n /** Key of the parent command; used to represent subcommand nesting. */\n parentCommandKey?: string;\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n /** The raw value passed to the CLI for this choice. */\n value: string;\n /** Human-readable label shown to the user for this enum choice. */\n displayName: string;\n /** Description of what this enum value does or represents. */\n description?: string;\n /** Whether this is the default selection when no value is provided. */\n isDefault?: boolean;\n /** Display sort position relative to sibling enum values. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n /** The list of allowed enum choices. */\n values: ParameterEnumValue[];\n /** Whether the user can select multiple values at once. */\n allowMultiple?: boolean;\n /** Separator character used when joining multiple selected values. */\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n /** Unique identifier for this validation rule. */\n key: string;\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. the max length number, or a regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n /** Unique identifier for this dependency rule. */\n key: string;\n /** Key of the parameter that owns this dependency. */\n parameterKey: string;\n /** Key of the parameter this dependency references. */\n dependsOnParameterKey: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n /** Arbitrary tags for categorising or filtering parameters. */\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n /** Unique identifier for this parameter within the tool. */\n key: string;\n /** Human-readable display name of the parameter. */\n name: string;\n /** Key of the command this parameter belongs to; omit for global parameters. */\n commandKey?: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: ParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n /** Unique identifier for this exclusion group. */\n key?: string;\n /** Key of the command this exclusion group belongs to; omit for global groups. */\n commandKey?: string;\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Keys of the parameters that participate in this exclusion group. */\n parameterKeys: string[];\n}\n\nexport interface Tool {\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** List of all commands and subcommands defined for this tool. */\n commands: Command[];\n /** Flat list of all parameters across all commands and global scope. */\n parameters: Parameter[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: ExclusionGroup[];\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n",
"type": "registry:file",
"target": "components/commandly/types/flat.ts"
},
{
"path": "registry/commandly/types/nested.ts",
- "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n group?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n info?: ToolInfo;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n",
+ "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. max length number or regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n /** Name of the parameter this dependency references. */\n dependsOnParameter: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n /** Human-readable display name of the parameter. */\n name: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: NestedParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder: number;\n /** Parameters that belong directly to this command. */\n parameters: NestedParameter[];\n /** Nested subcommands of this command. */\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Names of the parameters that participate in this exclusion group. */\n parameters: string[];\n}\n\nexport interface NestedTool {\n /** Optional JSON schema URI for validation. */\n $schema?: string;\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n /** Parameters that apply to all commands globally. */\n globalParameters: NestedParameter[];\n /** Hierarchical list of commands and their nested subcommands. */\n commands: NestedCommand[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: NestedExclusionGroup[] | null;\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n",
"type": "registry:file",
"target": "components/commandly/types/nested.ts"
},
{
"path": "registry/commandly/utils/flat.ts",
- "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n url: tool.info?.url,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n",
+ "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n",
"type": "registry:file",
"target": "components/commandly/utils/flat.ts"
},
{
"path": "registry/commandly/utils/nested.ts",
- "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n",
+ "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n",
"type": "registry:file",
"target": "components/commandly/utils/nested.ts"
}
diff --git a/public/r/json-output.json b/public/r/json-output.json
index 93f7e55..d7340ba 100644
--- a/public/r/json-output.json
+++ b/public/r/json-output.json
@@ -13,31 +13,31 @@
"files": [
{
"path": "registry/commandly/json-output.tsx",
- "content": "import { Tool } from \"@/components/commandly/types/flat\";\nimport { exportToStructuredJSON } from \"@/components/commandly/utils/flat\";\nimport { convertToNestedStructure } from \"@/components/commandly/utils/nested\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n \n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n \n \n {isEditing ? (\n \n
\n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : (\n \n \n {jsonString}\n \n \n \n \n )}\n \n \n );\n}\n",
+ "content": "import { Tool } from \"@/components/commandly/types/flat\";\nimport { exportToStructuredJSON } from \"@/components/commandly/utils/flat\";\nimport { convertToNestedStructure } from \"@/components/commandly/utils/nested\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ntype DiffLine = { type: \"same\" | \"added\" | \"removed\"; text: string };\n\nfunction diffLines(before: string, after: string): DiffLine[] {\n const a = before.split(\"\\n\");\n const b = after.split(\"\\n\");\n const m = a.length;\n const n = b.length;\n const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));\n for (let i = 1; i <= m; i++)\n for (let j = 1; j <= n; j++)\n dp[i][j] =\n a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);\n const result: DiffLine[] = [];\n let i = m,\n j = n;\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {\n result.unshift({ type: \"same\", text: a[i - 1] });\n i--;\n j--;\n } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {\n result.unshift({ type: \"added\", text: b[j - 1] });\n j--;\n } else {\n result.unshift({ type: \"removed\", text: a[i - 1] });\n i--;\n }\n }\n return result;\n}\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n originalTool?: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, originalTool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [originalJsonString, setOriginalJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n const [showDiff, setShowDiff] = useState(true);\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n useEffect(() => {\n if (!originalTool) {\n setOriginalJsonString(undefined);\n return;\n }\n const config =\n jsonType === \"flat\"\n ? exportToStructuredJSON(originalTool)\n : convertToNestedStructure(originalTool);\n setOriginalJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, originalTool]);\n\n const diff = useMemo(() => {\n if (!originalJsonString || !jsonString || originalJsonString === jsonString) return null;\n return diffLines(originalJsonString, jsonString);\n }, [originalJsonString, jsonString]);\n\n const diffStats = useMemo(() => {\n if (!diff) return null;\n const added = diff.filter((l) => l.type === \"added\").length;\n const removed = diff.filter((l) => l.type === \"removed\").length;\n return { added, removed };\n }, [diff]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n \n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n \n \n {diffStats && !isEditing && (\n \n {diffStats.added > 0 && (\n \n +{diffStats.added} added\n \n )}\n {diffStats.removed > 0 && (\n \n -{diffStats.removed} removed\n \n )}\n setShowDiff((v) => !v)}\n >\n {showDiff ? \"Full view\" : \"Diff view\"}\n \n
\n )}\n {isEditing ? (\n \n
\n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : diff && showDiff ? (\n \n \n {diff.map((line, idx) => (\n \n \n {line.type === \"added\" ? \"+ \" : line.type === \"removed\" ? \"- \" : \" \"}\n \n {line.text}\n
\n ))}\n \n \n \n \n ) : (\n \n \n {jsonString}\n \n \n \n \n )}\n \n \n );\n}\n",
"type": "registry:component",
"target": "components/commandly/json-output.tsx"
},
{
"path": "registry/commandly/types/flat.ts",
- "content": "export interface ToolInfo {\n description?: string;\n version?: string;\n url?: string;\n}\n\nexport interface Command {\n key: string;\n parentCommandKey?: string;\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n values: ParameterEnumValue[];\n allowMultiple?: boolean;\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n key: string;\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n key: string;\n parameterKey: string;\n dependsOnParameterKey: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n key: string;\n name: string;\n commandKey?: string;\n description?: string;\n group?: string;\n metadata?: ParameterMetadata;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: ParameterValidation[];\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n key?: string;\n commandKey?: string;\n name: string;\n exclusionType: ExclusionType;\n parameterKeys: string[];\n}\n\nexport interface Tool {\n name: string;\n displayName: string;\n info?: ToolInfo;\n commands: Command[];\n parameters: Parameter[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: ToolMetadata;\n}\n",
+ "content": "export interface ToolInfo {\n /** A brief human-readable description of what the tool does. */\n description?: string;\n /** The version string of the tool (e.g. \"1.0.0\"). */\n version?: string;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n}\n\nexport interface Command {\n /** Unique identifier for this command within the tool. */\n key: string;\n /** Key of the parent command; used to represent subcommand nesting. */\n parentCommandKey?: string;\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n /** The raw value passed to the CLI for this choice. */\n value: string;\n /** Human-readable label shown to the user for this enum choice. */\n displayName: string;\n /** Description of what this enum value does or represents. */\n description?: string;\n /** Whether this is the default selection when no value is provided. */\n isDefault?: boolean;\n /** Display sort position relative to sibling enum values. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n /** The list of allowed enum choices. */\n values: ParameterEnumValue[];\n /** Whether the user can select multiple values at once. */\n allowMultiple?: boolean;\n /** Separator character used when joining multiple selected values. */\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n /** Unique identifier for this validation rule. */\n key: string;\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. the max length number, or a regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n /** Unique identifier for this dependency rule. */\n key: string;\n /** Key of the parameter that owns this dependency. */\n parameterKey: string;\n /** Key of the parameter this dependency references. */\n dependsOnParameterKey: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n /** Arbitrary tags for categorising or filtering parameters. */\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n /** Unique identifier for this parameter within the tool. */\n key: string;\n /** Human-readable display name of the parameter. */\n name: string;\n /** Key of the command this parameter belongs to; omit for global parameters. */\n commandKey?: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: ParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n /** Unique identifier for this exclusion group. */\n key?: string;\n /** Key of the command this exclusion group belongs to; omit for global groups. */\n commandKey?: string;\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Keys of the parameters that participate in this exclusion group. */\n parameterKeys: string[];\n}\n\nexport interface Tool {\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** List of all commands and subcommands defined for this tool. */\n commands: Command[];\n /** Flat list of all parameters across all commands and global scope. */\n parameters: Parameter[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: ExclusionGroup[];\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n",
"type": "registry:file",
"target": "components/commandly/types/flat.ts"
},
{
"path": "registry/commandly/types/nested.ts",
- "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n group?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n info?: ToolInfo;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n",
+ "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. max length number or regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n /** Name of the parameter this dependency references. */\n dependsOnParameter: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n /** Human-readable display name of the parameter. */\n name: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: NestedParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder: number;\n /** Parameters that belong directly to this command. */\n parameters: NestedParameter[];\n /** Nested subcommands of this command. */\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Names of the parameters that participate in this exclusion group. */\n parameters: string[];\n}\n\nexport interface NestedTool {\n /** Optional JSON schema URI for validation. */\n $schema?: string;\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n /** Parameters that apply to all commands globally. */\n globalParameters: NestedParameter[];\n /** Hierarchical list of commands and their nested subcommands. */\n commands: NestedCommand[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: NestedExclusionGroup[] | null;\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n",
"type": "registry:file",
"target": "components/commandly/types/nested.ts"
},
{
"path": "registry/commandly/utils/flat.ts",
- "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n url: tool.info?.url,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n",
+ "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n",
"type": "registry:file",
"target": "components/commandly/utils/flat.ts"
},
{
"path": "registry/commandly/utils/nested.ts",
- "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n",
+ "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n",
"type": "registry:file",
"target": "components/commandly/utils/nested.ts"
}
diff --git a/public/r/tool-renderer.json b/public/r/tool-renderer.json
index 7e79cc8..04535ee 100644
--- a/public/r/tool-renderer.json
+++ b/public/r/tool-renderer.json
@@ -14,7 +14,7 @@
"files": [
{
"path": "registry/commandly/tool-renderer.tsx",
- "content": "import { ParameterValue } from \"@/components/commandly/types/flat\";\nimport { Command, Tool } from \"@/components/commandly/types/flat\";\nimport {\n ParameterRenderContext,\n ParameterRendererEntry,\n} from \"@/components/commandly/types/renderer\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command as UICommand,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, InfoIcon, PlusIcon, XIcon } from \"lucide-react\";\nimport React from \"react\";\n\nconst findDefaultCommand = (tool: Tool): Command | null => {\n const defaultCommand = tool.commands.find((command) => command.isDefault);\n if (defaultCommand) return defaultCommand;\n\n const nameMatchCommand = tool.commands.find(\n (command) => command.name.toLowerCase() === tool.name.toLowerCase(),\n );\n if (nameMatchCommand) return nameMatchCommand;\n\n return tool.commands.length > 0 ? tool.commands[0] : null;\n};\n\ninterface ParameterLabelProps {\n name: string;\n longFlag?: string;\n shortFlag?: string;\n isRequired?: boolean;\n isGlobal?: boolean;\n description?: string;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction ParameterLabel({\n name,\n longFlag,\n shortFlag,\n isRequired,\n isGlobal,\n description,\n className,\n children,\n}: ParameterLabelProps) {\n return (\n \n {name}\n {(longFlag || shortFlag) && (\n \n ({[longFlag, shortFlag].filter(Boolean).join(\", \")})\n \n )}\n {isRequired && * }\n \n \n \n \n \n {description} \n \n \n {children}\n {isGlobal && (\n \n global\n \n )}\n \n );\n}\n\nfunction FlagInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n );\n}\n\nfunction OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n const [open, setOpen] = React.useState(false);\n const separator = parameter.enum?.separator || \",\";\n const options =\n parameter.enum?.values?.map((e) => ({\n value: e.value,\n label: e.displayName || e.value,\n })) ?? [];\n\n const label = (\n \n );\n\n if (parameter.enum?.allowMultiple) {\n const selected = value ? (value as string).split(separator).filter(Boolean) : [];\n return (\n \n {label}\n onUpdate(vals.join(separator))}\n placeholder=\"Select options\"\n />\n
\n );\n }\n\n return (\n \n {label}\n
\n \n \n {options.find((o) => o.value === value)?.label ?? \"Select an option\"}\n \n \n \n \n \n \n \n No option found. \n \n {options.map((option) => (\n {\n onUpdate(currentValue === value ? \"\" : currentValue);\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n
\n );\n}\n\nfunction OptionBooleanInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
onUpdate(checked.toString())}\n />\n \n \n );\n}\n\nfunction OptionTextInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
\n
onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\nfunction ArgumentInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
\n \n {parameter.parameterType}\n {parameter.position !== undefined && ` (${parameter.position})`}\n \n \n
onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\ninterface RepeatableWrapperProps {\n parameter: ParameterRenderContext[\"parameter\"];\n value: ParameterRenderContext[\"value\"];\n onUpdate: ParameterRenderContext[\"onUpdate\"];\n renderEntry: ParameterRendererEntry[\"component\"];\n}\n\nfunction RepeatableWrapper({ parameter, value, onUpdate, renderEntry }: RepeatableWrapperProps) {\n const toArray = (v: ParameterRenderContext[\"value\"]): string[] => {\n if (Array.isArray(v)) return v;\n if (v !== undefined && v !== \"\" && v !== false) return [String(v)];\n return [\"\"];\n };\n\n const values = toArray(value);\n\n const updateAt = (index: number, val: ParameterRenderContext[\"value\"]) => {\n const next = [...values];\n next[index] = String(val);\n onUpdate(next);\n };\n\n const addRow = () => onUpdate([...values, \"\"]);\n\n const removeAt = (index: number) => {\n const next = values.filter((_, i) => i !== index);\n onUpdate(next);\n };\n\n return (\n \n {values.map((val, index) => (\n
\n
\n {renderEntry({ parameter, value: val, onUpdate: (v) => updateAt(index, v) })}\n
\n {index > 0 && (\n
removeAt(index)}\n >\n \n \n )}\n
\n ))}\n
\n \n Add another\n \n
\n );\n}\n\nexport function defaultComponents(): ParameterRendererEntry[] {\n return [\n { condition: (p) => p.parameterType === \"Flag\", component: (ctx) => },\n {\n condition: (p) => p.parameterType === \"Argument\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Enum\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Boolean\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\",\n component: (ctx) => ,\n },\n ];\n}\n\ninterface ToolRendererProps {\n selectedCommand?: Command | null;\n tool: Tool;\n catalog?: ParameterRendererEntry[];\n parameterValues: Record;\n updateParameterValue: (parameterKey: string, value: ParameterValue) => void;\n}\n\nexport function ToolRenderer({\n selectedCommand: providedCommand,\n tool,\n catalog = defaultComponents(),\n parameterValues,\n updateParameterValue,\n}: ToolRendererProps) {\n const selectedCommand = providedCommand ?? findDefaultCommand(tool);\n\n return (\n \n {selectedCommand && tool.commands.length === 0 ? (\n No commands available for this tool.
\n ) : (\n \n {tool.parameters.length > 0 ? (\n tool.parameters\n .filter((param) => param.commandKey === selectedCommand?.key || param.isGlobal)\n .map((parameter) => {\n const value = parameterValues[parameter.key] ?? \"\";\n const onUpdate = (val: ParameterValue) => updateParameterValue(parameter.key, val);\n const entry = catalog.find((e) => e.condition(parameter));\n if (!entry) return null;\n return (\n
\n {parameter.isRepeatable ? (\n \n ) : (\n entry.component({ parameter, value, onUpdate })\n )}\n \n );\n })\n ) : (\n
\n No parameters available for this command.\n
\n )}\n
\n )}\n \n );\n}\n",
+ "content": "import { ParameterValue } from \"@/components/commandly/types/flat\";\nimport { Command, Tool } from \"@/components/commandly/types/flat\";\nimport {\n ParameterRenderContext,\n ParameterRendererEntry,\n} from \"@/components/commandly/types/renderer\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command as UICommand,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, InfoIcon, PlusIcon, XIcon } from \"lucide-react\";\nimport React from \"react\";\n\nconst findDefaultCommand = (tool: Tool): Command | null => {\n const nameMatchCommand = tool.commands.find(\n (command) => command.name.toLowerCase() === tool.name.toLowerCase(),\n );\n if (nameMatchCommand) return nameMatchCommand;\n\n return tool.commands.length > 0 ? tool.commands[0] : null;\n};\n\ninterface ParameterLabelProps {\n name: string;\n longFlag?: string;\n shortFlag?: string;\n isRequired?: boolean;\n isGlobal?: boolean;\n description?: string;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction ParameterLabel({\n name,\n longFlag,\n shortFlag,\n isRequired,\n isGlobal,\n description,\n className,\n children,\n}: ParameterLabelProps) {\n return (\n \n {name}\n {(longFlag || shortFlag) && (\n \n ({[longFlag, shortFlag].filter(Boolean).join(\", \")})\n \n )}\n {isRequired && * }\n \n \n \n \n \n {description} \n \n \n {children}\n {isGlobal && (\n \n global\n \n )}\n \n );\n}\n\nfunction FlagInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n );\n}\n\nfunction OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n const [open, setOpen] = React.useState(false);\n const separator = parameter.enum?.separator || \",\";\n const options =\n parameter.enum?.values?.map((e) => ({\n value: e.value,\n label: e.displayName || e.value,\n })) ?? [];\n\n const label = (\n \n );\n\n if (parameter.enum?.allowMultiple) {\n const selected = Array.isArray(value)\n ? (value as string[]).filter(Boolean)\n : value\n ? (value as string).split(separator).filter(Boolean)\n : [];\n return (\n \n {label}\n onUpdate(vals.join(separator))}\n placeholder=\"Select options\"\n />\n
\n );\n }\n\n return (\n \n {label}\n
\n \n \n {options.find((o) => o.value === value)?.label ?? \"Select an option\"}\n \n \n \n \n \n \n \n No option found. \n \n {options.map((option) => (\n {\n onUpdate(currentValue === value ? \"\" : currentValue);\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n
\n );\n}\n\nfunction OptionBooleanInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
onUpdate(checked.toString())}\n />\n \n \n );\n}\n\nfunction OptionTextInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
\n
onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\nfunction ArgumentInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
\n \n {parameter.parameterType}\n {parameter.position !== undefined && ` (${parameter.position})`}\n \n \n
onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\ninterface RepeatableWrapperProps {\n parameter: ParameterRenderContext[\"parameter\"];\n value: ParameterRenderContext[\"value\"];\n onUpdate: ParameterRenderContext[\"onUpdate\"];\n renderEntry: ParameterRendererEntry[\"component\"];\n}\n\nfunction RepeatableWrapper({ parameter, value, onUpdate, renderEntry }: RepeatableWrapperProps) {\n const toArray = (v: ParameterRenderContext[\"value\"]): string[] => {\n if (Array.isArray(v)) return v;\n if (v !== undefined && v !== \"\" && v !== false) return [String(v)];\n return [\"\"];\n };\n\n const values = toArray(value);\n\n const updateAt = (index: number, val: ParameterRenderContext[\"value\"]) => {\n const next = [...values];\n next[index] = String(val);\n onUpdate(next);\n };\n\n const addRow = () => onUpdate([...values, \"\"]);\n\n const removeAt = (index: number) => {\n const next = values.filter((_, i) => i !== index);\n onUpdate(next);\n };\n\n return (\n \n {values.map((val, index) => (\n
\n
\n {renderEntry({ parameter, value: val, onUpdate: (v) => updateAt(index, v) })}\n
\n {index > 0 && (\n
removeAt(index)}\n >\n \n \n )}\n
\n ))}\n
\n \n Add another\n \n
\n );\n}\n\nexport function defaultComponents(): ParameterRendererEntry[] {\n return [\n { condition: (p) => p.parameterType === \"Flag\", component: (ctx) => },\n {\n condition: (p) => p.parameterType === \"Argument\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Enum\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Boolean\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\",\n component: (ctx) => ,\n },\n ];\n}\n\ninterface ToolRendererProps {\n selectedCommand?: Command | null;\n tool: Tool;\n catalog?: ParameterRendererEntry[];\n parameterValues: Record;\n updateParameterValue: (parameterKey: string, value: ParameterValue) => void;\n}\n\nexport function ToolRenderer({\n selectedCommand: providedCommand,\n tool,\n catalog = defaultComponents(),\n parameterValues,\n updateParameterValue,\n}: ToolRendererProps) {\n const selectedCommand = providedCommand ?? findDefaultCommand(tool);\n\n return (\n \n {selectedCommand && tool.commands.length === 0 ? (\n No commands available for this tool.
\n ) : (\n \n {tool.parameters.length > 0 ? (\n tool.parameters\n .filter((param) => param.commandKey === selectedCommand?.key || param.isGlobal)\n .map((parameter) => {\n const value = parameterValues[parameter.key] ?? \"\";\n const onUpdate = (val: ParameterValue) => updateParameterValue(parameter.key, val);\n const entry = catalog.find((e) => e.condition(parameter));\n if (!entry) return null;\n return (\n
\n {parameter.isRepeatable ? (\n \n ) : (\n entry.component({ parameter, value, onUpdate })\n )}\n \n );\n })\n ) : (\n
\n No parameters available for this command.\n
\n )}\n
\n )}\n \n );\n}\n",
"type": "registry:component",
"target": "components/commandly/tool-renderer.tsx"
},
@@ -26,13 +26,13 @@
},
{
"path": "registry/commandly/types/flat.ts",
- "content": "export interface ToolInfo {\n description?: string;\n version?: string;\n url?: string;\n}\n\nexport interface Command {\n key: string;\n parentCommandKey?: string;\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n values: ParameterEnumValue[];\n allowMultiple?: boolean;\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n key: string;\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n key: string;\n parameterKey: string;\n dependsOnParameterKey: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n key: string;\n name: string;\n commandKey?: string;\n description?: string;\n group?: string;\n metadata?: ParameterMetadata;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: ParameterValidation[];\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n key?: string;\n commandKey?: string;\n name: string;\n exclusionType: ExclusionType;\n parameterKeys: string[];\n}\n\nexport interface Tool {\n name: string;\n displayName: string;\n info?: ToolInfo;\n commands: Command[];\n parameters: Parameter[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: ToolMetadata;\n}\n",
+ "content": "export interface ToolInfo {\n /** A brief human-readable description of what the tool does. */\n description?: string;\n /** The version string of the tool (e.g. \"1.0.0\"). */\n version?: string;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n}\n\nexport interface Command {\n /** Unique identifier for this command within the tool. */\n key: string;\n /** Key of the parent command; used to represent subcommand nesting. */\n parentCommandKey?: string;\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n /** The raw value passed to the CLI for this choice. */\n value: string;\n /** Human-readable label shown to the user for this enum choice. */\n displayName: string;\n /** Description of what this enum value does or represents. */\n description?: string;\n /** Whether this is the default selection when no value is provided. */\n isDefault?: boolean;\n /** Display sort position relative to sibling enum values. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n /** The list of allowed enum choices. */\n values: ParameterEnumValue[];\n /** Whether the user can select multiple values at once. */\n allowMultiple?: boolean;\n /** Separator character used when joining multiple selected values. */\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n /** Unique identifier for this validation rule. */\n key: string;\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. the max length number, or a regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n /** Unique identifier for this dependency rule. */\n key: string;\n /** Key of the parameter that owns this dependency. */\n parameterKey: string;\n /** Key of the parameter this dependency references. */\n dependsOnParameterKey: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n /** Arbitrary tags for categorising or filtering parameters. */\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n /** Unique identifier for this parameter within the tool. */\n key: string;\n /** Human-readable display name of the parameter. */\n name: string;\n /** Key of the command this parameter belongs to; omit for global parameters. */\n commandKey?: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: ParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n /** Unique identifier for this exclusion group. */\n key?: string;\n /** Key of the command this exclusion group belongs to; omit for global groups. */\n commandKey?: string;\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Keys of the parameters that participate in this exclusion group. */\n parameterKeys: string[];\n}\n\nexport interface Tool {\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** List of all commands and subcommands defined for this tool. */\n commands: Command[];\n /** Flat list of all parameters across all commands and global scope. */\n parameters: Parameter[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: ExclusionGroup[];\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n",
"type": "registry:file",
"target": "components/commandly/types/flat.ts"
},
{
"path": "registry/commandly/types/nested.ts",
- "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n group?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n info?: ToolInfo;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n",
+ "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. max length number or regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n /** Name of the parameter this dependency references. */\n dependsOnParameter: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n /** Human-readable display name of the parameter. */\n name: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: NestedParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder: number;\n /** Parameters that belong directly to this command. */\n parameters: NestedParameter[];\n /** Nested subcommands of this command. */\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Names of the parameters that participate in this exclusion group. */\n parameters: string[];\n}\n\nexport interface NestedTool {\n /** Optional JSON schema URI for validation. */\n $schema?: string;\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n /** Parameters that apply to all commands globally. */\n globalParameters: NestedParameter[];\n /** Hierarchical list of commands and their nested subcommands. */\n commands: NestedCommand[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: NestedExclusionGroup[] | null;\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n",
"type": "registry:file",
"target": "components/commandly/types/nested.ts"
}
diff --git a/public/r/ui.json b/public/r/ui.json
index c5e6b5c..f62eedd 100644
--- a/public/r/ui.json
+++ b/public/r/ui.json
@@ -29,13 +29,13 @@
},
{
"path": "registry/commandly/json-output.tsx",
- "content": "import { Tool } from \"@/components/commandly/types/flat\";\nimport { exportToStructuredJSON } from \"@/components/commandly/utils/flat\";\nimport { convertToNestedStructure } from \"@/components/commandly/utils/nested\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n \n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n \n \n {isEditing ? (\n \n
\n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : (\n \n \n {jsonString}\n \n \n \n \n )}\n \n \n );\n}\n",
+ "content": "import { Tool } from \"@/components/commandly/types/flat\";\nimport { exportToStructuredJSON } from \"@/components/commandly/utils/flat\";\nimport { convertToNestedStructure } from \"@/components/commandly/utils/nested\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardAction, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport {\n Command as UICommand,\n CommandGroup,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { ScrollArea, ScrollBar } from \"@/components/ui/scroll-area\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from \"lucide-react\";\nimport { useEffect, useMemo, useState } from \"react\";\nimport { toast } from \"sonner\";\n\nconst jsonOptions = [\n { value: \"nested\", label: \"Nested\" },\n { value: \"flat\", label: \"Flat\" },\n];\n\ntype DiffLine = { type: \"same\" | \"added\" | \"removed\"; text: string };\n\nfunction diffLines(before: string, after: string): DiffLine[] {\n const a = before.split(\"\\n\");\n const b = after.split(\"\\n\");\n const m = a.length;\n const n = b.length;\n const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));\n for (let i = 1; i <= m; i++)\n for (let j = 1; j <= n; j++)\n dp[i][j] =\n a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);\n const result: DiffLine[] = [];\n let i = m,\n j = n;\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {\n result.unshift({ type: \"same\", text: a[i - 1] });\n i--;\n j--;\n } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {\n result.unshift({ type: \"added\", text: b[j - 1] });\n j--;\n } else {\n result.unshift({ type: \"removed\", text: a[i - 1] });\n i--;\n }\n }\n return result;\n}\n\ninterface JsonTypeComponentProps {\n tool: Tool;\n originalTool?: Tool;\n onApply?: (tool: Tool) => void;\n}\n\nexport function JsonOutput({ tool, originalTool, onApply }: JsonTypeComponentProps) {\n const [open, setOpen] = useState(false);\n const [jsonString, setJsonString] = useState();\n const [originalJsonString, setOriginalJsonString] = useState();\n const [jsonType, setJsonType] = useState<\"nested\" | \"flat\">(\"flat\");\n const [isEditing, setIsEditing] = useState(false);\n const [editValue, setEditValue] = useState(\"\");\n const [showDiff, setShowDiff] = useState(true);\n\n useEffect(() => {\n const config =\n jsonType === \"flat\" ? exportToStructuredJSON(tool) : convertToNestedStructure(tool);\n setJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, tool]);\n\n useEffect(() => {\n if (!originalTool) {\n setOriginalJsonString(undefined);\n return;\n }\n const config =\n jsonType === \"flat\"\n ? exportToStructuredJSON(originalTool)\n : convertToNestedStructure(originalTool);\n setOriginalJsonString(JSON.stringify(config, null, 2));\n }, [jsonType, originalTool]);\n\n const diff = useMemo(() => {\n if (!originalJsonString || !jsonString || originalJsonString === jsonString) return null;\n return diffLines(originalJsonString, jsonString);\n }, [originalJsonString, jsonString]);\n\n const diffStats = useMemo(() => {\n if (!diff) return null;\n const added = diff.filter((l) => l.type === \"added\").length;\n const removed = diff.filter((l) => l.type === \"removed\").length;\n return { added, removed };\n }, [diff]);\n\n const handleEditToggle = () => {\n setEditValue(jsonString ?? \"\");\n setIsEditing(true);\n };\n\n const handleApply = () => {\n try {\n const parsed = JSON.parse(editValue) as Tool;\n onApply!(parsed);\n setIsEditing(false);\n } catch {\n toast.error(\"Invalid JSON\", { description: \"Please fix the JSON before applying.\" });\n }\n };\n\n const handleCancel = () => {\n setIsEditing(false);\n setEditValue(\"\");\n };\n\n return (\n \n \n \n Output type: \n \n \n \n {jsonOptions.find((option) => option.value === jsonType)?.label}\n \n \n \n \n \n \n \n {jsonOptions.map((option) => (\n {\n setJsonType(currentValue as \"nested\" | \"flat\");\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n \n \n {onApply && !isEditing && (\n \n \n \n )}\n {onApply && isEditing && (\n \n \n \n )}\n {\n navigator.clipboard.writeText(jsonString!);\n toast(\"Copied!\");\n }}\n >\n \n \n
\n \n \n {diffStats && !isEditing && (\n \n {diffStats.added > 0 && (\n \n +{diffStats.added} added\n \n )}\n {diffStats.removed > 0 && (\n \n -{diffStats.removed} removed\n \n )}\n setShowDiff((v) => !v)}\n >\n {showDiff ? \"Full view\" : \"Diff view\"}\n \n
\n )}\n {isEditing ? (\n \n
\n \n
\n \n Cancel\n \n \n Apply\n \n
\n
\n ) : diff && showDiff ? (\n \n \n {diff.map((line, idx) => (\n \n \n {line.type === \"added\" ? \"+ \" : line.type === \"removed\" ? \"- \" : \" \"}\n \n {line.text}\n
\n ))}\n \n \n \n \n ) : (\n \n \n {jsonString}\n \n \n \n \n )}\n \n \n );\n}\n",
"type": "registry:component",
"target": "components/commandly/json-output.tsx"
},
{
"path": "registry/commandly/tool-renderer.tsx",
- "content": "import { ParameterValue } from \"@/components/commandly/types/flat\";\nimport { Command, Tool } from \"@/components/commandly/types/flat\";\nimport {\n ParameterRenderContext,\n ParameterRendererEntry,\n} from \"@/components/commandly/types/renderer\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command as UICommand,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, InfoIcon, PlusIcon, XIcon } from \"lucide-react\";\nimport React from \"react\";\n\nconst findDefaultCommand = (tool: Tool): Command | null => {\n const defaultCommand = tool.commands.find((command) => command.isDefault);\n if (defaultCommand) return defaultCommand;\n\n const nameMatchCommand = tool.commands.find(\n (command) => command.name.toLowerCase() === tool.name.toLowerCase(),\n );\n if (nameMatchCommand) return nameMatchCommand;\n\n return tool.commands.length > 0 ? tool.commands[0] : null;\n};\n\ninterface ParameterLabelProps {\n name: string;\n longFlag?: string;\n shortFlag?: string;\n isRequired?: boolean;\n isGlobal?: boolean;\n description?: string;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction ParameterLabel({\n name,\n longFlag,\n shortFlag,\n isRequired,\n isGlobal,\n description,\n className,\n children,\n}: ParameterLabelProps) {\n return (\n \n {name}\n {(longFlag || shortFlag) && (\n \n ({[longFlag, shortFlag].filter(Boolean).join(\", \")})\n \n )}\n {isRequired && * }\n \n \n \n \n \n {description} \n \n \n {children}\n {isGlobal && (\n \n global\n \n )}\n \n );\n}\n\nfunction FlagInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n );\n}\n\nfunction OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n const [open, setOpen] = React.useState(false);\n const separator = parameter.enum?.separator || \",\";\n const options =\n parameter.enum?.values?.map((e) => ({\n value: e.value,\n label: e.displayName || e.value,\n })) ?? [];\n\n const label = (\n \n );\n\n if (parameter.enum?.allowMultiple) {\n const selected = value ? (value as string).split(separator).filter(Boolean) : [];\n return (\n \n {label}\n onUpdate(vals.join(separator))}\n placeholder=\"Select options\"\n />\n
\n );\n }\n\n return (\n \n {label}\n
\n \n \n {options.find((o) => o.value === value)?.label ?? \"Select an option\"}\n \n \n \n \n \n \n \n No option found. \n \n {options.map((option) => (\n {\n onUpdate(currentValue === value ? \"\" : currentValue);\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n
\n );\n}\n\nfunction OptionBooleanInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
onUpdate(checked.toString())}\n />\n \n \n );\n}\n\nfunction OptionTextInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
\n
onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\nfunction ArgumentInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
\n \n {parameter.parameterType}\n {parameter.position !== undefined && ` (${parameter.position})`}\n \n \n
onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\ninterface RepeatableWrapperProps {\n parameter: ParameterRenderContext[\"parameter\"];\n value: ParameterRenderContext[\"value\"];\n onUpdate: ParameterRenderContext[\"onUpdate\"];\n renderEntry: ParameterRendererEntry[\"component\"];\n}\n\nfunction RepeatableWrapper({ parameter, value, onUpdate, renderEntry }: RepeatableWrapperProps) {\n const toArray = (v: ParameterRenderContext[\"value\"]): string[] => {\n if (Array.isArray(v)) return v;\n if (v !== undefined && v !== \"\" && v !== false) return [String(v)];\n return [\"\"];\n };\n\n const values = toArray(value);\n\n const updateAt = (index: number, val: ParameterRenderContext[\"value\"]) => {\n const next = [...values];\n next[index] = String(val);\n onUpdate(next);\n };\n\n const addRow = () => onUpdate([...values, \"\"]);\n\n const removeAt = (index: number) => {\n const next = values.filter((_, i) => i !== index);\n onUpdate(next);\n };\n\n return (\n \n {values.map((val, index) => (\n
\n
\n {renderEntry({ parameter, value: val, onUpdate: (v) => updateAt(index, v) })}\n
\n {index > 0 && (\n
removeAt(index)}\n >\n \n \n )}\n
\n ))}\n
\n \n Add another\n \n
\n );\n}\n\nexport function defaultComponents(): ParameterRendererEntry[] {\n return [\n { condition: (p) => p.parameterType === \"Flag\", component: (ctx) => },\n {\n condition: (p) => p.parameterType === \"Argument\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Enum\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Boolean\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\",\n component: (ctx) => ,\n },\n ];\n}\n\ninterface ToolRendererProps {\n selectedCommand?: Command | null;\n tool: Tool;\n catalog?: ParameterRendererEntry[];\n parameterValues: Record;\n updateParameterValue: (parameterKey: string, value: ParameterValue) => void;\n}\n\nexport function ToolRenderer({\n selectedCommand: providedCommand,\n tool,\n catalog = defaultComponents(),\n parameterValues,\n updateParameterValue,\n}: ToolRendererProps) {\n const selectedCommand = providedCommand ?? findDefaultCommand(tool);\n\n return (\n \n {selectedCommand && tool.commands.length === 0 ? (\n No commands available for this tool.
\n ) : (\n \n {tool.parameters.length > 0 ? (\n tool.parameters\n .filter((param) => param.commandKey === selectedCommand?.key || param.isGlobal)\n .map((parameter) => {\n const value = parameterValues[parameter.key] ?? \"\";\n const onUpdate = (val: ParameterValue) => updateParameterValue(parameter.key, val);\n const entry = catalog.find((e) => e.condition(parameter));\n if (!entry) return null;\n return (\n
\n {parameter.isRepeatable ? (\n \n ) : (\n entry.component({ parameter, value, onUpdate })\n )}\n \n );\n })\n ) : (\n
\n No parameters available for this command.\n
\n )}\n
\n )}\n \n );\n}\n",
+ "content": "import { ParameterValue } from \"@/components/commandly/types/flat\";\nimport { Command, Tool } from \"@/components/commandly/types/flat\";\nimport {\n ParameterRenderContext,\n ParameterRendererEntry,\n} from \"@/components/commandly/types/renderer\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n Command as UICommand,\n CommandEmpty,\n CommandGroup,\n CommandInput,\n CommandItem,\n CommandList,\n} from \"@/components/ui/command\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { MultiSelect } from \"@/components/ui/multi-select\";\nimport { Popover, PopoverContent, PopoverTrigger } from \"@/components/ui/popover\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { CheckIcon, ChevronsUpDownIcon, InfoIcon, PlusIcon, XIcon } from \"lucide-react\";\nimport React from \"react\";\n\nconst findDefaultCommand = (tool: Tool): Command | null => {\n const nameMatchCommand = tool.commands.find(\n (command) => command.name.toLowerCase() === tool.name.toLowerCase(),\n );\n if (nameMatchCommand) return nameMatchCommand;\n\n return tool.commands.length > 0 ? tool.commands[0] : null;\n};\n\ninterface ParameterLabelProps {\n name: string;\n longFlag?: string;\n shortFlag?: string;\n isRequired?: boolean;\n isGlobal?: boolean;\n description?: string;\n className?: string;\n children?: React.ReactNode;\n}\n\nfunction ParameterLabel({\n name,\n longFlag,\n shortFlag,\n isRequired,\n isGlobal,\n description,\n className,\n children,\n}: ParameterLabelProps) {\n return (\n \n {name}\n {(longFlag || shortFlag) && (\n \n ({[longFlag, shortFlag].filter(Boolean).join(\", \")})\n \n )}\n {isRequired && * }\n \n \n \n \n \n {description} \n \n \n {children}\n {isGlobal && (\n \n global\n \n )}\n \n );\n}\n\nfunction FlagInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n );\n}\n\nfunction OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n const [open, setOpen] = React.useState(false);\n const separator = parameter.enum?.separator || \",\";\n const options =\n parameter.enum?.values?.map((e) => ({\n value: e.value,\n label: e.displayName || e.value,\n })) ?? [];\n\n const label = (\n \n );\n\n if (parameter.enum?.allowMultiple) {\n const selected = Array.isArray(value)\n ? (value as string[]).filter(Boolean)\n : value\n ? (value as string).split(separator).filter(Boolean)\n : [];\n return (\n \n {label}\n onUpdate(vals.join(separator))}\n placeholder=\"Select options\"\n />\n
\n );\n }\n\n return (\n \n {label}\n
\n \n \n {options.find((o) => o.value === value)?.label ?? \"Select an option\"}\n \n \n \n \n \n \n \n No option found. \n \n {options.map((option) => (\n {\n onUpdate(currentValue === value ? \"\" : currentValue);\n setOpen(false);\n }}\n >\n {option.label}\n \n \n ))}\n \n \n \n \n \n
\n );\n}\n\nfunction OptionBooleanInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
onUpdate(checked.toString())}\n />\n \n \n );\n}\n\nfunction OptionTextInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
\n
onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\nfunction ArgumentInput({ parameter, value, onUpdate }: ParameterRenderContext) {\n return (\n \n
\n \n {parameter.parameterType}\n {parameter.position !== undefined && ` (${parameter.position})`}\n \n \n
onUpdate(e.target.value)}\n placeholder=\"Enter value\"\n />\n
\n );\n}\n\ninterface RepeatableWrapperProps {\n parameter: ParameterRenderContext[\"parameter\"];\n value: ParameterRenderContext[\"value\"];\n onUpdate: ParameterRenderContext[\"onUpdate\"];\n renderEntry: ParameterRendererEntry[\"component\"];\n}\n\nfunction RepeatableWrapper({ parameter, value, onUpdate, renderEntry }: RepeatableWrapperProps) {\n const toArray = (v: ParameterRenderContext[\"value\"]): string[] => {\n if (Array.isArray(v)) return v;\n if (v !== undefined && v !== \"\" && v !== false) return [String(v)];\n return [\"\"];\n };\n\n const values = toArray(value);\n\n const updateAt = (index: number, val: ParameterRenderContext[\"value\"]) => {\n const next = [...values];\n next[index] = String(val);\n onUpdate(next);\n };\n\n const addRow = () => onUpdate([...values, \"\"]);\n\n const removeAt = (index: number) => {\n const next = values.filter((_, i) => i !== index);\n onUpdate(next);\n };\n\n return (\n \n {values.map((val, index) => (\n
\n
\n {renderEntry({ parameter, value: val, onUpdate: (v) => updateAt(index, v) })}\n
\n {index > 0 && (\n
removeAt(index)}\n >\n \n \n )}\n
\n ))}\n
\n \n Add another\n \n
\n );\n}\n\nexport function defaultComponents(): ParameterRendererEntry[] {\n return [\n { condition: (p) => p.parameterType === \"Flag\", component: (ctx) => },\n {\n condition: (p) => p.parameterType === \"Argument\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Enum\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\" && p.dataType === \"Boolean\",\n component: (ctx) => ,\n },\n {\n condition: (p) => p.parameterType === \"Option\",\n component: (ctx) => ,\n },\n ];\n}\n\ninterface ToolRendererProps {\n selectedCommand?: Command | null;\n tool: Tool;\n catalog?: ParameterRendererEntry[];\n parameterValues: Record;\n updateParameterValue: (parameterKey: string, value: ParameterValue) => void;\n}\n\nexport function ToolRenderer({\n selectedCommand: providedCommand,\n tool,\n catalog = defaultComponents(),\n parameterValues,\n updateParameterValue,\n}: ToolRendererProps) {\n const selectedCommand = providedCommand ?? findDefaultCommand(tool);\n\n return (\n \n {selectedCommand && tool.commands.length === 0 ? (\n No commands available for this tool.
\n ) : (\n \n {tool.parameters.length > 0 ? (\n tool.parameters\n .filter((param) => param.commandKey === selectedCommand?.key || param.isGlobal)\n .map((parameter) => {\n const value = parameterValues[parameter.key] ?? \"\";\n const onUpdate = (val: ParameterValue) => updateParameterValue(parameter.key, val);\n const entry = catalog.find((e) => e.condition(parameter));\n if (!entry) return null;\n return (\n
\n {parameter.isRepeatable ? (\n \n ) : (\n entry.component({ parameter, value, onUpdate })\n )}\n \n );\n })\n ) : (\n
\n No parameters available for this command.\n
\n )}\n
\n )}\n \n );\n}\n",
"type": "registry:component",
"target": "components/commandly/tool-renderer.tsx"
},
@@ -47,25 +47,25 @@
},
{
"path": "registry/commandly/types/flat.ts",
- "content": "export interface ToolInfo {\n description?: string;\n version?: string;\n url?: string;\n}\n\nexport interface Command {\n key: string;\n parentCommandKey?: string;\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n value: string;\n displayName: string;\n description?: string;\n isDefault?: boolean;\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n values: ParameterEnumValue[];\n allowMultiple?: boolean;\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n key: string;\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n key: string;\n parameterKey: string;\n dependsOnParameterKey: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n key: string;\n name: string;\n commandKey?: string;\n description?: string;\n group?: string;\n metadata?: ParameterMetadata;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: ParameterValidation[];\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n key?: string;\n commandKey?: string;\n name: string;\n exclusionType: ExclusionType;\n parameterKeys: string[];\n}\n\nexport interface Tool {\n name: string;\n displayName: string;\n info?: ToolInfo;\n commands: Command[];\n parameters: Parameter[];\n exclusionGroups?: ExclusionGroup[];\n metadata?: ToolMetadata;\n}\n",
+ "content": "export interface ToolInfo {\n /** A brief human-readable description of what the tool does. */\n description?: string;\n /** The version string of the tool (e.g. \"1.0.0\"). */\n version?: string;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n}\n\nexport interface Command {\n /** Unique identifier for this command within the tool. */\n key: string;\n /** Key of the parent command; used to represent subcommand nesting. */\n parentCommandKey?: string;\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValue {\n /** The raw value passed to the CLI for this choice. */\n value: string;\n /** Human-readable label shown to the user for this enum choice. */\n displayName: string;\n /** Description of what this enum value does or represents. */\n description?: string;\n /** Whether this is the default selection when no value is provided. */\n isDefault?: boolean;\n /** Display sort position relative to sibling enum values. */\n sortOrder?: number;\n}\n\nexport interface ParameterEnumValues {\n /** The list of allowed enum choices. */\n values: ParameterEnumValue[];\n /** Whether the user can select multiple values at once. */\n allowMultiple?: boolean;\n /** Separator character used when joining multiple selected values. */\n separator?: string;\n}\n\nexport type ParameterValidationType =\n | \"min_length\"\n | \"max_length\"\n | \"min_value\"\n | \"max_value\"\n | \"regex\";\n\nexport interface ParameterValidation {\n /** Unique identifier for this validation rule. */\n key: string;\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. the max length number, or a regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport type ParameterDependencyType = \"requires\" | \"conflicts_with\";\n\nexport interface ParameterDependency {\n /** Unique identifier for this dependency rule. */\n key: string;\n /** Key of the parameter that owns this dependency. */\n parameterKey: string;\n /** Key of the parameter this dependency references. */\n dependsOnParameterKey: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport type ParameterValue = string | number | boolean | string[];\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ToolMetadata {}\n\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface ParameterMetadata {\n /** Arbitrary tags for categorising or filtering parameters. */\n tags?: string[];\n}\n\nexport type ParameterType = \"Flag\" | \"Option\" | \"Argument\";\n\nexport type ParameterDataType = \"String\" | \"Number\" | \"Boolean\" | \"Enum\";\n\nexport interface Parameter {\n /** Unique identifier for this parameter within the tool. */\n key: string;\n /** Human-readable display name of the parameter. */\n name: string;\n /** Key of the command this parameter belongs to; omit for global parameters. */\n commandKey?: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: ParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: ParameterDependency[];\n}\n\nexport type ExclusionType = \"mutual_exclusive\" | \"required_one_of\";\n\nexport interface ExclusionGroup {\n /** Unique identifier for this exclusion group. */\n key?: string;\n /** Key of the command this exclusion group belongs to; omit for global groups. */\n commandKey?: string;\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Keys of the parameters that participate in this exclusion group. */\n parameterKeys: string[];\n}\n\nexport interface Tool {\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** List of all commands and subcommands defined for this tool. */\n commands: Command[];\n /** Flat list of all parameters across all commands and global scope. */\n parameters: Parameter[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: ExclusionGroup[];\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n",
"type": "registry:file",
"target": "components/commandly/types/flat.ts"
},
{
"path": "registry/commandly/types/nested.ts",
- "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n validationType: ParameterValidationType;\n validationValue: string;\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n dependsOnParameter: string;\n dependencyType: ParameterDependencyType;\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n name: string;\n description?: string;\n group?: string;\n parameterType: ParameterType;\n dataType: ParameterDataType;\n metadata?: ParameterMetadata;\n isRequired?: boolean;\n isRepeatable?: boolean;\n isGlobal?: boolean;\n shortFlag?: string;\n longFlag?: string;\n position?: number;\n sortOrder?: number;\n arraySeparator?: string;\n keyValueSeparator?: string;\n enum?: ParameterEnumValues;\n validations?: NestedParameterValidation[];\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n name: string;\n description?: string;\n interactive?: boolean;\n isDefault: boolean;\n sortOrder: number;\n parameters: NestedParameter[];\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n name: string;\n exclusionType: ExclusionType;\n parameters: string[];\n}\n\nexport interface NestedTool {\n $schema?: string;\n name: string;\n displayName: string;\n info?: ToolInfo;\n url?: string;\n globalParameters: NestedParameter[];\n commands: NestedCommand[];\n exclusionGroups?: NestedExclusionGroup[] | null;\n metadata?: ToolMetadata;\n}\n",
+ "content": "import type {\n ExclusionType,\n ParameterDataType,\n ParameterDependencyType,\n ParameterEnumValues,\n ParameterMetadata,\n ParameterType,\n ParameterValidationType,\n ToolInfo,\n ToolMetadata,\n} from \"@/components/commandly/types/flat\";\n\nexport interface NestedParameterValidation {\n /** The type of validation to apply. */\n validationType: ParameterValidationType;\n /** The value to validate against (e.g. max length number or regex pattern). */\n validationValue: string;\n /** The error message to display when validation fails. */\n errorMessage: string;\n}\n\nexport interface NestedParameterDependency {\n /** Name of the parameter this dependency references. */\n dependsOnParameter: string;\n /** Whether this parameter requires or conflicts with the referenced parameter. */\n dependencyType: ParameterDependencyType;\n /** Optional value that the referenced parameter must have for this dependency to apply. */\n conditionValue?: string;\n}\n\nexport interface NestedParameter {\n /** Human-readable display name of the parameter. */\n name: string;\n /** Brief description of what this parameter does or accepts. */\n description?: string;\n /** Optional grouping label for organising related parameters in the UI. */\n group?: string;\n /** Whether this is a boolean flag, a key-value option, or a positional argument. */\n parameterType: ParameterType;\n /** The data type of the parameter's value. */\n dataType: ParameterDataType;\n /** Additional metadata such as tags. */\n metadata?: ParameterMetadata;\n /** Whether the user must provide this parameter. */\n isRequired?: boolean;\n /** Whether this parameter can be specified multiple times. */\n isRepeatable?: boolean;\n /** Whether this parameter applies to all commands rather than a single command. */\n isGlobal?: boolean;\n /** The single-character short flag (e.g. \"-v\"). */\n shortFlag?: string;\n /** The long-form flag or option name (e.g. \"--verbose\"). */\n longFlag?: string;\n /** Zero-based position index for positional arguments. */\n position?: number;\n /** Display sort position relative to sibling parameters. */\n sortOrder?: number;\n /** Separator character used when the option accepts multiple values in one argument (e.g. \",\"). */\n arraySeparator?: string;\n /** Separator between key and value for key=value style options (e.g. \"=\"). */\n keyValueSeparator?: string;\n /** Allowed enum choices when dataType is \"Enum\". */\n enum?: ParameterEnumValues;\n /** Validation rules applied to this parameter's value. */\n validations?: NestedParameterValidation[];\n /** Dependencies on other parameters (requires or conflicts-with relationships). */\n dependencies?: NestedParameterDependency[];\n}\n\nexport interface NestedCommand {\n /** Human-readable display name of the command. */\n name: string;\n /** Brief description of what this command does. */\n description?: string;\n /** Whether this command opens an interactive session or prompt. */\n interactive?: boolean;\n /** Display sort position relative to sibling commands. */\n sortOrder: number;\n /** Parameters that belong directly to this command. */\n parameters: NestedParameter[];\n /** Nested subcommands of this command. */\n subcommands: NestedCommand[];\n}\n\nexport interface NestedExclusionGroup {\n /** Human-readable name for this exclusion group. */\n name: string;\n /** Whether parameters in this group are mutually exclusive or one is required. */\n exclusionType: ExclusionType;\n /** Names of the parameters that participate in this exclusion group. */\n parameters: string[];\n}\n\nexport interface NestedTool {\n /** Optional JSON schema URI for validation. */\n $schema?: string;\n /** Unique machine-readable identifier for the tool (e.g. \"httpx\"). */\n name: string;\n /** Human-readable display name for the tool (e.g. \"HTTPx\"). */\n displayName: string;\n /** General information about the tool such as description, version, and URL. */\n info?: ToolInfo;\n /** The homepage or documentation URL for the tool. */\n url?: string;\n /** Parameters that apply to all commands globally. */\n globalParameters: NestedParameter[];\n /** Hierarchical list of commands and their nested subcommands. */\n commands: NestedCommand[];\n /** Groups of parameters with mutual exclusion or required-one-of constraints. */\n exclusionGroups?: NestedExclusionGroup[] | null;\n /** Arbitrary metadata attached to the tool. */\n metadata?: ToolMetadata;\n}\n",
"type": "registry:file",
"target": "components/commandly/types/nested.ts"
},
{
"path": "registry/commandly/utils/flat.ts",
- "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n if (command.name === tool.name && command.isDefault) {\n return tool.name;\n }\n\n const rootCommand = tool.commands.find((c) => c.name === tool.name);\n if (rootCommand?.isDefault && path[0] === tool.name) {\n path[0] = tool.name;\n }\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n url: tool.info?.url,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewCommand = (parentKey?: string): Command => {\n const name = randomCommandName();\n return {\n key: slugify(name),\n parentCommandKey: parentKey,\n name,\n isDefault: false,\n sortOrder: 1,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nexport const randomCommandName = () => {\n const characters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let result = \"\";\n const charactersLength = characters.length;\n for (let i = 0; i < 7; i++) {\n result += characters.charAt(Math.floor(Math.random() * charactersLength));\n }\n return result;\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n",
+ "content": "import type { Command, Parameter, Tool } from \"@/components/commandly/types/flat\";\n\nexport const slugify = (text: string): string => {\n return text\n .toString()\n .toLowerCase()\n .trim()\n .replace(/\\s+/g, \"-\") // Replace spaces with -\n .replace(/[^\\w-]+/g, \"\") // Remove all non-word chars\n .replace(/--+/g, \"-\") // Replace multiple - with single -\n .replace(/^-+/, \"\") // Trim - from start of text\n .replace(/-+$/, \"\"); // Trim - from end of text\n};\n\nexport const getCommandPath = (command: Command, tool: Tool): string => {\n const findCommandPath = (\n targetKey: string,\n commands: Command[],\n path: string[] = [],\n ): string[] | null => {\n for (const cmd of commands) {\n if (cmd.name === targetKey) {\n return [...path, cmd.name];\n }\n\n const childCommands = commands.filter((c) => c.parentCommandKey === cmd.key);\n if (childCommands.length > 0) {\n const subPath = findCommandPath(targetKey, childCommands, [...path, cmd.name]);\n if (subPath) {\n return subPath;\n }\n }\n }\n return null;\n };\n\n const rootCommands = tool.commands.filter((c) => !c.parentCommandKey);\n const path = findCommandPath(command.name, rootCommands);\n\n if (!path) return command.name;\n\n return path.join(\" \");\n};\n\nexport const getAllSubcommands = (commandKey: string, commands: Command[]): Command[] => {\n const result: Command[] = [];\n\n const findSubcommands = (parentKey: string) => {\n commands.forEach((cmd) => {\n if (cmd.parentCommandKey === parentKey) {\n result.push(cmd);\n findSubcommands(cmd.key);\n }\n });\n };\n\n findSubcommands(commandKey);\n return result;\n};\n\nconst SCHEMA_URL = \"https://commandly.divyeshio.in/specification/flat.json\";\n\nexport const sanitizeToolJSON = (tool: Tool) => {\n const parameters = tool.parameters.map(({ metadata: _metadata, ...param }) => param);\n\n return {\n $schema: SCHEMA_URL,\n ...tool,\n parameters,\n };\n};\n\nexport const exportToStructuredJSON = (tool: Tool) => {\n return {\n $schema: SCHEMA_URL,\n name: tool.name,\n displayName: tool.displayName,\n info: tool.info,\n commands: tool.commands.map((cmd) => ({ ...cmd })),\n parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),\n exclusionGroups: tool.exclusionGroups,\n metadata: tool.metadata,\n };\n};\n\nexport const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {\n return {\n key: \"\",\n name: \"\",\n commandKey: isGlobal ? undefined : commandKey,\n parameterType: \"Option\",\n dataType: \"String\",\n ...(isGlobal ? { isGlobal: true } : {}),\n longFlag: \"\",\n };\n};\n\nconst isEmpty = (value: object | null | undefined): boolean => {\n if (value == null) return true;\n if (Array.isArray(value)) return value.length === 0;\n return Object.keys(value).length === 0;\n};\n\nconst cleanParameter = (param: Parameter): Parameter => {\n const cleaned = { ...param };\n\n if (!cleaned.enum || cleaned.enum.values.length === 0) delete cleaned.enum;\n if (isEmpty(cleaned.validations)) delete cleaned.validations;\n if (isEmpty(cleaned.dependencies)) delete cleaned.dependencies;\n\n if (cleaned.metadata) {\n const meta = { ...cleaned.metadata };\n if (isEmpty(meta.tags)) delete meta.tags;\n if (isEmpty(meta)) {\n delete cleaned.metadata;\n } else {\n cleaned.metadata = meta;\n }\n }\n\n return cleaned;\n};\n\nexport const cleanupTool = (tool: Tool): Tool => {\n const cleaned = { ...tool };\n\n if (isEmpty(cleaned.exclusionGroups)) delete cleaned.exclusionGroups;\n if (isEmpty(cleaned.metadata)) delete cleaned.metadata;\n\n cleaned.parameters = cleaned.parameters.map(cleanParameter);\n\n return cleaned;\n};\n",
"type": "registry:file",
"target": "components/commandly/utils/flat.ts"
},
{
"path": "registry/commandly/utils/nested.ts",
- "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n isDefault: cmd.isDefault ?? false,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n",
+ "content": "import type { Tool, Command, Parameter } from \"@/components/commandly/types/flat\";\nimport {\n NestedCommand,\n NestedExclusionGroup,\n NestedParameter,\n NestedTool,\n} from \"@/components/commandly/types/nested\";\n\nexport const convertToNestedStructure = (tool: Tool): NestedTool => {\n const globalParameters = tool.parameters.filter((p) => p.isGlobal);\n\n const convertParameter = (param: Parameter): NestedParameter => {\n const { ...rest } = param;\n return {\n ...rest,\n validations: param.validations?.map((v) => {\n return {\n validationType: v.validationType,\n validationValue: v.validationValue,\n errorMessage: v.errorMessage,\n };\n }),\n metadata: param.metadata,\n dataType: param.dataType,\n dependencies: param.dependencies?.map((dep) => {\n const dependsOnParam = tool.parameters.find((p) => p.key === dep.dependsOnParameterKey);\n return {\n dependsOnParameter: dependsOnParam?.longFlag || \"\",\n dependencyType: dep.dependencyType,\n conditionValue: dep.conditionValue,\n };\n }),\n };\n };\n\n const buildNestedCommands = (commands: Command[], parentKey?: string): NestedCommand[] => {\n return commands\n .filter((cmd) => cmd.parentCommandKey === parentKey)\n .map((cmd) => {\n const commandParameters = tool.parameters.filter(\n (p) => p.commandKey === cmd.key && !p.isGlobal,\n );\n return {\n name: cmd.name,\n description: cmd.description,\n interactive: cmd.interactive,\n sortOrder: cmd.sortOrder ?? 0,\n parameters: commandParameters.map(convertParameter),\n subcommands: buildNestedCommands(commands, cmd.key),\n };\n });\n };\n\n const nestedExclusionGroups: NestedExclusionGroup[] | undefined = tool.exclusionGroups?.map(\n (group) => {\n return {\n name: group.name,\n exclusionType: group.exclusionType,\n parameters: group.parameterKeys.map((pk) => {\n const param = tool.parameters.find((p) => p.key === pk);\n return param?.longFlag || \"\";\n }),\n };\n },\n );\n\n return {\n $schema: \"https://commandly.divyeshio.in/specification/nested.json\",\n name: tool.name,\n url: tool.info?.url,\n displayName: tool.displayName,\n info: tool.info,\n metadata: tool.metadata,\n globalParameters: globalParameters.map(convertParameter),\n commands: buildNestedCommands(tool.commands),\n exclusionGroups: nestedExclusionGroups,\n };\n};\n",
"type": "registry:file",
"target": "components/commandly/utils/nested.ts"
}
diff --git a/public/specification/flat.json b/public/specification/flat.json
index bc38b91..e847c65 100644
--- a/public/specification/flat.json
+++ b/public/specification/flat.json
@@ -2,33 +2,40 @@
"type": "object",
"properties": {
"name": {
+ "description": "Unique machine-readable identifier for the tool (e.g. \"httpx\").",
"type": "string"
},
"displayName": {
+ "description": "Human-readable display name for the tool (e.g. \"HTTPx\").",
"type": "string"
},
"info": {
+ "description": "General information about the tool such as description, version, and URL.",
"$ref": "#/definitions/ToolInfo"
},
"commands": {
+ "description": "List of all commands and subcommands defined for this tool.",
"type": "array",
"items": {
"$ref": "#/definitions/Command"
}
},
"parameters": {
+ "description": "Flat list of all parameters across all commands and global scope.",
"type": "array",
"items": {
"$ref": "#/definitions/Parameter"
}
},
"exclusionGroups": {
+ "description": "Groups of parameters with mutual exclusion or required-one-of constraints.",
"type": "array",
"items": {
"$ref": "#/definitions/ExclusionGroup"
}
},
"metadata": {
+ "description": "Arbitrary metadata attached to the tool.",
"$ref": "#/definitions/ToolMetadata"
}
},
@@ -43,12 +50,15 @@
"type": "object",
"properties": {
"description": {
+ "description": "A brief human-readable description of what the tool does.",
"type": "string"
},
"version": {
+ "description": "The version string of the tool (e.g. \"1.0.0\").",
"type": "string"
},
"url": {
+ "description": "The homepage or documentation URL for the tool.",
"type": "string"
}
}
@@ -57,24 +67,27 @@
"type": "object",
"properties": {
"key": {
+ "description": "Unique identifier for this command within the tool.",
"type": "string"
},
"parentCommandKey": {
+ "description": "Key of the parent command; used to represent subcommand nesting.",
"type": "string"
},
"name": {
+ "description": "Human-readable display name of the command.",
"type": "string"
},
"description": {
+ "description": "Brief description of what this command does.",
"type": "string"
},
"interactive": {
- "type": "boolean"
- },
- "isDefault": {
+ "description": "Whether this command opens an interactive session or prompt.",
"type": "boolean"
},
"sortOrder": {
+ "description": "Display sort position relative to sibling commands.",
"type": "number"
}
},
@@ -87,66 +100,86 @@
"type": "object",
"properties": {
"key": {
+ "description": "Unique identifier for this parameter within the tool.",
"type": "string"
},
"name": {
+ "description": "Human-readable display name of the parameter.",
"type": "string"
},
"commandKey": {
+ "description": "Key of the command this parameter belongs to; omit for global parameters.",
"type": "string"
},
"description": {
+ "description": "Brief description of what this parameter does or accepts.",
"type": "string"
},
"group": {
+ "description": "Optional grouping label for organising related parameters in the UI.",
"type": "string"
},
"metadata": {
+ "description": "Additional metadata such as tags.",
"$ref": "#/definitions/ParameterMetadata"
},
"parameterType": {
- "$ref": "#/definitions/ParameterType"
+ "$ref": "#/definitions/ParameterType",
+ "description": "Whether this is a boolean flag, a key-value option, or a positional argument."
},
"dataType": {
- "$ref": "#/definitions/ParameterDataType"
+ "$ref": "#/definitions/ParameterDataType",
+ "description": "The data type of the parameter's value."
},
"isRequired": {
+ "description": "Whether the user must provide this parameter.",
"type": "boolean"
},
"isRepeatable": {
+ "description": "Whether this parameter can be specified multiple times.",
"type": "boolean"
},
"isGlobal": {
+ "description": "Whether this parameter applies to all commands rather than a single command.",
"type": "boolean"
},
"shortFlag": {
+ "description": "The single-character short flag (e.g. \"-v\").",
"type": "string"
},
"longFlag": {
+ "description": "The long-form flag or option name (e.g. \"--verbose\").",
"type": "string"
},
"position": {
+ "description": "Zero-based position index for positional arguments.",
"type": "number"
},
"sortOrder": {
+ "description": "Display sort position relative to sibling parameters.",
"type": "number"
},
"arraySeparator": {
+ "description": "Separator character used when the option accepts multiple values in one argument (e.g. \",\").",
"type": "string"
},
"keyValueSeparator": {
+ "description": "Separator between key and value for key=value style options (e.g. \"=\").",
"type": "string"
},
"enum": {
+ "description": "Allowed enum choices when dataType is \"Enum\".",
"$ref": "#/definitions/ParameterEnumValues"
},
"validations": {
+ "description": "Validation rules applied to this parameter's value.",
"type": "array",
"items": {
"$ref": "#/definitions/ParameterValidation"
}
},
"dependencies": {
+ "description": "Dependencies on other parameters (requires or conflicts-with relationships).",
"type": "array",
"items": {
"$ref": "#/definitions/ParameterDependency"
@@ -164,6 +197,7 @@
"type": "object",
"properties": {
"tags": {
+ "description": "Arbitrary tags for categorising or filtering parameters.",
"type": "array",
"items": {
"type": "string"
@@ -192,15 +226,18 @@
"type": "object",
"properties": {
"values": {
+ "description": "The list of allowed enum choices.",
"type": "array",
"items": {
"$ref": "#/definitions/ParameterEnumValue"
}
},
"allowMultiple": {
+ "description": "Whether the user can select multiple values at once.",
"type": "boolean"
},
"separator": {
+ "description": "Separator character used when joining multiple selected values.",
"type": "string"
}
},
@@ -212,18 +249,23 @@
"type": "object",
"properties": {
"value": {
+ "description": "The raw value passed to the CLI for this choice.",
"type": "string"
},
"displayName": {
+ "description": "Human-readable label shown to the user for this enum choice.",
"type": "string"
},
"description": {
+ "description": "Description of what this enum value does or represents.",
"type": "string"
},
"isDefault": {
+ "description": "Whether this is the default selection when no value is provided.",
"type": "boolean"
},
"sortOrder": {
+ "description": "Display sort position relative to sibling enum values.",
"type": "number"
}
},
@@ -236,15 +278,19 @@
"type": "object",
"properties": {
"key": {
+ "description": "Unique identifier for this validation rule.",
"type": "string"
},
"validationType": {
- "$ref": "#/definitions/ParameterValidationType"
+ "$ref": "#/definitions/ParameterValidationType",
+ "description": "The type of validation to apply."
},
"validationValue": {
+ "description": "The value to validate against (e.g. the max length number, or a regex pattern).",
"type": "string"
},
"errorMessage": {
+ "description": "The error message to display when validation fails.",
"type": "string"
}
},
@@ -269,18 +315,23 @@
"type": "object",
"properties": {
"key": {
+ "description": "Unique identifier for this dependency rule.",
"type": "string"
},
"parameterKey": {
+ "description": "Key of the parameter that owns this dependency.",
"type": "string"
},
"dependsOnParameterKey": {
+ "description": "Key of the parameter this dependency references.",
"type": "string"
},
"dependencyType": {
- "$ref": "#/definitions/ParameterDependencyType"
+ "$ref": "#/definitions/ParameterDependencyType",
+ "description": "Whether this parameter requires or conflicts with the referenced parameter."
},
"conditionValue": {
+ "description": "Optional value that the referenced parameter must have for this dependency to apply.",
"type": "string"
}
},
@@ -302,18 +353,23 @@
"type": "object",
"properties": {
"key": {
+ "description": "Unique identifier for this exclusion group.",
"type": "string"
},
"commandKey": {
+ "description": "Key of the command this exclusion group belongs to; omit for global groups.",
"type": "string"
},
"name": {
+ "description": "Human-readable name for this exclusion group.",
"type": "string"
},
"exclusionType": {
- "$ref": "#/definitions/ExclusionType"
+ "$ref": "#/definitions/ExclusionType",
+ "description": "Whether parameters in this group are mutually exclusive or one is required."
},
"parameterKeys": {
+ "description": "Keys of the parameters that participate in this exclusion group.",
"type": "array",
"items": {
"type": "string"
diff --git a/public/specification/nested.json b/public/specification/nested.json
index b7dd801..e7361c1 100644
--- a/public/specification/nested.json
+++ b/public/specification/nested.json
@@ -2,33 +2,41 @@
"type": "object",
"properties": {
"$schema": {
+ "description": "Optional JSON schema URI for validation.",
"type": "string"
},
"name": {
+ "description": "Unique machine-readable identifier for the tool (e.g. \"httpx\").",
"type": "string"
},
"displayName": {
+ "description": "Human-readable display name for the tool (e.g. \"HTTPx\").",
"type": "string"
},
"info": {
+ "description": "General information about the tool such as description, version, and URL.",
"$ref": "#/definitions/ToolInfo"
},
"url": {
+ "description": "The homepage or documentation URL for the tool.",
"type": "string"
},
"globalParameters": {
+ "description": "Parameters that apply to all commands globally.",
"type": "array",
"items": {
"$ref": "#/definitions/NestedParameter"
}
},
"commands": {
+ "description": "Hierarchical list of commands and their nested subcommands.",
"type": "array",
"items": {
"$ref": "#/definitions/NestedCommand"
}
},
"exclusionGroups": {
+ "description": "Groups of parameters with mutual exclusion or required-one-of constraints.",
"anyOf": [
{
"type": "array",
@@ -42,6 +50,7 @@
]
},
"metadata": {
+ "description": "Arbitrary metadata attached to the tool.",
"$ref": "#/definitions/ToolMetadata"
}
},
@@ -56,12 +65,15 @@
"type": "object",
"properties": {
"description": {
+ "description": "A brief human-readable description of what the tool does.",
"type": "string"
},
"version": {
+ "description": "The version string of the tool (e.g. \"1.0.0\").",
"type": "string"
},
"url": {
+ "description": "The homepage or documentation URL for the tool.",
"type": "string"
}
}
@@ -70,60 +82,78 @@
"type": "object",
"properties": {
"name": {
+ "description": "Human-readable display name of the parameter.",
"type": "string"
},
"description": {
+ "description": "Brief description of what this parameter does or accepts.",
"type": "string"
},
"group": {
+ "description": "Optional grouping label for organising related parameters in the UI.",
"type": "string"
},
"parameterType": {
- "$ref": "#/definitions/ParameterType"
+ "$ref": "#/definitions/ParameterType",
+ "description": "Whether this is a boolean flag, a key-value option, or a positional argument."
},
"dataType": {
- "$ref": "#/definitions/ParameterDataType"
+ "$ref": "#/definitions/ParameterDataType",
+ "description": "The data type of the parameter's value."
},
"metadata": {
+ "description": "Additional metadata such as tags.",
"$ref": "#/definitions/ParameterMetadata"
},
"isRequired": {
+ "description": "Whether the user must provide this parameter.",
"type": "boolean"
},
"isRepeatable": {
+ "description": "Whether this parameter can be specified multiple times.",
"type": "boolean"
},
"isGlobal": {
+ "description": "Whether this parameter applies to all commands rather than a single command.",
"type": "boolean"
},
"shortFlag": {
+ "description": "The single-character short flag (e.g. \"-v\").",
"type": "string"
},
"longFlag": {
+ "description": "The long-form flag or option name (e.g. \"--verbose\").",
"type": "string"
},
"position": {
+ "description": "Zero-based position index for positional arguments.",
"type": "number"
},
"sortOrder": {
+ "description": "Display sort position relative to sibling parameters.",
"type": "number"
},
"arraySeparator": {
+ "description": "Separator character used when the option accepts multiple values in one argument (e.g. \",\").",
"type": "string"
},
"keyValueSeparator": {
+ "description": "Separator between key and value for key=value style options (e.g. \"=\").",
"type": "string"
},
"enum": {
+ "description": "Allowed enum choices when dataType is \"Enum\".",
"$ref": "#/definitions/ParameterEnumValues"
},
"validations": {
+ "description": "Validation rules applied to this parameter's value.",
"type": "array",
"items": {
"$ref": "#/definitions/NestedParameterValidation"
}
},
"dependencies": {
+ "description": "Dependencies on other parameters (requires or conflicts-with relationships).",
"type": "array",
"items": {
"$ref": "#/definitions/NestedParameterDependency"
@@ -157,6 +187,7 @@
"type": "object",
"properties": {
"tags": {
+ "description": "Arbitrary tags for categorising or filtering parameters.",
"type": "array",
"items": {
"type": "string"
@@ -168,15 +199,18 @@
"type": "object",
"properties": {
"values": {
+ "description": "The list of allowed enum choices.",
"type": "array",
"items": {
"$ref": "#/definitions/ParameterEnumValue"
}
},
"allowMultiple": {
+ "description": "Whether the user can select multiple values at once.",
"type": "boolean"
},
"separator": {
+ "description": "Separator character used when joining multiple selected values.",
"type": "string"
}
},
@@ -188,18 +222,23 @@
"type": "object",
"properties": {
"value": {
+ "description": "The raw value passed to the CLI for this choice.",
"type": "string"
},
"displayName": {
+ "description": "Human-readable label shown to the user for this enum choice.",
"type": "string"
},
"description": {
+ "description": "Description of what this enum value does or represents.",
"type": "string"
},
"isDefault": {
+ "description": "Whether this is the default selection when no value is provided.",
"type": "boolean"
},
"sortOrder": {
+ "description": "Display sort position relative to sibling enum values.",
"type": "number"
}
},
@@ -212,12 +251,15 @@
"type": "object",
"properties": {
"validationType": {
- "$ref": "#/definitions/ParameterValidationType"
+ "$ref": "#/definitions/ParameterValidationType",
+ "description": "The type of validation to apply."
},
"validationValue": {
+ "description": "The value to validate against (e.g. max length number or regex pattern).",
"type": "string"
},
"errorMessage": {
+ "description": "The error message to display when validation fails.",
"type": "string"
}
},
@@ -241,12 +283,15 @@
"type": "object",
"properties": {
"dependsOnParameter": {
+ "description": "Name of the parameter this dependency references.",
"type": "string"
},
"dependencyType": {
- "$ref": "#/definitions/ParameterDependencyType"
+ "$ref": "#/definitions/ParameterDependencyType",
+ "description": "Whether this parameter requires or conflicts with the referenced parameter."
},
"conditionValue": {
+ "description": "Optional value that the referenced parameter must have for this dependency to apply.",
"type": "string"
}
},
@@ -266,27 +311,30 @@
"type": "object",
"properties": {
"name": {
+ "description": "Human-readable display name of the command.",
"type": "string"
},
"description": {
+ "description": "Brief description of what this command does.",
"type": "string"
},
"interactive": {
- "type": "boolean"
- },
- "isDefault": {
+ "description": "Whether this command opens an interactive session or prompt.",
"type": "boolean"
},
"sortOrder": {
+ "description": "Display sort position relative to sibling commands.",
"type": "number"
},
"parameters": {
+ "description": "Parameters that belong directly to this command.",
"type": "array",
"items": {
"$ref": "#/definitions/NestedParameter"
}
},
"subcommands": {
+ "description": "Nested subcommands of this command.",
"type": "array",
"items": {
"$ref": "#/definitions/NestedCommand"
@@ -294,7 +342,6 @@
}
},
"required": [
- "isDefault",
"name",
"parameters",
"sortOrder",
@@ -305,12 +352,15 @@
"type": "object",
"properties": {
"name": {
+ "description": "Human-readable name for this exclusion group.",
"type": "string"
},
"exclusionType": {
- "$ref": "#/definitions/ExclusionType"
+ "$ref": "#/definitions/ExclusionType",
+ "description": "Whether parameters in this group are mutually exclusive or one is required."
},
"parameters": {
+ "description": "Names of the parameters that participate in this exclusion group.",
"type": "array",
"items": {
"type": "string"
diff --git a/public/tools-collection/asnmap.json b/public/tools-collection/asnmap.json
index bc16d05..3add69a 100644
--- a/public/tools-collection/asnmap.json
+++ b/public/tools-collection/asnmap.json
@@ -11,7 +11,6 @@
{
"name": "asnmap",
"description": "Map organization network ranges using ASN information.",
- "isDefault": true,
"sortOrder": 1,
"key": "asnmap"
}
diff --git a/public/tools-collection/cdncheck.json b/public/tools-collection/cdncheck.json
index 8c430e6..c94b5e6 100644
--- a/public/tools-collection/cdncheck.json
+++ b/public/tools-collection/cdncheck.json
@@ -10,7 +10,6 @@
"commands": [
{
"name": "cdncheck",
- "isDefault": true,
"sortOrder": 1,
"key": "cdncheck"
}
diff --git a/public/tools-collection/curl.json b/public/tools-collection/curl.json
index fe3b5d1..8948d85 100644
--- a/public/tools-collection/curl.json
+++ b/public/tools-collection/curl.json
@@ -11,7 +11,6 @@
{
"name": "curl",
"description": "Run curl to download files.",
- "isDefault": true,
"sortOrder": 1,
"key": "curl"
}
diff --git a/public/tools-collection/dnsx.json b/public/tools-collection/dnsx.json
index f435ec7..c9bd643 100644
--- a/public/tools-collection/dnsx.json
+++ b/public/tools-collection/dnsx.json
@@ -11,7 +11,6 @@
{
"name": "dnsx",
"description": "Root command for dnsx CLI.",
- "isDefault": true,
"sortOrder": 1,
"key": "dnsx"
}
diff --git a/public/tools-collection/gospider.json b/public/tools-collection/gospider.json
index 8ec28ec..70404d0 100644
--- a/public/tools-collection/gospider.json
+++ b/public/tools-collection/gospider.json
@@ -11,7 +11,6 @@
{
"name": "gospider",
"description": "Fast web spider written in Go.",
- "isDefault": true,
"sortOrder": 1,
"key": "gospider"
}
diff --git a/public/tools-collection/httpx.json b/public/tools-collection/httpx.json
index 858b756..a2083c5 100644
--- a/public/tools-collection/httpx.json
+++ b/public/tools-collection/httpx.json
@@ -11,7 +11,6 @@
{
"name": "httpx",
"description": "Run httpx with the specified parameters.",
- "isDefault": true,
"sortOrder": 1,
"key": "httpx"
}
diff --git a/public/tools-collection/katana.json b/public/tools-collection/katana.json
index 0174f2b..c8803d2 100644
--- a/public/tools-collection/katana.json
+++ b/public/tools-collection/katana.json
@@ -11,7 +11,6 @@
{
"name": "katana",
"description": "Primary command for katana crawler.",
- "isDefault": true,
"sortOrder": 0,
"key": "katana"
}
@@ -1114,4 +1113,4 @@
"commandKey": "katana"
}
]
-}
\ No newline at end of file
+}
diff --git a/public/tools-collection/naabu.json b/public/tools-collection/naabu.json
index 35a1b43..8dcbd0f 100644
--- a/public/tools-collection/naabu.json
+++ b/public/tools-collection/naabu.json
@@ -11,7 +11,6 @@
{
"name": "naabu",
"description": "Run naabu with flags.",
- "isDefault": true,
"sortOrder": 1,
"key": "naabu"
}
@@ -85,22 +84,22 @@
"value": "full",
"displayName": "Full",
"description": "Scan full set of top ports.",
- "isDefault": false,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": false
},
{
"value": "100",
"displayName": "100",
"description": "Scan top 100 ports.",
- "isDefault": true,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": true
},
{
"value": "1000",
"displayName": "1000",
"description": "Scan top 1000 ports.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
}
]
},
@@ -271,15 +270,15 @@
"value": "4",
"displayName": "4",
"description": "IPv4.",
- "isDefault": true,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": true
},
{
"value": "6",
"displayName": "6",
"description": "IPv6.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
}
]
},
@@ -300,15 +299,15 @@
"value": "SYN",
"displayName": "SYN",
"description": "TCP SYN scan.",
- "isDefault": false,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": false
},
{
"value": "CONNECT",
"displayName": "CONNECT",
"description": "TCP connect scan.",
- "isDefault": true,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": true
}
]
},
@@ -845,4 +844,4 @@
"commandKey": "naabu"
}
]
-}
+}
\ No newline at end of file
diff --git a/public/tools-collection/nuclei.json b/public/tools-collection/nuclei.json
index 4404cc4..44b73ab 100644
--- a/public/tools-collection/nuclei.json
+++ b/public/tools-collection/nuclei.json
@@ -11,7 +11,6 @@
{
"name": "nuclei",
"description": "Run nuclei vulnerability scanner.",
- "isDefault": true,
"sortOrder": 1,
"key": "nuclei"
}
@@ -82,15 +81,15 @@
"value": "4",
"displayName": "4",
"description": "Use IPv4.",
- "isDefault": true,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": true
},
{
"value": "6",
"displayName": "6",
"description": "Use IPv6.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
}
]
},
@@ -110,43 +109,43 @@
"value": "list",
"displayName": "List",
"description": "Use list input mode.",
- "isDefault": true,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": true
},
{
"value": "burp",
"displayName": "Burp",
"description": "Use burp input mode.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
},
{
"value": "jsonl",
"displayName": "JSONL",
"description": "Use jsonl input mode.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
},
{
"value": "yaml",
"displayName": "Yaml",
"description": "Use yaml input mode.",
- "isDefault": false,
- "sortOrder": 4
+ "sortOrder": 4,
+ "isDefault": false
},
{
"value": "openapi",
"displayName": "OpenAPI",
"description": "Use openapi input mode.",
- "isDefault": false,
- "sortOrder": 5
+ "sortOrder": 5,
+ "isDefault": false
},
{
"value": "swagger",
"displayName": "Swagger",
"description": "Use swagger input mode.",
- "isDefault": false,
- "sortOrder": 6
+ "sortOrder": 6,
+ "isDefault": false
}
]
},
@@ -474,43 +473,43 @@
"value": "info",
"displayName": "Info",
"description": "Information severity.",
- "isDefault": false,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": false
},
{
"value": "low",
"displayName": "Low",
"description": "Low severity.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
},
{
"value": "medium",
"displayName": "Medium",
"description": "Medium severity.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
},
{
"value": "high",
"displayName": "High",
"description": "High severity.",
- "isDefault": false,
- "sortOrder": 4
+ "sortOrder": 4,
+ "isDefault": false
},
{
"value": "critical",
"displayName": "Critical",
"description": "Critical severity.",
- "isDefault": false,
- "sortOrder": 5
+ "sortOrder": 5,
+ "isDefault": false
},
{
"value": "unknown",
"displayName": "Unknown",
"description": "Unknown severity.",
- "isDefault": false,
- "sortOrder": 6
+ "sortOrder": 6,
+ "isDefault": false
}
]
},
@@ -531,43 +530,43 @@
"value": "info",
"displayName": "Info",
"description": "Exclude information severity.",
- "isDefault": false,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": false
},
{
"value": "low",
"displayName": "Low",
"description": "Exclude low severity.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
},
{
"value": "medium",
"displayName": "Medium",
"description": "Exclude medium severity.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
},
{
"value": "high",
"displayName": "High",
"description": "Exclude high severity.",
- "isDefault": false,
- "sortOrder": 4
+ "sortOrder": 4,
+ "isDefault": false
},
{
"value": "critical",
"displayName": "Critical",
"description": "Exclude critical severity.",
- "isDefault": false,
- "sortOrder": 5
+ "sortOrder": 5,
+ "isDefault": false
},
{
"value": "unknown",
"displayName": "Unknown",
"description": "Exclude unknown severity.",
- "isDefault": false,
- "sortOrder": 6
+ "sortOrder": 6,
+ "isDefault": false
}
]
},
@@ -588,78 +587,78 @@
"value": "dns",
"displayName": "Dns",
"description": "DNS templates.",
- "isDefault": false,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": false
},
{
"value": "file",
"displayName": "File",
"description": "File templates.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
},
{
"value": "http",
"displayName": "Http",
"description": "HTTP templates.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
},
{
"value": "headless",
"displayName": "Headless",
"description": "Headless templates.",
- "isDefault": false,
- "sortOrder": 4
+ "sortOrder": 4,
+ "isDefault": false
},
{
"value": "tcp",
"displayName": "Tcp",
"description": "TCP templates.",
- "isDefault": false,
- "sortOrder": 5
+ "sortOrder": 5,
+ "isDefault": false
},
{
"value": "workflow",
"displayName": "Workflow",
"description": "Workflow templates.",
- "isDefault": false,
- "sortOrder": 6
+ "sortOrder": 6,
+ "isDefault": false
},
{
"value": "ssl",
"displayName": "Ssl",
"description": "SSL templates.",
- "isDefault": false,
- "sortOrder": 7
+ "sortOrder": 7,
+ "isDefault": false
},
{
"value": "websocket",
"displayName": "Websocket",
"description": "Websocket templates.",
- "isDefault": false,
- "sortOrder": 8
+ "sortOrder": 8,
+ "isDefault": false
},
{
"value": "whois",
"displayName": "Whois",
"description": "WHOIS templates.",
- "isDefault": false,
- "sortOrder": 9
+ "sortOrder": 9,
+ "isDefault": false
},
{
"value": "code",
"displayName": "Code",
"description": "Code templates.",
- "isDefault": false,
- "sortOrder": 10
+ "sortOrder": 10,
+ "isDefault": false
},
{
"value": "javascript",
"displayName": "Javascript",
"description": "JavaScript templates.",
- "isDefault": false,
- "sortOrder": 11
+ "sortOrder": 11,
+ "isDefault": false
}
]
},
@@ -680,78 +679,78 @@
"value": "dns",
"displayName": "Dns",
"description": "Exclude DNS templates.",
- "isDefault": false,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": false
},
{
"value": "file",
"displayName": "File",
"description": "Exclude file templates.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
},
{
"value": "http",
"displayName": "Http",
"description": "Exclude HTTP templates.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
},
{
"value": "headless",
"displayName": "Headless",
"description": "Exclude headless templates.",
- "isDefault": false,
- "sortOrder": 4
+ "sortOrder": 4,
+ "isDefault": false
},
{
"value": "tcp",
"displayName": "Tcp",
"description": "Exclude TCP templates.",
- "isDefault": false,
- "sortOrder": 5
+ "sortOrder": 5,
+ "isDefault": false
},
{
"value": "workflow",
"displayName": "Workflow",
"description": "Exclude workflow templates.",
- "isDefault": false,
- "sortOrder": 6
+ "sortOrder": 6,
+ "isDefault": false
},
{
"value": "ssl",
"displayName": "Ssl",
"description": "Exclude SSL templates.",
- "isDefault": false,
- "sortOrder": 7
+ "sortOrder": 7,
+ "isDefault": false
},
{
"value": "websocket",
"displayName": "Websocket",
"description": "Exclude websocket templates.",
- "isDefault": false,
- "sortOrder": 8
+ "sortOrder": 8,
+ "isDefault": false
},
{
"value": "whois",
"displayName": "Whois",
"description": "Exclude WHOIS templates.",
- "isDefault": false,
- "sortOrder": 9
+ "sortOrder": 9,
+ "isDefault": false
},
{
"value": "code",
"displayName": "Code",
"description": "Exclude code templates.",
- "isDefault": false,
- "sortOrder": 10
+ "sortOrder": 10,
+ "isDefault": false
},
{
"value": "javascript",
"displayName": "Javascript",
"description": "Exclude JavaScript templates.",
- "isDefault": false,
- "sortOrder": 11
+ "sortOrder": 11,
+ "isDefault": false
}
]
},
@@ -1219,22 +1218,22 @@
"value": "batteringram",
"displayName": "Batteringram",
"description": "Use battering ram attack type.",
- "isDefault": false,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": false
},
{
"value": "pitchfork",
"displayName": "Pitchfork",
"description": "Use pitchfork attack type.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
},
{
"value": "clusterbomb",
"displayName": "Clusterbomb",
"description": "Use cluster bomb attack type.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
}
]
},
@@ -1379,29 +1378,29 @@
"value": "replace",
"displayName": "Replace",
"description": "Replace fuzzing type.",
- "isDefault": false,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": false
},
{
"value": "prefix",
"displayName": "Prefix",
"description": "Prefix fuzzing type.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
},
{
"value": "postfix",
"displayName": "Postfix",
"description": "Postfix fuzzing type.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
},
{
"value": "infix",
"displayName": "Infix",
"description": "Infix fuzzing type.",
- "isDefault": false,
- "sortOrder": 4
+ "sortOrder": 4,
+ "isDefault": false
}
]
},
@@ -1421,15 +1420,15 @@
"value": "multiple",
"displayName": "Multiple",
"description": "Multiple fuzzing mode.",
- "isDefault": false,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": false
},
{
"value": "single",
"displayName": "Single",
"description": "Single fuzzing mode.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
}
]
},
@@ -1525,22 +1524,22 @@
"value": "low",
"displayName": "Low",
"description": "Low fuzzing aggression.",
- "isDefault": true,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": true
},
{
"value": "medium",
"displayName": "Medium",
"description": "Medium fuzzing aggression.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
},
{
"value": "high",
"displayName": "High",
"description": "High fuzzing aggression.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
}
]
},
@@ -1839,22 +1838,22 @@
"value": "auto",
"displayName": "Auto",
"description": "Automatic scan strategy.",
- "isDefault": true,
- "sortOrder": 1
+ "sortOrder": 1,
+ "isDefault": true
},
{
"value": "host-spray",
"displayName": "Host-spray",
"description": "Host spray scan strategy.",
- "isDefault": false,
- "sortOrder": 2
+ "sortOrder": 2,
+ "isDefault": false
},
{
"value": "template-spray",
"displayName": "Template-spray",
"description": "Template spray scan strategy.",
- "isDefault": false,
- "sortOrder": 3
+ "sortOrder": 3,
+ "isDefault": false
}
]
},
@@ -2306,4 +2305,4 @@
"commandKey": "nuclei"
}
]
-}
+}
\ No newline at end of file
diff --git a/public/tools-collection/shuffledns.json b/public/tools-collection/shuffledns.json
index 1fa6eb3..917428e 100644
--- a/public/tools-collection/shuffledns.json
+++ b/public/tools-collection/shuffledns.json
@@ -11,7 +11,6 @@
{
"name": "shuffledns",
"description": "shuffleDNS is a wrapper around massdns written in go that allows you to enumerate valid subdomains using active bruteforce as well as resolve subdomains with wildcard handling and easy input-output support.",
- "isDefault": true,
"sortOrder": 1,
"key": "shuffledns"
}
diff --git a/public/tools-collection/subfinder.json b/public/tools-collection/subfinder.json
index 28bf51a..26cd4f8 100644
--- a/public/tools-collection/subfinder.json
+++ b/public/tools-collection/subfinder.json
@@ -11,7 +11,6 @@
{
"name": "subfinder",
"description": "Subfinder is a subdomain discovery tool that discovers subdomains for websites by using passive online sources.",
- "isDefault": true,
"sortOrder": 1,
"key": "subfinder"
}
diff --git a/public/tools-collection/urlfinder.json b/public/tools-collection/urlfinder.json
index 58070ce..887958d 100644
--- a/public/tools-collection/urlfinder.json
+++ b/public/tools-collection/urlfinder.json
@@ -11,7 +11,6 @@
{
"name": "urlfinder",
"description": "A streamlined tool for discovering associated URLs.",
- "isDefault": true,
"sortOrder": 1,
"key": "urlfinder"
}
diff --git a/public/tools-collection/yt-dlp.json b/public/tools-collection/yt-dlp.json
index 2308297..853d16e 100644
--- a/public/tools-collection/yt-dlp.json
+++ b/public/tools-collection/yt-dlp.json
@@ -11,7 +11,6 @@
{
"name": "yt-dlp",
"description": "yt-dlp is a command-line program to download videos from YouTube and other sites.",
- "isDefault": true,
"sortOrder": 1,
"commandKey": "yt-dlp",
"key": "yt-dlp"
diff --git a/public/tools.json b/public/tools.json
index 164d488..3cdb231 100644
--- a/public/tools.json
+++ b/public/tools.json
@@ -2,66 +2,131 @@
{
"name": "asnmap",
"displayName": "ASNMap",
- "description": "Go CLI and Library for quickly mapping organization network ranges using ASN information."
+ "description": "Go CLI and Library for quickly mapping organization network ranges using ASN information.",
+ "info": {
+ "description": "Go CLI and Library for quickly mapping organization network ranges using ASN information.",
+ "version": "1.1.1",
+ "url": "https://github.com/projectdiscovery/asnmap"
+ }
},
{
"name": "cdncheck",
"displayName": "CDNCheck",
- "description": "cdncheck is a tool for identifying the technology associated with dns / ip network addresses."
+ "description": "cdncheck is a tool for identifying the technology associated with dns / ip network addresses.",
+ "info": {
+ "description": "cdncheck is a tool for identifying the technology associated with dns / ip network addresses.",
+ "version": "1.2.27",
+ "url": "https://github.com/projectdiscovery/cdncheck"
+ }
},
{
"name": "curl",
"displayName": "Curl",
- "description": "curl is a command line tool and library for transferring data with URLs."
+ "description": "curl is a command line tool and library for transferring data with URLs.",
+ "info": {
+ "description": "curl is a command line tool and library for transferring data with URLs.",
+ "version": "8.19.0",
+ "url": "https://curl.se/"
+ }
},
{
"name": "dnsx",
"displayName": "DNSX",
- "description": "A fast and multi-purpose DNS toolkit designed for running DNS queries."
+ "description": "A fast and multi-purpose DNS toolkit designed for running DNS queries.",
+ "info": {
+ "description": "A fast and multi-purpose DNS toolkit designed for running DNS queries.",
+ "version": "1.2.3",
+ "url": "https://github.com/projectdiscovery/dnsx"
+ }
},
{
"name": "gospider",
"displayName": "GoSpider",
- "description": "Fast web spider written in Go."
+ "description": "Fast web spider written in Go.",
+ "info": {
+ "description": "Fast web spider written in Go.",
+ "version": "1.1.6",
+ "url": "https://github.com/jaeles-project/gospider"
+ }
},
{
"name": "httpx",
"displayName": "Httpx",
- "description": "Httpx is a fast and multi-purpose HTTP toolkit that allows running multiple probes using the retryablehttp library."
+ "description": "Httpx is a fast and multi-purpose HTTP toolkit that allows running multiple probes using the retryablehttp library.",
+ "info": {
+ "description": "Httpx is a fast and multi-purpose HTTP toolkit that allows running multiple probes using the retryablehttp library.",
+ "version": "1.9.0",
+ "url": "https://github.com/projectdiscovery/httpx"
+ }
},
{
"name": "katana",
"displayName": "Katana",
- "description": "Katana is a fast crawler focused on execution in automation pipelines offering both headless and non-headless crawling."
+ "description": "Katana is a fast crawler focused on execution in automation pipelines offering both headless and non-headless crawling.",
+ "info": {
+ "description": "Katana is a fast crawler focused on execution in automation pipelines offering both headless and non-headless crawling.",
+ "version": "1.5.0",
+ "url": "https://github.com/projectdiscovery/katana"
+ }
},
{
"name": "naabu",
"displayName": "Naabu",
- "description": "Fast port scanner for discovering open ports on hosts."
+ "description": "Fast port scanner for discovering open ports on hosts.",
+ "info": {
+ "description": "Fast port scanner for discovering open ports on hosts.",
+ "version": "2.5.0",
+ "url": "https://github.com/projectdiscovery/naabu"
+ }
},
{
"name": "nuclei",
"displayName": "Nuclei",
- "description": "Nuclei is a fast, template based vulnerability scanner focusing on extensive configurability, massive extensibility and ease of use."
+ "description": "Nuclei is a fast, template based vulnerability scanner focusing on extensive configurability, massive extensibility and ease of use.",
+ "info": {
+ "description": "Nuclei is a fast, template based vulnerability scanner focusing on extensive configurability, massive extensibility and ease of use.",
+ "version": "3.7.1",
+ "url": "https://github.com/projectdiscovery/nuclei"
+ }
},
{
"name": "shuffledns",
"displayName": "ShuffleDNS",
- "description": "MassDNS wrapper written in go to enumerate valid subdomains using active bruteforce as well as resolve subdomains with wildcard filtering and easy input-output support."
+ "description": "MassDNS wrapper written in go to enumerate valid subdomains using active bruteforce as well as resolve subdomains with wildcard filtering and easy input-output support.",
+ "info": {
+ "description": "MassDNS wrapper written in go to enumerate valid subdomains using active bruteforce as well as resolve subdomains with wildcard filtering and easy input-output support.",
+ "version": "1.2.1",
+ "url": "https://github.com/projectdiscovery/shuffledns"
+ }
},
{
"name": "subfinder",
"displayName": "Subfinder",
- "description": "Subfinder is a subdomain discovery tool that discovers subdomains for websites by using passive online sources."
+ "description": "Subfinder is a subdomain discovery tool that discovers subdomains for websites by using passive online sources.",
+ "info": {
+ "description": "Subfinder is a subdomain discovery tool that discovers subdomains for websites by using passive online sources.",
+ "version": "2.13.0",
+ "url": "https://github.com/projectdiscovery/subfinder"
+ }
},
{
"name": "urlfinder",
"displayName": "URLFinder",
- "description": "A streamlined tool for discovering associated URLs."
+ "description": "A streamlined tool for discovering associated URLs.",
+ "info": {
+ "description": "A streamlined tool for discovering associated URLs.",
+ "version": "0.0.3",
+ "url": "https://github.com/projectdiscovery/urlfinder"
+ }
},
{
"name": "yt-dlp",
"displayName": "yt-dlp",
- "description": "yt-dlp is a command-line program to download videos from YouTube and other sites."
+ "description": "yt-dlp is a command-line program to download videos from YouTube and other sites.",
+ "info": {
+ "description": "yt-dlp is a command-line program to download videos from YouTube and other sites.",
+ "version": "2026.03.17",
+ "url": "https://github.com/yt-dlp/yt-dlp"
+ }
}
]
\ No newline at end of file
diff --git a/registry/commandly/__tests__/generated-command.test.tsx b/registry/commandly/__tests__/generated-command.test.tsx
index 581e806..20ad239 100644
--- a/registry/commandly/__tests__/generated-command.test.tsx
+++ b/registry/commandly/__tests__/generated-command.test.tsx
@@ -4,7 +4,7 @@ import { render, screen } from "@testing-library/react";
const testTool = {
name: "tool",
displayName: "Tool",
- commands: [{ key: "test-key", name: "test", isDefault: true, sortOrder: 0 }],
+ commands: [{ key: "test-key", name: "test", sortOrder: 0 }],
parameters: [],
};
@@ -23,7 +23,7 @@ describe("GeneratedCommand", () => {
const tool = {
name: "curl",
displayName: "Curl",
- commands: [{ key: "curl", name: "curl", isDefault: true, sortOrder: 1 }],
+ commands: [{ key: "curl", name: "curl", sortOrder: 1 }],
parameters: [
{
key: "header",
@@ -55,7 +55,7 @@ describe("GeneratedCommand", () => {
const tool = {
name: "mytool",
displayName: "My Tool",
- commands: [{ key: "mytool", name: "mytool", isDefault: true, sortOrder: 1 }],
+ commands: [{ key: "mytool", name: "mytool", sortOrder: 1 }],
parameters: [
{
key: "filter",
@@ -84,7 +84,7 @@ describe("GeneratedCommand", () => {
const tool = {
name: "curl",
displayName: "Curl",
- commands: [{ key: "curl", name: "curl", isDefault: true, sortOrder: 1 }],
+ commands: [{ key: "curl", name: "curl", sortOrder: 1 }],
parameters: [
{
key: "header",
@@ -111,7 +111,7 @@ describe("GeneratedCommand", () => {
const tool = {
name: "ssh",
displayName: "SSH",
- commands: [{ key: "ssh", name: "ssh", isDefault: true, sortOrder: 1 }],
+ commands: [{ key: "ssh", name: "ssh", sortOrder: 1 }],
parameters: [
{
key: "verbose",
@@ -139,7 +139,7 @@ describe("GeneratedCommand", () => {
const tool = {
name: "curl",
displayName: "Curl",
- commands: [{ key: "curl", name: "curl", isDefault: true, sortOrder: 1 }],
+ commands: [{ key: "curl", name: "curl", sortOrder: 1 }],
parameters: [
{
key: "target",
diff --git a/registry/commandly/__tests__/json-output.test.tsx b/registry/commandly/__tests__/json-output.test.tsx
index 548763e..53404bf 100644
--- a/registry/commandly/__tests__/json-output.test.tsx
+++ b/registry/commandly/__tests__/json-output.test.tsx
@@ -45,7 +45,6 @@ describe("exportToStructuredJSON", () => {
value: "val",
displayName: "Val",
description: "",
- isDefault: true,
sortOrder: 0,
},
],
diff --git a/registry/commandly/__tests__/tool-renderer.test.tsx b/registry/commandly/__tests__/tool-renderer.test.tsx
index 9cb222e..565afa2 100644
--- a/registry/commandly/__tests__/tool-renderer.test.tsx
+++ b/registry/commandly/__tests__/tool-renderer.test.tsx
@@ -4,7 +4,7 @@ import { createNewParameter } from "@/components/commandly/utils/flat";
import { defaultTool } from "@/lib/utils";
import { render, screen } from "@testing-library/react";
-const baseCommand = { key: "my-tool", name: "my-tool", isDefault: true, sortOrder: 0 };
+const baseCommand = { key: "my-tool", name: "my-tool", sortOrder: 0 };
const baseTool = { ...defaultTool(), commands: [baseCommand] };
describe("ToolRenderer", () => {
@@ -165,6 +165,36 @@ describe("ToolRenderer", () => {
expect(inputs).toHaveLength(2);
});
+ it("renders an allowMultiple Enum parameter without crashing when value is an array (repeatable-to-non-repeatable transition)", () => {
+ const param = {
+ ...createNewParameter(false, "my-tool"),
+ key: "format",
+ name: "Format",
+ parameterType: "Option" as const,
+ dataType: "Enum" as const,
+ isRepeatable: false,
+ enum: {
+ values: [
+ { value: "json", displayName: "JSON" },
+ { value: "xml", displayName: "XML" },
+ ],
+ allowMultiple: true,
+ separator: ",",
+ },
+ };
+ expect(() =>
+ render(
+ {}}
+ />,
+ ),
+ ).not.toThrow();
+ expect(screen.getByText("Format")).toBeInTheDocument();
+ });
+
it("custom catalog entry takes precedence over built-in", () => {
const param = {
...createNewParameter(false, "my-tool"),
diff --git a/registry/commandly/json-output.tsx b/registry/commandly/json-output.tsx
index dbed8a2..f241483 100644
--- a/registry/commandly/json-output.tsx
+++ b/registry/commandly/json-output.tsx
@@ -1,6 +1,7 @@
import { Tool } from "@/components/commandly/types/flat";
import { exportToStructuredJSON } from "@/components/commandly/utils/flat";
import { convertToNestedStructure } from "@/components/commandly/utils/nested";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
@@ -14,7 +15,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { CheckIcon, ChevronsUpDownIcon, CopyIcon, Edit2Icon, XIcon } from "lucide-react";
-import { useEffect, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
const jsonOptions = [
@@ -22,17 +23,51 @@ const jsonOptions = [
{ value: "flat", label: "Flat" },
];
+type DiffLine = { type: "same" | "added" | "removed"; text: string };
+
+function diffLines(before: string, after: string): DiffLine[] {
+ const a = before.split("\n");
+ const b = after.split("\n");
+ const m = a.length;
+ const n = b.length;
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
+ for (let i = 1; i <= m; i++)
+ for (let j = 1; j <= n; j++)
+ dp[i][j] =
+ a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
+ const result: DiffLine[] = [];
+ let i = m,
+ j = n;
+ while (i > 0 || j > 0) {
+ if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
+ result.unshift({ type: "same", text: a[i - 1] });
+ i--;
+ j--;
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
+ result.unshift({ type: "added", text: b[j - 1] });
+ j--;
+ } else {
+ result.unshift({ type: "removed", text: a[i - 1] });
+ i--;
+ }
+ }
+ return result;
+}
+
interface JsonTypeComponentProps {
tool: Tool;
+ originalTool?: Tool;
onApply?: (tool: Tool) => void;
}
-export function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {
+export function JsonOutput({ tool, originalTool, onApply }: JsonTypeComponentProps) {
const [open, setOpen] = useState(false);
const [jsonString, setJsonString] = useState();
+ const [originalJsonString, setOriginalJsonString] = useState();
const [jsonType, setJsonType] = useState<"nested" | "flat">("flat");
const [isEditing, setIsEditing] = useState(false);
const [editValue, setEditValue] = useState("");
+ const [showDiff, setShowDiff] = useState(true);
useEffect(() => {
const config =
@@ -40,6 +75,30 @@ export function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {
setJsonString(JSON.stringify(config, null, 2));
}, [jsonType, tool]);
+ useEffect(() => {
+ if (!originalTool) {
+ setOriginalJsonString(undefined);
+ return;
+ }
+ const config =
+ jsonType === "flat"
+ ? exportToStructuredJSON(originalTool)
+ : convertToNestedStructure(originalTool);
+ setOriginalJsonString(JSON.stringify(config, null, 2));
+ }, [jsonType, originalTool]);
+
+ const diff = useMemo(() => {
+ if (!originalJsonString || !jsonString || originalJsonString === jsonString) return null;
+ return diffLines(originalJsonString, jsonString);
+ }, [originalJsonString, jsonString]);
+
+ const diffStats = useMemo(() => {
+ if (!diff) return null;
+ const added = diff.filter((l) => l.type === "added").length;
+ const removed = diff.filter((l) => l.type === "removed").length;
+ return { added, removed };
+ }, [diff]);
+
const handleEditToggle = () => {
setEditValue(jsonString ?? "");
setIsEditing(true);
@@ -137,6 +196,34 @@ export function JsonOutput({ tool, onApply }: JsonTypeComponentProps) {
+ {diffStats && !isEditing && (
+
+ {diffStats.added > 0 && (
+
+ +{diffStats.added} added
+
+ )}
+ {diffStats.removed > 0 && (
+
+ -{diffStats.removed} removed
+
+ )}
+ setShowDiff((v) => !v)}
+ >
+ {showDiff ? "Full view" : "Diff view"}
+
+
+ )}
{isEditing ? (
+ ) : diff && showDiff ? (
+
+
+ {diff.map((line, idx) => (
+
+
+ {line.type === "added" ? "+ " : line.type === "removed" ? "- " : " "}
+
+ {line.text}
+
+ ))}
+
+
+
+
) : (
{
- const defaultCommand = tool.commands.find((command) => command.isDefault);
- if (defaultCommand) return defaultCommand;
-
const nameMatchCommand = tool.commands.find(
(command) => command.name.toLowerCase() === tool.name.toLowerCase(),
);
@@ -129,7 +126,11 @@ function OptionEnumInput({ parameter, value, onUpdate }: ParameterRenderContext)
);
if (parameter.enum?.allowMultiple) {
- const selected = value ? (value as string).split(separator).filter(Boolean) : [];
+ const selected = Array.isArray(value)
+ ? (value as string[]).filter(Boolean)
+ : value
+ ? (value as string).split(separator).filter(Boolean)
+ : [];
return (
{label}
diff --git a/registry/commandly/types/flat.ts b/registry/commandly/types/flat.ts
index 7d31e20..5728c12 100644
--- a/registry/commandly/types/flat.ts
+++ b/registry/commandly/types/flat.ts
@@ -1,30 +1,46 @@
export interface ToolInfo {
+ /** A brief human-readable description of what the tool does. */
description?: string;
+ /** The version string of the tool (e.g. "1.0.0"). */
version?: string;
+ /** The homepage or documentation URL for the tool. */
url?: string;
}
export interface Command {
+ /** Unique identifier for this command within the tool. */
key: string;
+ /** Key of the parent command; used to represent subcommand nesting. */
parentCommandKey?: string;
+ /** Human-readable display name of the command. */
name: string;
+ /** Brief description of what this command does. */
description?: string;
+ /** Whether this command opens an interactive session or prompt. */
interactive?: boolean;
- isDefault?: boolean;
+ /** Display sort position relative to sibling commands. */
sortOrder?: number;
}
export interface ParameterEnumValue {
+ /** The raw value passed to the CLI for this choice. */
value: string;
+ /** Human-readable label shown to the user for this enum choice. */
displayName: string;
+ /** Description of what this enum value does or represents. */
description?: string;
+ /** Whether this is the default selection when no value is provided. */
isDefault?: boolean;
+ /** Display sort position relative to sibling enum values. */
sortOrder?: number;
}
export interface ParameterEnumValues {
+ /** The list of allowed enum choices. */
values: ParameterEnumValue[];
+ /** Whether the user can select multiple values at once. */
allowMultiple?: boolean;
+ /** Separator character used when joining multiple selected values. */
separator?: string;
}
@@ -36,19 +52,28 @@ export type ParameterValidationType =
| "regex";
export interface ParameterValidation {
+ /** Unique identifier for this validation rule. */
key: string;
+ /** The type of validation to apply. */
validationType: ParameterValidationType;
+ /** The value to validate against (e.g. the max length number, or a regex pattern). */
validationValue: string;
+ /** The error message to display when validation fails. */
errorMessage: string;
}
export type ParameterDependencyType = "requires" | "conflicts_with";
export interface ParameterDependency {
+ /** Unique identifier for this dependency rule. */
key: string;
+ /** Key of the parameter that owns this dependency. */
parameterKey: string;
+ /** Key of the parameter this dependency references. */
dependsOnParameterKey: string;
+ /** Whether this parameter requires or conflicts with the referenced parameter. */
dependencyType: ParameterDependencyType;
+ /** Optional value that the referenced parameter must have for this dependency to apply. */
conditionValue?: string;
}
@@ -59,6 +84,7 @@ export interface ToolMetadata {}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ParameterMetadata {
+ /** Arbitrary tags for categorising or filtering parameters. */
tags?: string[];
}
@@ -67,44 +93,76 @@ export type ParameterType = "Flag" | "Option" | "Argument";
export type ParameterDataType = "String" | "Number" | "Boolean" | "Enum";
export interface Parameter {
+ /** Unique identifier for this parameter within the tool. */
key: string;
+ /** Human-readable display name of the parameter. */
name: string;
+ /** Key of the command this parameter belongs to; omit for global parameters. */
commandKey?: string;
+ /** Brief description of what this parameter does or accepts. */
description?: string;
+ /** Optional grouping label for organising related parameters in the UI. */
group?: string;
+ /** Additional metadata such as tags. */
metadata?: ParameterMetadata;
+ /** Whether this is a boolean flag, a key-value option, or a positional argument. */
parameterType: ParameterType;
+ /** The data type of the parameter's value. */
dataType: ParameterDataType;
+ /** Whether the user must provide this parameter. */
isRequired?: boolean;
+ /** Whether this parameter can be specified multiple times. */
isRepeatable?: boolean;
+ /** Whether this parameter applies to all commands rather than a single command. */
isGlobal?: boolean;
+ /** The single-character short flag (e.g. "-v"). */
shortFlag?: string;
+ /** The long-form flag or option name (e.g. "--verbose"). */
longFlag?: string;
+ /** Zero-based position index for positional arguments. */
position?: number;
+ /** Display sort position relative to sibling parameters. */
sortOrder?: number;
+ /** Separator character used when the option accepts multiple values in one argument (e.g. ","). */
arraySeparator?: string;
+ /** Separator between key and value for key=value style options (e.g. "="). */
keyValueSeparator?: string;
+ /** Allowed enum choices when dataType is "Enum". */
enum?: ParameterEnumValues;
+ /** Validation rules applied to this parameter's value. */
validations?: ParameterValidation[];
+ /** Dependencies on other parameters (requires or conflicts-with relationships). */
dependencies?: ParameterDependency[];
}
export type ExclusionType = "mutual_exclusive" | "required_one_of";
export interface ExclusionGroup {
+ /** Unique identifier for this exclusion group. */
key?: string;
+ /** Key of the command this exclusion group belongs to; omit for global groups. */
commandKey?: string;
+ /** Human-readable name for this exclusion group. */
name: string;
+ /** Whether parameters in this group are mutually exclusive or one is required. */
exclusionType: ExclusionType;
+ /** Keys of the parameters that participate in this exclusion group. */
parameterKeys: string[];
}
export interface Tool {
+ /** Unique machine-readable identifier for the tool (e.g. "httpx"). */
name: string;
+ /** Human-readable display name for the tool (e.g. "HTTPx"). */
displayName: string;
+ /** General information about the tool such as description, version, and URL. */
info?: ToolInfo;
+ /** List of all commands and subcommands defined for this tool. */
commands: Command[];
+ /** Flat list of all parameters across all commands and global scope. */
parameters: Parameter[];
+ /** Groups of parameters with mutual exclusion or required-one-of constraints. */
exclusionGroups?: ExclusionGroup[];
+ /** Arbitrary metadata attached to the tool. */
metadata?: ToolMetadata;
}
diff --git a/registry/commandly/types/nested.ts b/registry/commandly/types/nested.ts
index 8606997..4bd6b54 100644
--- a/registry/commandly/types/nested.ts
+++ b/registry/commandly/types/nested.ts
@@ -11,62 +11,103 @@ import type {
} from "@/components/commandly/types/flat";
export interface NestedParameterValidation {
+ /** The type of validation to apply. */
validationType: ParameterValidationType;
+ /** The value to validate against (e.g. max length number or regex pattern). */
validationValue: string;
+ /** The error message to display when validation fails. */
errorMessage: string;
}
export interface NestedParameterDependency {
+ /** Name of the parameter this dependency references. */
dependsOnParameter: string;
+ /** Whether this parameter requires or conflicts with the referenced parameter. */
dependencyType: ParameterDependencyType;
+ /** Optional value that the referenced parameter must have for this dependency to apply. */
conditionValue?: string;
}
export interface NestedParameter {
+ /** Human-readable display name of the parameter. */
name: string;
+ /** Brief description of what this parameter does or accepts. */
description?: string;
+ /** Optional grouping label for organising related parameters in the UI. */
group?: string;
+ /** Whether this is a boolean flag, a key-value option, or a positional argument. */
parameterType: ParameterType;
+ /** The data type of the parameter's value. */
dataType: ParameterDataType;
+ /** Additional metadata such as tags. */
metadata?: ParameterMetadata;
+ /** Whether the user must provide this parameter. */
isRequired?: boolean;
+ /** Whether this parameter can be specified multiple times. */
isRepeatable?: boolean;
+ /** Whether this parameter applies to all commands rather than a single command. */
isGlobal?: boolean;
+ /** The single-character short flag (e.g. "-v"). */
shortFlag?: string;
+ /** The long-form flag or option name (e.g. "--verbose"). */
longFlag?: string;
+ /** Zero-based position index for positional arguments. */
position?: number;
+ /** Display sort position relative to sibling parameters. */
sortOrder?: number;
+ /** Separator character used when the option accepts multiple values in one argument (e.g. ","). */
arraySeparator?: string;
+ /** Separator between key and value for key=value style options (e.g. "="). */
keyValueSeparator?: string;
+ /** Allowed enum choices when dataType is "Enum". */
enum?: ParameterEnumValues;
+ /** Validation rules applied to this parameter's value. */
validations?: NestedParameterValidation[];
+ /** Dependencies on other parameters (requires or conflicts-with relationships). */
dependencies?: NestedParameterDependency[];
}
export interface NestedCommand {
+ /** Human-readable display name of the command. */
name: string;
+ /** Brief description of what this command does. */
description?: string;
+ /** Whether this command opens an interactive session or prompt. */
interactive?: boolean;
- isDefault: boolean;
+ /** Display sort position relative to sibling commands. */
sortOrder: number;
+ /** Parameters that belong directly to this command. */
parameters: NestedParameter[];
+ /** Nested subcommands of this command. */
subcommands: NestedCommand[];
}
export interface NestedExclusionGroup {
+ /** Human-readable name for this exclusion group. */
name: string;
+ /** Whether parameters in this group are mutually exclusive or one is required. */
exclusionType: ExclusionType;
+ /** Names of the parameters that participate in this exclusion group. */
parameters: string[];
}
export interface NestedTool {
+ /** Optional JSON schema URI for validation. */
$schema?: string;
+ /** Unique machine-readable identifier for the tool (e.g. "httpx"). */
name: string;
+ /** Human-readable display name for the tool (e.g. "HTTPx"). */
displayName: string;
+ /** General information about the tool such as description, version, and URL. */
info?: ToolInfo;
+ /** The homepage or documentation URL for the tool. */
url?: string;
+ /** Parameters that apply to all commands globally. */
globalParameters: NestedParameter[];
+ /** Hierarchical list of commands and their nested subcommands. */
commands: NestedCommand[];
+ /** Groups of parameters with mutual exclusion or required-one-of constraints. */
exclusionGroups?: NestedExclusionGroup[] | null;
+ /** Arbitrary metadata attached to the tool. */
metadata?: ToolMetadata;
}
diff --git a/registry/commandly/utils/flat.ts b/registry/commandly/utils/flat.ts
index f2b24c0..1ee93bd 100644
--- a/registry/commandly/utils/flat.ts
+++ b/registry/commandly/utils/flat.ts
@@ -39,15 +39,6 @@ export const getCommandPath = (command: Command, tool: Tool): string => {
if (!path) return command.name;
- if (command.name === tool.name && command.isDefault) {
- return tool.name;
- }
-
- const rootCommand = tool.commands.find((c) => c.name === tool.name);
- if (rootCommand?.isDefault && path[0] === tool.name) {
- path[0] = tool.name;
- }
-
return path.join(" ");
};
@@ -85,7 +76,6 @@ export const exportToStructuredJSON = (tool: Tool) => {
name: tool.name,
displayName: tool.displayName,
info: tool.info,
- url: tool.info?.url,
commands: tool.commands.map((cmd) => ({ ...cmd })),
parameters: tool.parameters.map(({ metadata: _metadata, ...param }) => param),
exclusionGroups: tool.exclusionGroups,
@@ -93,17 +83,6 @@ export const exportToStructuredJSON = (tool: Tool) => {
};
};
-export const createNewCommand = (parentKey?: string): Command => {
- const name = randomCommandName();
- return {
- key: slugify(name),
- parentCommandKey: parentKey,
- name,
- isDefault: false,
- sortOrder: 1,
- };
-};
-
export const createNewParameter = (isGlobal: boolean, commandKey?: string): Parameter => {
return {
key: "",
@@ -116,16 +95,6 @@ export const createNewParameter = (isGlobal: boolean, commandKey?: string): Para
};
};
-export const randomCommandName = () => {
- const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
- let result = "";
- const charactersLength = characters.length;
- for (let i = 0; i < 7; i++) {
- result += characters.charAt(Math.floor(Math.random() * charactersLength));
- }
- return result;
-};
-
const isEmpty = (value: object | null | undefined): boolean => {
if (value == null) return true;
if (Array.isArray(value)) return value.length === 0;
diff --git a/registry/commandly/utils/nested.ts b/registry/commandly/utils/nested.ts
index a215760..9b9ce2e 100644
--- a/registry/commandly/utils/nested.ts
+++ b/registry/commandly/utils/nested.ts
@@ -44,7 +44,6 @@ export const convertToNestedStructure = (tool: Tool): NestedTool => {
name: cmd.name,
description: cmd.description,
interactive: cmd.interactive,
- isDefault: cmd.isDefault ?? false,
sortOrder: cmd.sortOrder ?? 0,
parameters: commandParameters.map(convertParameter),
subcommands: buildNestedCommands(commands, cmd.key),
diff --git a/scripts/generate-tools-json.ts b/scripts/generate-tools-json.ts
index 2dd8cea..71d0ec9 100644
--- a/scripts/generate-tools-json.ts
+++ b/scripts/generate-tools-json.ts
@@ -13,7 +13,7 @@ const tools = files.sort().map((file) => {
name: tool.name,
displayName: tool.displayName || tool.name,
description: tool.info?.description,
- url: tool.info?.url,
+ info: tool.info,
};
});
diff --git a/src/components/ai-chat/ai-chat-message-mapping.ts b/src/components/ai-chat/ai-chat-message-mapping.ts
new file mode 100644
index 0000000..943e1a1
--- /dev/null
+++ b/src/components/ai-chat/ai-chat-message-mapping.ts
@@ -0,0 +1,165 @@
+import { Tool } from "@/components/commandly/types/flat";
+import { getToolName, isReasoningUIPart, isTextUIPart, isToolUIPart, type UIMessage } from "ai";
+
+export interface ToolCallEntry {
+ toolCallId: string;
+ toolName: string;
+ approvalId?: string;
+ title: string;
+ input: Record
;
+ state:
+ | "input-streaming"
+ | "input-available"
+ | "approval-requested"
+ | "approval-responded"
+ | "output-available"
+ | "output-denied"
+ | "output-error";
+ output?: unknown;
+ errorText?: string;
+ originalTool?: Tool;
+ previewTool?: Tool;
+}
+
+export interface ChatMessage {
+ id: string;
+ role: "user" | "assistant";
+ content: string;
+ toolApplied?: boolean;
+ toolCalls?: ToolCallEntry[];
+ isEditing?: boolean;
+ editingContent?: string;
+ reasoningContent?: string;
+}
+
+export interface ApprovalArtifact {
+ approvalId: string;
+ previewTool: Tool;
+ originalTool: Tool;
+ summary: string;
+}
+
+function getToolTitle(toolName: string, input: Record, fallbackTitle?: string) {
+ if (fallbackTitle) {
+ return fallbackTitle;
+ }
+
+ if (toolName === "editTool") {
+ return typeof input.summary === "string" ? input.summary : "Editing…";
+ }
+
+ if (toolName === "readTool") {
+ const summary = input.summary as string | undefined;
+ const jsonPath = input.jsonPath as string | undefined;
+ const pathLabel = jsonPath && jsonPath !== "$" ? jsonPath : "whole tool";
+ return summary ? `${summary} (${pathLabel})` : `Reading ${pathLabel}…`;
+ }
+
+ if (toolName === "applyToolDefinition") {
+ return "Preparing to apply";
+ }
+
+ if (toolName === "tavilySearch") {
+ return "Web Search";
+ }
+
+ if (toolName === "tavilyExtract") {
+ return "Extract Content";
+ }
+
+ return toolName;
+}
+
+function toToolInput(value: unknown): Record {
+ return value && typeof value === "object" && !Array.isArray(value)
+ ? (value as Record)
+ : {};
+}
+
+export function toChatMessage(
+ message: UIMessage,
+ approvalArtifacts: Record,
+): ChatMessage {
+ const content = message.parts
+ .filter(isTextUIPart)
+ .map((part) => part.text)
+ .join("");
+
+ const reasoningContent = message.parts
+ .filter(isReasoningUIPart)
+ .map((part) => part.text)
+ .join("");
+
+ const toolCalls = message.parts.filter(isToolUIPart).map((part) => {
+ const toolName = getToolName(part);
+ const input = toToolInput("input" in part ? part.input : undefined);
+ const approvalId = part.approval?.id;
+ const artifact = approvalId ? approvalArtifacts[approvalId] : undefined;
+
+ return {
+ toolCallId: part.toolCallId,
+ toolName,
+ approvalId,
+ title: getToolTitle(toolName, input, part.title),
+ input,
+ state: part.state,
+ output: "output" in part ? part.output : undefined,
+ errorText: "errorText" in part ? part.errorText : undefined,
+ originalTool: artifact?.originalTool,
+ previewTool: artifact?.previewTool,
+ } satisfies ToolCallEntry;
+ });
+
+ return {
+ id: message.id,
+ role: message.role === "assistant" ? "assistant" : "user",
+ content,
+ toolApplied: toolCalls.some(
+ (toolCall) =>
+ toolCall.toolName === "applyToolDefinition" && toolCall.state === "output-available",
+ ),
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
+ reasoningContent: reasoningContent || undefined,
+ };
+}
+
+export function countCompletedToolCalls(messages: UIMessage[]): number {
+ return messages.reduce(
+ (count, message) =>
+ count +
+ message.parts.filter(
+ (part) =>
+ isToolUIPart(part) &&
+ (part.state === "output-available" ||
+ part.state === "output-error" ||
+ part.state === "output-denied"),
+ ).length,
+ 0,
+ );
+}
+
+export function findPendingApproval(messages: ChatMessage[]) {
+ for (let index = messages.length - 1; index >= 0; index--) {
+ const applyToolCall = messages[index].toolCalls?.find(
+ (toolCall) =>
+ toolCall.toolName === "applyToolDefinition" &&
+ toolCall.state === "approval-requested" &&
+ toolCall.approvalId,
+ );
+
+ if (applyToolCall?.approvalId && applyToolCall.originalTool && applyToolCall.previewTool) {
+ return {
+ approvalId: applyToolCall.approvalId,
+ previewTool: applyToolCall.previewTool,
+ originalTool: applyToolCall.originalTool,
+ summary:
+ typeof applyToolCall.input.summary === "string"
+ ? applyToolCall.input.summary
+ : "Apply AI changes",
+ messageIndex: index,
+ };
+ }
+ }
+
+ return null;
+}
diff --git a/src/components/ai-chat/ai-chat-persistence.ts b/src/components/ai-chat/ai-chat-persistence.ts
new file mode 100644
index 0000000..17fd826
--- /dev/null
+++ b/src/components/ai-chat/ai-chat-persistence.ts
@@ -0,0 +1,276 @@
+import type { UIMessage } from "ai";
+
+const DB_NAME = "commandly";
+const DB_VERSION = 4;
+
+export interface Chat {
+ id: string;
+ toolName: string;
+ createdAt: number;
+}
+
+export interface StoredMessage {
+ id: string;
+ chatId: string;
+ role: UIMessage["role"];
+ parts: UIMessage["parts"];
+ order: number;
+}
+
+export interface ChatWithPreview extends Chat {
+ preview: string;
+ updatedAt: number;
+ messageCount: number;
+}
+
+function openDB(): Promise {
+ return new Promise((resolve, reject) => {
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
+ req.onupgradeneeded = (event) => {
+ const db = req.result;
+ const oldVersion = (event as IDBVersionChangeEvent).oldVersion;
+
+ if (!db.objectStoreNames.contains("keys")) {
+ db.createObjectStore("keys");
+ }
+
+ if (oldVersion < 4) {
+ if (db.objectStoreNames.contains("sessions")) {
+ db.deleteObjectStore("sessions");
+ }
+ if (db.objectStoreNames.contains("chats")) {
+ db.deleteObjectStore("chats");
+ }
+ if (db.objectStoreNames.contains("messages")) {
+ db.deleteObjectStore("messages");
+ }
+ }
+
+ if (!db.objectStoreNames.contains("chats")) {
+ const chatStore = db.createObjectStore("chats", { keyPath: "id" });
+ chatStore.createIndex("toolName", "toolName", { unique: false });
+ }
+ if (!db.objectStoreNames.contains("messages")) {
+ const msgStore = db.createObjectStore("messages", { keyPath: "id" });
+ msgStore.createIndex("chatId", "chatId", { unique: false });
+ }
+ };
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ });
+}
+
+function toSerializable(value: T): T {
+ return JSON.parse(JSON.stringify(value)) as T;
+}
+
+function getMessageText(message: UIMessage): string {
+ return message.parts
+ .filter(
+ (part): part is Extract => part.type === "text",
+ )
+ .map((part) => part.text)
+ .join("");
+}
+
+export function hasPersistableContent(message: UIMessage): boolean {
+ return message.parts.some((part) => {
+ if (part.type === "text" || part.type === "reasoning") {
+ return part.text.trim().length > 0;
+ }
+ return part.type !== "step-start";
+ });
+}
+
+export function getPersistableMessages(messages: UIMessage[]): UIMessage[] {
+ return messages.filter(
+ (message) => message.role !== "assistant" || hasPersistableContent(message),
+ );
+}
+
+export function getSessionPreview(messages: UIMessage[]): string {
+ const firstUserMessage = messages.find((message) => message.role === "user");
+ return getMessageText(firstUserMessage ?? { id: "", role: "user", parts: [] }).slice(0, 80);
+}
+
+export async function truncateMessages(chatId: string, keepCount: number): Promise {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction("messages", "readwrite");
+ const index = tx.objectStore("messages").index("chatId");
+ const req = index.getAll(chatId);
+ req.onsuccess = () => {
+ for (const msg of req.result as StoredMessage[]) {
+ if (msg.order >= keepCount) {
+ tx.objectStore("messages").delete(msg.id);
+ }
+ }
+ };
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ });
+}
+
+export async function createChat(id: string, toolName: string): Promise {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction("chats", "readwrite");
+ tx.objectStore("chats").put({ id, toolName, createdAt: Date.now() } satisfies Chat);
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ });
+}
+
+export async function upsertMessage(
+ chatId: string,
+ message: UIMessage,
+ order: number,
+): Promise {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction(["chats", "messages"], "readwrite");
+
+ const chatStore = tx.objectStore("chats");
+ const getChat = chatStore.get(chatId);
+ getChat.onsuccess = () => {
+ if (!getChat.result) {
+ tx.abort();
+ reject(new Error(`Chat ${chatId} not found`));
+ return;
+ }
+
+ const stored: StoredMessage = {
+ id: message.id,
+ chatId,
+ role: message.role,
+ parts: toSerializable(message.parts),
+ order,
+ };
+ tx.objectStore("messages").put(stored);
+ };
+
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ });
+}
+
+export async function loadChat(
+ chatId: string,
+): Promise {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction("messages", "readonly");
+ const index = tx.objectStore("messages").index("chatId");
+ const req = index.getAll(chatId);
+ req.onsuccess = () => {
+ const stored = (req.result as StoredMessage[]).sort((a, b) => a.order - b.order);
+ resolve(
+ stored.map((msg) => ({
+ id: msg.id,
+ role: msg.role,
+ parts: msg.parts,
+ })) as UI_MESSAGE[],
+ );
+ };
+ req.onerror = () => reject(req.error);
+ });
+}
+
+export async function getChats(toolName: string, limit = 5): Promise {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction(["chats", "messages"], "readonly");
+ const chatIndex = tx.objectStore("chats").index("toolName");
+ const chatReq = chatIndex.getAll(toolName);
+
+ chatReq.onsuccess = () => {
+ const chats = (chatReq.result as Chat[]).sort((a, b) => b.createdAt - a.createdAt);
+ const msgIndex = tx.objectStore("messages").index("chatId");
+ const results: ChatWithPreview[] = [];
+ let pending = chats.length;
+
+ if (pending === 0) {
+ resolve([]);
+ return;
+ }
+
+ for (const chat of chats) {
+ const msgReq = msgIndex.getAll(chat.id);
+ msgReq.onsuccess = () => {
+ const stored = (msgReq.result as StoredMessage[]).sort((a, b) => a.order - b.order);
+ const messages = stored.map(
+ (m) => ({ id: m.id, role: m.role, parts: m.parts }) as UIMessage,
+ );
+ results.push({
+ ...chat,
+ preview: getSessionPreview(messages),
+ updatedAt: chat.createdAt,
+ messageCount: stored.length,
+ });
+ pending--;
+ if (pending === 0) {
+ results.sort((a, b) => b.createdAt - a.createdAt);
+ resolve(results.slice(0, limit));
+ }
+ };
+ msgReq.onerror = () => {
+ pending--;
+ if (pending === 0) {
+ results.sort((a, b) => b.createdAt - a.createdAt);
+ resolve(results.slice(0, limit));
+ }
+ };
+ }
+ };
+ chatReq.onerror = () => reject(chatReq.error);
+ });
+}
+
+export async function deleteChat(chatId: string): Promise {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction(["chats", "messages"], "readwrite");
+
+ tx.objectStore("chats").delete(chatId);
+
+ const msgIndex = tx.objectStore("messages").index("chatId");
+ const req = msgIndex.getAll(chatId);
+ req.onsuccess = () => {
+ for (const msg of req.result as StoredMessage[]) {
+ tx.objectStore("messages").delete(msg.id);
+ }
+ };
+
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ });
+}
+
+export async function deleteMessage(messageId: string): Promise {
+ const db = await openDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction("messages", "readwrite");
+
+ const getReq = tx.objectStore("messages").get(messageId);
+ getReq.onsuccess = () => {
+ const target = getReq.result as StoredMessage | undefined;
+ if (!target) {
+ resolve();
+ return;
+ }
+
+ const chatIndex = tx.objectStore("messages").index("chatId");
+ const allReq = chatIndex.getAll(target.chatId);
+ allReq.onsuccess = () => {
+ for (const msg of allReq.result as StoredMessage[]) {
+ if (msg.order >= target.order) {
+ tx.objectStore("messages").delete(msg.id);
+ }
+ }
+ };
+ };
+
+ tx.oncomplete = () => resolve();
+ tx.onerror = () => reject(tx.error);
+ });
+}
diff --git a/src/components/ai-chat/api-key-settings.tsx b/src/components/ai-chat/api-key-settings.tsx
new file mode 100644
index 0000000..ddd459e
--- /dev/null
+++ b/src/components/ai-chat/api-key-settings.tsx
@@ -0,0 +1,147 @@
+import { MODEL_GROUPS } from "./model-picker";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Separator } from "@/components/ui/separator";
+import { type AIProvider } from "@/lib/ai-keys";
+import { CheckIcon, EyeIcon, EyeOffIcon, GlobeIcon, Settings2Icon } from "lucide-react";
+import { useState } from "react";
+
+interface AIKeyState {
+ key: string;
+ setKey: (key: string) => void;
+ isSaved: boolean;
+ setSaved: (saved: boolean) => void;
+}
+
+interface ApiKeySettingsProps {
+ allProviderKeys: Record, AIKeyState>;
+ tavilyKeys: AIKeyState;
+}
+
+export function ApiKeySettings({ allProviderKeys, tavilyKeys }: ApiKeySettingsProps) {
+ const [revealed, setRevealed] = useState>({});
+
+ const toggleReveal = (key: string) => setRevealed((prev) => ({ ...prev, [key]: !prev[key] }));
+ return (
+
+
+
+
+
+
+
+ API Keys
+
+ {MODEL_GROUPS.map((g, gi) => {
+ const keys = allProviderKeys[g.provider as Exclude
];
+ return (
+
+ {gi > 0 &&
}
+
+
+
+
{g.label}
+ {keys.isSaved &&
}
+
+
+ keys.setKey(e.target.value)}
+ className="h-8 pr-8 text-xs"
+ placeholder={g.keyPlaceholder}
+ />
+ toggleReveal(g.provider)}
+ tabIndex={-1}
+ aria-label={revealed[g.provider] ? "Hide key" : "Reveal key"}
+ >
+ {revealed[g.provider] ? (
+
+ ) : (
+
+ )}
+
+
+
+ keys.setSaved(c === "indeterminate" ? false : c)}
+ />
+
+ Save key locally
+
+
+
+
+ );
+ })}
+
+
+
+
+ Tavily (Web Search)
+ {tavilyKeys.isSaved && }
+
+
+ tavilyKeys.setKey(e.target.value)}
+ className="h-8 pr-8 text-xs"
+ placeholder="tvly-..."
+ />
+ toggleReveal("tavily")}
+ tabIndex={-1}
+ aria-label={revealed["tavily"] ? "Hide key" : "Reveal key"}
+ >
+ {revealed["tavily"] ? (
+
+ ) : (
+
+ )}
+
+
+
+ tavilyKeys.setSaved(c === "indeterminate" ? false : c)}
+ />
+
+ Save key locally
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ai-chat/diff-view.tsx b/src/components/ai-chat/diff-view.tsx
new file mode 100644
index 0000000..e5753e0
--- /dev/null
+++ b/src/components/ai-chat/diff-view.tsx
@@ -0,0 +1,114 @@
+import { Tool } from "@/components/commandly/types/flat";
+import { exportToStructuredJSON } from "@/components/commandly/utils/flat";
+import { cn } from "@/lib/utils";
+
+function computeLineDiff(
+ a: string,
+ b: string,
+): Array<{ type: "same" | "add" | "remove"; text: string }> {
+ const aLines = a.split("\n");
+ const bLines = b.split("\n");
+ const m = aLines.length;
+ const n = bLines.length;
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
+
+ for (let i = 1; i <= m; i++) {
+ for (let j = 1; j <= n; j++) {
+ dp[i][j] =
+ aLines[i - 1] === bLines[j - 1]
+ ? dp[i - 1][j - 1] + 1
+ : Math.max(dp[i - 1][j], dp[i][j - 1]);
+ }
+ }
+
+ const result: Array<{ type: "same" | "add" | "remove"; text: string }> = [];
+ let i = m;
+ let j = n;
+
+ while (i > 0 || j > 0) {
+ if (i > 0 && j > 0 && aLines[i - 1] === bLines[j - 1]) {
+ result.unshift({ type: "same", text: aLines[i - 1] });
+ i--;
+ j--;
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
+ result.unshift({ type: "add", text: bLines[j - 1] });
+ j--;
+ } else {
+ result.unshift({ type: "remove", text: aLines[i - 1] });
+ i--;
+ }
+ }
+
+ return result;
+}
+
+export function DiffView({ original, updated }: { original: Tool; updated: Tool }) {
+ const aJson = JSON.stringify(exportToStructuredJSON(original), null, 2);
+ const bJson = JSON.stringify(exportToStructuredJSON(updated), null, 2);
+ const diff = computeLineDiff(aJson, bJson);
+ const visibleSet = new Set();
+
+ for (const index of diff.flatMap((line, currentIndex) =>
+ line.type !== "same" ? [currentIndex] : [],
+ )) {
+ for (
+ let cursor = Math.max(0, index - 2);
+ cursor <= Math.min(diff.length - 1, index + 2);
+ cursor++
+ ) {
+ visibleSet.add(cursor);
+ }
+ }
+
+ if (visibleSet.size === 0) {
+ return No changes
;
+ }
+
+ const sortedIndices = [...visibleSet].sort((a, b) => a - b);
+ const chunks: number[][] = [];
+ let currentChunk: number[] = [];
+
+ for (let index = 0; index < sortedIndices.length; index++) {
+ if (currentChunk.length === 0 || sortedIndices[index] === sortedIndices[index - 1] + 1) {
+ currentChunk.push(sortedIndices[index]);
+ continue;
+ }
+
+ chunks.push(currentChunk);
+ currentChunk = [sortedIndices[index]];
+ }
+
+ if (currentChunk.length > 0) {
+ chunks.push(currentChunk);
+ }
+
+ return (
+
+ {chunks.map((chunk, chunkIndex) => (
+
+ {chunkIndex > 0 && (
+
···
+ )}
+ {chunk.map((lineIndex) => {
+ const line = diff[lineIndex];
+ return (
+
+ {line.type === "add" ? "+ " : line.type === "remove" ? "- " : " "}
+ {line.text}
+
+ );
+ })}
+
+ ))}
+
+ );
+}
diff --git a/src/components/ai-chat/extracted-content.tsx b/src/components/ai-chat/extracted-content.tsx
new file mode 100644
index 0000000..e9d6410
--- /dev/null
+++ b/src/components/ai-chat/extracted-content.tsx
@@ -0,0 +1,43 @@
+export interface ExtractResult {
+ url: string;
+ raw_content: string;
+ hasMore?: boolean;
+}
+
+export function ExtractedContent({ results }: { results: ExtractResult[] }) {
+ return (
+
+ {results.map((result) => {
+ let hostname = result.url;
+
+ try {
+ hostname = new URL(result.url).hostname;
+ } catch {
+ // Ignore invalid URLs in partial tool output.
+ }
+
+ return (
+
+
+
+ {result.raw_content}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/ai-chat/model-picker.tsx b/src/components/ai-chat/model-picker.tsx
new file mode 100644
index 0000000..8957469
--- /dev/null
+++ b/src/components/ai-chat/model-picker.tsx
@@ -0,0 +1,333 @@
+import { Button } from "@/components/ui/button";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { type AIProvider } from "@/lib/ai-keys";
+import { cn } from "@/lib/utils";
+import { CheckIcon, ChevronsUpDownIcon, ChevronUpIcon } from "lucide-react";
+import { useState } from "react";
+
+interface AIKeyState {
+ key: string;
+ setKey: (key: string) => void;
+ isSaved: boolean;
+ setSaved: (saved: boolean) => void;
+}
+
+export const MODEL_GROUPS: {
+ provider: AIProvider;
+ label: string;
+ logoUrl: string;
+ keyPlaceholder: string;
+ models: { value: string; label: string; reasoning?: true }[];
+}[] = [
+ {
+ provider: "openai",
+ label: "OpenAI",
+ logoUrl: "https://www.google.com/s2/favicons?domain=openai.com&sz=32",
+ keyPlaceholder: "sk-...",
+ models: [
+ { value: "gpt-5.4", label: "GPT-5.4", reasoning: true },
+ { value: "gpt-5.4-pro", label: "GPT-5.4 pro", reasoning: true },
+ { value: "gpt-5.4-mini", label: "GPT-5.4 mini", reasoning: true },
+ { value: "gpt-5.4-nano", label: "GPT-5.4 nano", reasoning: true },
+ { value: "gpt-5", label: "GPT-5", reasoning: true },
+ { value: "gpt-5-pro", label: "GPT-5 pro", reasoning: true },
+ { value: "gpt-5-mini", label: "GPT-5 mini", reasoning: true },
+ { value: "gpt-5-nano", label: "GPT-5 nano", reasoning: true },
+ { value: "gpt-5.2", label: "GPT-5.2", reasoning: true },
+ { value: "gpt-5.2-pro", label: "GPT-5.2 pro", reasoning: true },
+ { value: "gpt-5.1", label: "GPT-5.1", reasoning: true },
+ { value: "gpt-4.1", label: "GPT-4.1" },
+ { value: "gpt-4.1-mini", label: "GPT-4.1 mini" },
+ { value: "gpt-4.1-nano", label: "GPT-4.1 nano" },
+ { value: "o4-mini", label: "o4-mini", reasoning: true },
+ { value: "o3", label: "o3", reasoning: true },
+ { value: "o3-pro", label: "o3-pro", reasoning: true },
+ { value: "o1-pro", label: "o1-pro", reasoning: true },
+ { value: "gpt-5-codex", label: "GPT-5 Codex", reasoning: true },
+ { value: "gpt-5.3-codex", label: "GPT-5.3 Codex", reasoning: true },
+ { value: "gpt-5.2-codex", label: "GPT-5.2 Codex", reasoning: true },
+ { value: "gpt-5.1-codex", label: "GPT-5.1 Codex", reasoning: true },
+ { value: "gpt-5.1-codex-max", label: "GPT-5.1 Codex Max", reasoning: true },
+ { value: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex mini", reasoning: true },
+ ],
+ },
+ {
+ provider: "anthropic",
+ label: "Anthropic",
+ logoUrl: "https://www.google.com/s2/favicons?domain=anthropic.com&sz=32",
+ keyPlaceholder: "sk-ant-...",
+ models: [
+ { value: "claude-opus-4-6", label: "Claude Opus 4.6", reasoning: true },
+ { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", reasoning: true },
+ { value: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
+ { value: "claude-opus-4-5", label: "Claude Opus 4.5", reasoning: true },
+ { value: "claude-sonnet-4-5", label: "Claude Sonnet 4.5", reasoning: true },
+ ],
+ },
+ {
+ provider: "google",
+ label: "Google",
+ logoUrl: "https://www.google.com/s2/favicons?domain=google.com&sz=32",
+ keyPlaceholder: "AIza...",
+ models: [
+ { value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro", reasoning: true },
+ { value: "gemini-3-flash-preview", label: "Gemini 3 Flash", reasoning: true },
+ { value: "gemini-2.5-pro", label: "Gemini 2.5 Pro", reasoning: true },
+ { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash", reasoning: true },
+ { value: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" },
+ ],
+ },
+ {
+ provider: "groq",
+ label: "Groq",
+ logoUrl: "https://www.google.com/s2/favicons?domain=groq.com&sz=32",
+ keyPlaceholder: "gsk_...",
+ models: [
+ { value: "meta-llama/llama-4-maverick-17b-128e-instruct", label: "Llama 4 Maverick" },
+ { value: "meta-llama/llama-4-scout-17b-16e-instruct", label: "Llama 4 Scout" },
+ { value: "qwen/qwen3-32b", label: "Qwen 3 32B" },
+ { value: "llama-3.3-70b-versatile", label: "Llama 3.3 70B" },
+ { value: "llama-3.1-8b-instant", label: "Llama 3.1 8B" },
+ ],
+ },
+ {
+ provider: "mistral",
+ label: "Mistral",
+ logoUrl: "https://www.google.com/s2/favicons?domain=mistral.ai&sz=32",
+ keyPlaceholder: "...",
+ models: [
+ { value: "mistral-large-latest", label: "Mistral Large" },
+ { value: "magistral-medium-latest", label: "Magistral Medium" },
+ { value: "codestral-latest", label: "Codestral" },
+ { value: "mistral-medium-latest", label: "Mistral Medium" },
+ { value: "mistral-small-latest", label: "Mistral Small" },
+ ],
+ },
+ {
+ provider: "xai",
+ label: "xAI",
+ logoUrl: "https://www.google.com/s2/favicons?domain=x.ai&sz=32",
+ keyPlaceholder: "xai-...",
+ models: [
+ { value: "grok-4-0709", label: "Grok 4" },
+ { value: "grok-3", label: "Grok 3" },
+ { value: "grok-3-mini", label: "Grok 3 mini" },
+ ],
+ },
+];
+
+export function providerForModel(model: string): AIProvider {
+ if (model.startsWith("claude")) return "anthropic";
+ if (model.startsWith("gemini")) return "google";
+ if (model.startsWith("llama") || model.startsWith("meta-llama") || model.startsWith("qwen"))
+ return "groq";
+ if (model.startsWith("mistral") || model.startsWith("codestral") || model.startsWith("magistral"))
+ return "mistral";
+ if (model.startsWith("grok")) return "xai";
+ return "openai";
+}
+
+export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high";
+
+export function getProviderOptions(
+ provider: AIProvider,
+ model: string,
+ effort: ReasoningEffort,
+): Record {
+ if (provider === "openai") {
+ if (effort === "none") return {};
+ const effortMap = { minimal: "low", low: "low", medium: "medium", high: "high" } as const;
+ return { openai: { reasoningEffort: effortMap[effort] } };
+ }
+ if (provider === "anthropic") {
+ if (effort === "none") return { anthropic: { thinking: { type: "disabled" } } };
+ const budgetTokens = { minimal: 256, low: 1024, medium: 5000, high: 16000 }[effort];
+ return { anthropic: { thinking: { type: "enabled", budgetTokens } } };
+ }
+ if (provider === "google") {
+ if (effort === "none") return { google: { thinkingConfig: { thinkingBudget: 0 } } };
+ const thinkingBudget = { minimal: 128, low: 512, medium: 4096, high: 16000 }[effort];
+ return { google: { thinkingConfig: { thinkingBudget } } };
+ }
+ return {};
+}
+
+interface ModelPickerProps {
+ model: string;
+ setModel: (m: string) => void;
+ provider: AIProvider;
+ allProviderKeys: Record, AIKeyState>;
+}
+
+export function ModelPicker({ model, setModel, provider, allProviderKeys }: ModelPickerProps) {
+ const [open, setOpen] = useState(false);
+ const [selectedProvider, setSelectedProvider] = useState(provider);
+
+ const currentGroup = MODEL_GROUPS.find((g) => g.provider === provider);
+ const currentModelLabel =
+ MODEL_GROUPS.flatMap((g) => g.models).find((m) => m.value === model)?.label ?? model;
+ const selectedGroup =
+ MODEL_GROUPS.find((g) => g.provider === selectedProvider) ?? MODEL_GROUPS[0];
+ const selectedProviderKeys = allProviderKeys[selectedProvider as Exclude];
+
+ return (
+ {
+ setOpen(o);
+ if (o) setSelectedProvider(provider);
+ }}
+ >
+
+
+
+ {currentGroup && (
+
+ )}
+ {currentModelLabel}
+
+
+
+
+
+
+
+ {MODEL_GROUPS.map((g) => {
+ const isKeyConfigured =
+ allProviderKeys[g.provider as Exclude
]?.isSaved;
+ return (
+ setSelectedProvider(g.provider)}
+ title={`${g.label}${!isKeyConfigured ? " (no API key)" : ""}`}
+ className={cn(
+ "flex h-9 w-10 items-center justify-center rounded border border-transparent transition-all",
+ selectedProvider === g.provider
+ ? "border-border bg-accent"
+ : "opacity-50 hover:bg-muted hover:opacity-80",
+ )}
+ >
+
+
+ );
+ })}
+
+
+
+ {selectedGroup.label}
+
+ {!selectedProviderKeys?.isSaved && (
+
No API key configured
+ )}
+
+ {selectedGroup.models.map((m) => {
+ const isKeyConfigured = selectedProviderKeys?.isSaved;
+ return (
+ {
+ setModel(m.value);
+ setOpen(false);
+ }}
+ className={cn(
+ "flex w-full items-center justify-between rounded px-2 py-2 text-xs transition-colors",
+ isKeyConfigured
+ ? "cursor-pointer hover:bg-accent hover:text-accent-foreground"
+ : "cursor-not-allowed opacity-40",
+ model === m.value && "bg-accent text-accent-foreground",
+ )}
+ >
+ {m.label}
+ {model === m.value && }
+
+ );
+ })}
+
+
+
+
+
+ );
+}
+
+const EFFORT_OPTIONS: { value: ReasoningEffort; label: string }[] = [
+ { value: "none", label: "Off" },
+ { value: "minimal", label: "Min" },
+ { value: "low", label: "Low" },
+ { value: "medium", label: "Med" },
+ { value: "high", label: "High" },
+];
+
+interface ReasoningEffortPickerProps {
+ model: string;
+ effort: ReasoningEffort | null;
+ onEffortChange: (e: ReasoningEffort | null) => void;
+}
+
+export function ReasoningEffortPicker({
+ model,
+ effort,
+ onEffortChange,
+}: ReasoningEffortPickerProps) {
+ const [open, setOpen] = useState(false);
+
+ const isReasoningModel =
+ MODEL_GROUPS.flatMap((g) => g.models).find((m) => m.value === model)?.reasoning === true;
+ if (!isReasoningModel) return null;
+
+ const current = EFFORT_OPTIONS.find((o) => o.value === effort);
+
+ return (
+
+
+
+ Think:
+ {current?.label ?? "Off"}
+
+
+
+
+ {EFFORT_OPTIONS.map(({ value, label }) => (
+ {
+ onEffortChange(value);
+ setOpen(false);
+ }}
+ className={cn(
+ "flex w-full items-center justify-between rounded px-2 py-1.5 text-xs transition-colors hover:bg-accent hover:text-accent-foreground",
+ effort === value && "bg-accent text-accent-foreground",
+ )}
+ >
+ {label}
+ {effort === value && }
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/ai-chat/web-search-results.tsx b/src/components/ai-chat/web-search-results.tsx
new file mode 100644
index 0000000..66cc596
--- /dev/null
+++ b/src/components/ai-chat/web-search-results.tsx
@@ -0,0 +1,46 @@
+export interface WebSearchResult {
+ title: string;
+ url: string;
+ content: string;
+}
+
+export function WebSearchResults({ results }: { results: WebSearchResult[] }) {
+ return (
+
+ );
+}
diff --git a/src/components/ai-elements/chain-of-thought.tsx b/src/components/ai-elements/chain-of-thought.tsx
new file mode 100644
index 0000000..3c3461e
--- /dev/null
+++ b/src/components/ai-elements/chain-of-thought.tsx
@@ -0,0 +1,209 @@
+"use client";
+
+import { Badge } from "@/components/ui/badge";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
+import { cn } from "@/lib/utils";
+import { useControllableState } from "@radix-ui/react-use-controllable-state";
+import type { LucideIcon } from "lucide-react";
+import { BrainIcon, ChevronDownIcon, DotIcon } from "lucide-react";
+import type { ComponentProps, ReactNode } from "react";
+import { createContext, memo, useContext, useMemo } from "react";
+
+interface ChainOfThoughtContextValue {
+ isOpen: boolean;
+ setIsOpen: (open: boolean) => void;
+}
+
+const ChainOfThoughtContext = createContext(null);
+
+const useChainOfThought = () => {
+ const context = useContext(ChainOfThoughtContext);
+ if (!context) {
+ throw new Error("ChainOfThought components must be used within ChainOfThought");
+ }
+ return context;
+};
+
+export type ChainOfThoughtProps = ComponentProps<"div"> & {
+ open?: boolean;
+ defaultOpen?: boolean;
+ onOpenChange?: (open: boolean) => void;
+};
+
+export const ChainOfThought = memo(
+ ({
+ className,
+ open,
+ defaultOpen = false,
+ onOpenChange,
+ children,
+ ...props
+ }: ChainOfThoughtProps) => {
+ const [isOpen, setIsOpen] = useControllableState({
+ defaultProp: defaultOpen,
+ onChange: onOpenChange,
+ prop: open,
+ });
+
+ const chainOfThoughtContext = useMemo(() => ({ isOpen, setIsOpen }), [isOpen, setIsOpen]);
+
+ return (
+
+
+ {children}
+
+
+ );
+ },
+);
+
+export type ChainOfThoughtHeaderProps = ComponentProps;
+
+export const ChainOfThoughtHeader = memo(
+ ({ className, children, ...props }: ChainOfThoughtHeaderProps) => {
+ const { isOpen, setIsOpen } = useChainOfThought();
+
+ return (
+
+
+
+ {children ?? "Chain of Thought"}
+
+
+
+ );
+ },
+);
+
+export type ChainOfThoughtStepProps = ComponentProps<"div"> & {
+ icon?: LucideIcon;
+ label: ReactNode;
+ description?: ReactNode;
+ status?: "complete" | "active" | "pending";
+};
+
+const stepStatusStyles = {
+ active: "text-foreground",
+ complete: "text-muted-foreground",
+ pending: "text-muted-foreground/50",
+};
+
+export const ChainOfThoughtStep = memo(
+ ({
+ className,
+ icon: Icon = DotIcon,
+ label,
+ description,
+ status = "complete",
+ children,
+ ...props
+ }: ChainOfThoughtStepProps) => (
+
+
+
+
{label}
+ {description &&
{description}
}
+ {children}
+
+
+ ),
+);
+
+export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;
+
+export const ChainOfThoughtSearchResults = memo(
+ ({ className, ...props }: ChainOfThoughtSearchResultsProps) => (
+
+ ),
+);
+
+export type ChainOfThoughtSearchResultProps = ComponentProps;
+
+export const ChainOfThoughtSearchResult = memo(
+ ({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (
+
+ {children}
+
+ ),
+);
+
+export type ChainOfThoughtContentProps = ComponentProps;
+
+export const ChainOfThoughtContent = memo(
+ ({ className, children, ...props }: ChainOfThoughtContentProps) => {
+ const { isOpen } = useChainOfThought();
+
+ return (
+
+
+ {children}
+
+
+ );
+ },
+);
+
+export type ChainOfThoughtImageProps = ComponentProps<"div"> & {
+ caption?: string;
+};
+
+export const ChainOfThoughtImage = memo(
+ ({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (
+
+
+ {children}
+
+ {caption &&
{caption}
}
+
+ ),
+);
+
+ChainOfThought.displayName = "ChainOfThought";
+ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";
+ChainOfThoughtStep.displayName = "ChainOfThoughtStep";
+ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";
+ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";
+ChainOfThoughtContent.displayName = "ChainOfThoughtContent";
+ChainOfThoughtImage.displayName = "ChainOfThoughtImage";
diff --git a/src/components/ai-elements/context.tsx b/src/components/ai-elements/context.tsx
new file mode 100644
index 0000000..96128a9
--- /dev/null
+++ b/src/components/ai-elements/context.tsx
@@ -0,0 +1,399 @@
+"use client";
+
+import { Button } from "@/components/ui/button";
+import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
+import { Progress } from "@/components/ui/progress";
+import { cn } from "@/lib/utils";
+import type { LanguageModelUsage } from "ai";
+import type { ComponentProps } from "react";
+import { createContext, useContext, useMemo } from "react";
+import { getUsage } from "tokenlens";
+
+const PERCENT_MAX = 100;
+const ICON_RADIUS = 10;
+const ICON_VIEWBOX = 24;
+const ICON_CENTER = 12;
+const ICON_STROKE_WIDTH = 2;
+
+type ModelId = string;
+
+interface ContextSchema {
+ usedTokens: number;
+ maxTokens: number;
+ usage?: LanguageModelUsage;
+ modelId?: ModelId;
+}
+
+const ContextContext = createContext(null);
+
+const useContextValue = () => {
+ const context = useContext(ContextContext);
+
+ if (!context) {
+ throw new Error("Context components must be used within Context");
+ }
+
+ return context;
+};
+
+export type ContextProps = ComponentProps & ContextSchema;
+
+export const Context = ({ usedTokens, maxTokens, usage, modelId, ...props }: ContextProps) => {
+ const contextValue = useMemo(
+ () => ({ maxTokens, modelId, usage, usedTokens }),
+ [maxTokens, modelId, usage, usedTokens],
+ );
+
+ return (
+
+
+
+ );
+};
+
+const ContextIcon = () => {
+ const { usedTokens, maxTokens } = useContextValue();
+ const circumference = 2 * Math.PI * ICON_RADIUS;
+ const usedPercent = usedTokens / maxTokens;
+ const dashOffset = circumference * (1 - usedPercent);
+
+ return (
+
+
+
+
+ );
+};
+
+export type ContextTriggerProps = ComponentProps;
+
+export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
+ const { usedTokens, maxTokens } = useContextValue();
+ const usedPercent = usedTokens / maxTokens;
+ const renderedPercent = new Intl.NumberFormat("en-US", {
+ maximumFractionDigits: 1,
+ style: "percent",
+ }).format(usedPercent);
+
+ return (
+
+ {children ?? (
+
+ {renderedPercent}
+
+
+ )}
+
+ );
+};
+
+export type ContextContentProps = ComponentProps;
+
+export const ContextContent = ({ className, ...props }: ContextContentProps) => (
+
+);
+
+export type ContextContentHeaderProps = ComponentProps<"div">;
+
+export const ContextContentHeader = ({
+ children,
+ className,
+ ...props
+}: ContextContentHeaderProps) => {
+ const { usedTokens, maxTokens } = useContextValue();
+ const usedPercent = usedTokens / maxTokens;
+ const displayPct = new Intl.NumberFormat("en-US", {
+ maximumFractionDigits: 1,
+ style: "percent",
+ }).format(usedPercent);
+ const used = new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ }).format(usedTokens);
+ const total = new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ }).format(maxTokens);
+
+ return (
+
+ {children ?? (
+ <>
+
+
{displayPct}
+
+ {used} / {total}
+
+
+
+ >
+ )}
+
+ );
+};
+
+export type ContextContentBodyProps = ComponentProps<"div">;
+
+export const ContextContentBody = ({ children, className, ...props }: ContextContentBodyProps) => (
+
+ {children}
+
+);
+
+export type ContextContentFooterProps = ComponentProps<"div">;
+
+export const ContextContentFooter = ({
+ children,
+ className,
+ ...props
+}: ContextContentFooterProps) => {
+ const { modelId, usage } = useContextValue();
+ const costUSD = modelId
+ ? getUsage({
+ modelId,
+ usage: {
+ input: usage?.inputTokens ?? 0,
+ output: usage?.outputTokens ?? 0,
+ },
+ }).costUSD?.totalUSD
+ : undefined;
+ const totalCost = new Intl.NumberFormat("en-US", {
+ currency: "USD",
+ style: "currency",
+ }).format(costUSD ?? 0);
+
+ return (
+
+ {children ?? (
+ <>
+ Total cost
+ {totalCost}
+ >
+ )}
+
+ );
+};
+
+const TokensWithCost = ({ tokens, costText }: { tokens?: number; costText?: string }) => (
+
+ {tokens === undefined
+ ? "—"
+ : new Intl.NumberFormat("en-US", {
+ notation: "compact",
+ }).format(tokens)}
+ {costText ? • {costText} : null}
+
+);
+
+export type ContextInputUsageProps = ComponentProps<"div">;
+
+export const ContextInputUsage = ({ className, children, ...props }: ContextInputUsageProps) => {
+ const { usage, modelId } = useContextValue();
+ const inputTokens = usage?.inputTokens ?? 0;
+
+ if (children) {
+ return children;
+ }
+
+ if (!inputTokens) {
+ return null;
+ }
+
+ const inputCost = modelId
+ ? getUsage({
+ modelId,
+ usage: { input: inputTokens, output: 0 },
+ }).costUSD?.totalUSD
+ : undefined;
+ const inputCostText = new Intl.NumberFormat("en-US", {
+ currency: "USD",
+ style: "currency",
+ }).format(inputCost ?? 0);
+
+ return (
+
+ Input
+
+
+ );
+};
+
+export type ContextOutputUsageProps = ComponentProps<"div">;
+
+export const ContextOutputUsage = ({ className, children, ...props }: ContextOutputUsageProps) => {
+ const { usage, modelId } = useContextValue();
+ const outputTokens = usage?.outputTokens ?? 0;
+
+ if (children) {
+ return children;
+ }
+
+ if (!outputTokens) {
+ return null;
+ }
+
+ const outputCost = modelId
+ ? getUsage({
+ modelId,
+ usage: { input: 0, output: outputTokens },
+ }).costUSD?.totalUSD
+ : undefined;
+ const outputCostText = new Intl.NumberFormat("en-US", {
+ currency: "USD",
+ style: "currency",
+ }).format(outputCost ?? 0);
+
+ return (
+
+ Output
+
+
+ );
+};
+
+export type ContextReasoningUsageProps = ComponentProps<"div">;
+
+export const ContextReasoningUsage = ({
+ className,
+ children,
+ ...props
+}: ContextReasoningUsageProps) => {
+ const { usage, modelId } = useContextValue();
+ const reasoningTokens = usage?.reasoningTokens ?? 0;
+
+ if (children) {
+ return children;
+ }
+
+ if (!reasoningTokens) {
+ return null;
+ }
+
+ const reasoningCost = modelId
+ ? getUsage({
+ modelId,
+ usage: { reasoningTokens },
+ }).costUSD?.totalUSD
+ : undefined;
+ const reasoningCostText = new Intl.NumberFormat("en-US", {
+ currency: "USD",
+ style: "currency",
+ }).format(reasoningCost ?? 0);
+
+ return (
+
+ Reasoning
+
+
+ );
+};
+
+export type ContextCacheUsageProps = ComponentProps<"div">;
+
+export const ContextCacheUsage = ({ className, children, ...props }: ContextCacheUsageProps) => {
+ const { usage, modelId } = useContextValue();
+ const cacheTokens = usage?.cachedInputTokens ?? 0;
+
+ if (children) {
+ return children;
+ }
+
+ if (!cacheTokens) {
+ return null;
+ }
+
+ const cacheCost = modelId
+ ? getUsage({
+ modelId,
+ usage: { cacheReads: cacheTokens, input: 0, output: 0 },
+ }).costUSD?.totalUSD
+ : undefined;
+ const cacheCostText = new Intl.NumberFormat("en-US", {
+ currency: "USD",
+ style: "currency",
+ }).format(cacheCost ?? 0);
+
+ return (
+
+ Cache
+
+
+ );
+};
diff --git a/src/components/ai-elements/prompt-input.tsx b/src/components/ai-elements/prompt-input.tsx
new file mode 100644
index 0000000..2f81d6e
--- /dev/null
+++ b/src/components/ai-elements/prompt-input.tsx
@@ -0,0 +1,1391 @@
+"use client";
+
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from "@/components/ui/command";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card";
+import {
+ InputGroup,
+ InputGroupAddon,
+ InputGroupButton,
+ InputGroupTextarea,
+} from "@/components/ui/input-group";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Spinner } from "@/components/ui/spinner";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+import type { ChatStatus, FileUIPart, SourceDocumentUIPart } from "ai";
+import { ArrowUpIcon, ImageIcon, MonitorIcon, PlusIcon, SquareIcon, XIcon } from "lucide-react";
+import { nanoid } from "nanoid";
+import type {
+ ChangeEvent,
+ ChangeEventHandler,
+ ClipboardEventHandler,
+ ComponentProps,
+ FormEvent,
+ FormEventHandler,
+ HTMLAttributes,
+ KeyboardEventHandler,
+ PropsWithChildren,
+ ReactNode,
+ RefObject,
+} from "react";
+import {
+ Children,
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+
+// ============================================================================
+// Helpers
+// ============================================================================
+
+const convertBlobUrlToDataUrl = async (url: string): Promise => {
+ try {
+ const response = await fetch(url);
+ const blob = await response.blob();
+ // FileReader uses callback-based API, wrapping in Promise is necessary
+ // oxlint-disable-next-line eslint-plugin-promise(avoid-new)
+ return new Promise((resolve) => {
+ const reader = new FileReader();
+ // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
+ reader.onloadend = () => resolve(reader.result as string);
+ // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
+ reader.onerror = () => resolve(null);
+ reader.readAsDataURL(blob);
+ });
+ } catch {
+ return null;
+ }
+};
+
+const captureScreenshot = async (): Promise => {
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
+ return null;
+ }
+
+ let stream: MediaStream | null = null;
+ const video = document.createElement("video");
+ video.muted = true;
+ video.playsInline = true;
+
+ try {
+ stream = await navigator.mediaDevices.getDisplayMedia({
+ audio: false,
+ video: true,
+ });
+
+ video.srcObject = stream;
+
+ // Video element uses callback-based API, wrapping in Promise is necessary
+ // oxlint-disable-next-line eslint-plugin-promise(avoid-new)
+ await new Promise((resolve, reject) => {
+ // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
+ video.onloadedmetadata = () => resolve();
+ // oxlint-disable-next-line eslint-plugin-unicorn(prefer-add-event-listener)
+ video.onerror = () => reject(new Error("Failed to load screen stream"));
+ });
+
+ await video.play();
+
+ const width = video.videoWidth;
+ const height = video.videoHeight;
+ if (!width || !height) {
+ return null;
+ }
+
+ const canvas = document.createElement("canvas");
+ canvas.width = width;
+ canvas.height = height;
+ const context = canvas.getContext("2d");
+ if (!context) {
+ return null;
+ }
+
+ context.drawImage(video, 0, 0, width, height);
+ // canvas.toBlob uses callback-based API, wrapping in Promise is necessary
+ // oxlint-disable-next-line eslint-plugin-promise(avoid-new)
+ const blob = await new Promise((resolve) => {
+ canvas.toBlob(resolve, "image/png");
+ });
+ if (!blob) {
+ return null;
+ }
+
+ const timestamp = new Date()
+ .toISOString()
+ .replaceAll(/[:.]/g, "-")
+ .replace("T", "_")
+ .replace("Z", "");
+
+ return new File([blob], `screenshot-${timestamp}.png`, {
+ lastModified: Date.now(),
+ type: "image/png",
+ });
+ } finally {
+ if (stream) {
+ for (const track of stream.getTracks()) {
+ track.stop();
+ }
+ }
+ video.pause();
+ video.srcObject = null;
+ }
+};
+
+// ============================================================================
+// Provider Context & Types
+// ============================================================================
+
+export interface AttachmentsContext {
+ files: (FileUIPart & { id: string })[];
+ add: (files: File[] | FileList) => void;
+ remove: (id: string) => void;
+ clear: () => void;
+ openFileDialog: () => void;
+ fileInputRef: RefObject;
+}
+
+export interface TextInputContext {
+ value: string;
+ setInput: (v: string) => void;
+ clear: () => void;
+}
+
+export interface PromptInputControllerProps {
+ textInput: TextInputContext;
+ attachments: AttachmentsContext;
+ /** INTERNAL: Allows PromptInput to register its file textInput + "open" callback */
+ __registerFileInput: (ref: RefObject, open: () => void) => void;
+}
+
+const PromptInputController = createContext(null);
+const ProviderAttachmentsContext = createContext(null);
+
+export const usePromptInputController = () => {
+ const ctx = useContext(PromptInputController);
+ if (!ctx) {
+ throw new Error(
+ "Wrap your component inside to use usePromptInputController().",
+ );
+ }
+ return ctx;
+};
+
+// Optional variants (do NOT throw). Useful for dual-mode components.
+const useOptionalPromptInputController = () => useContext(PromptInputController);
+
+export const useProviderAttachments = () => {
+ const ctx = useContext(ProviderAttachmentsContext);
+ if (!ctx) {
+ throw new Error(
+ "Wrap your component inside to use useProviderAttachments().",
+ );
+ }
+ return ctx;
+};
+
+const useOptionalProviderAttachments = () => useContext(ProviderAttachmentsContext);
+
+export type PromptInputProviderProps = PropsWithChildren<{
+ initialInput?: string;
+}>;
+
+/**
+ * Optional global provider that lifts PromptInput state outside of PromptInput.
+ * If you don't use it, PromptInput stays fully self-managed.
+ */
+export const PromptInputProvider = ({
+ initialInput: initialTextInput = "",
+ children,
+}: PromptInputProviderProps) => {
+ // ----- textInput state
+ const [textInput, setTextInput] = useState(initialTextInput);
+ const clearInput = useCallback(() => setTextInput(""), []);
+
+ // ----- attachments state (global when wrapped)
+ const [attachmentFiles, setAttachmentFiles] = useState<(FileUIPart & { id: string })[]>([]);
+ const fileInputRef = useRef(null);
+ // oxlint-disable-next-line eslint(no-empty-function)
+ const openRef = useRef<() => void>(() => {});
+
+ const add = useCallback((files: File[] | FileList) => {
+ const incoming = [...files];
+ if (incoming.length === 0) {
+ return;
+ }
+
+ setAttachmentFiles((prev) => [
+ ...prev,
+ ...incoming.map((file) => ({
+ filename: file.name,
+ id: nanoid(),
+ mediaType: file.type,
+ type: "file" as const,
+ url: URL.createObjectURL(file),
+ })),
+ ]);
+ }, []);
+
+ const remove = useCallback((id: string) => {
+ setAttachmentFiles((prev) => {
+ const found = prev.find((f) => f.id === id);
+ if (found?.url) {
+ URL.revokeObjectURL(found.url);
+ }
+ return prev.filter((f) => f.id !== id);
+ });
+ }, []);
+
+ const clear = useCallback(() => {
+ setAttachmentFiles((prev) => {
+ for (const f of prev) {
+ if (f.url) {
+ URL.revokeObjectURL(f.url);
+ }
+ }
+ return [];
+ });
+ }, []);
+
+ // Keep a ref to attachments for cleanup on unmount (avoids stale closure)
+ const attachmentsRef = useRef(attachmentFiles);
+
+ useEffect(() => {
+ attachmentsRef.current = attachmentFiles;
+ }, [attachmentFiles]);
+
+ // Cleanup blob URLs on unmount to prevent memory leaks
+ useEffect(
+ () => () => {
+ for (const f of attachmentsRef.current) {
+ if (f.url) {
+ URL.revokeObjectURL(f.url);
+ }
+ }
+ },
+ [],
+ );
+
+ const openFileDialog = useCallback(() => {
+ openRef.current?.();
+ }, []);
+
+ const attachments = useMemo(
+ () => ({
+ add,
+ clear,
+ fileInputRef,
+ files: attachmentFiles,
+ openFileDialog,
+ remove,
+ }),
+ [attachmentFiles, add, remove, clear, openFileDialog],
+ );
+
+ const __registerFileInput = useCallback(
+ (ref: RefObject, open: () => void) => {
+ fileInputRef.current = ref.current;
+ openRef.current = open;
+ },
+ [],
+ );
+
+ const controller = useMemo(
+ () => ({
+ __registerFileInput,
+ attachments,
+ textInput: {
+ clear: clearInput,
+ setInput: setTextInput,
+ value: textInput,
+ },
+ }),
+ [textInput, clearInput, attachments, __registerFileInput],
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+// ============================================================================
+// Component Context & Hooks
+// ============================================================================
+
+const LocalAttachmentsContext = createContext(null);
+
+export const usePromptInputAttachments = () => {
+ // Prefer local context (inside PromptInput) as it has validation, fall back to provider
+ const provider = useOptionalProviderAttachments();
+ const local = useContext(LocalAttachmentsContext);
+ const context = local ?? provider;
+ if (!context) {
+ throw new Error(
+ "usePromptInputAttachments must be used within a PromptInput or PromptInputProvider",
+ );
+ }
+ return context;
+};
+
+// ============================================================================
+// Referenced Sources (Local to PromptInput)
+// ============================================================================
+
+export interface ReferencedSourcesContext {
+ sources: (SourceDocumentUIPart & { id: string })[];
+ add: (sources: SourceDocumentUIPart[] | SourceDocumentUIPart) => void;
+ remove: (id: string) => void;
+ clear: () => void;
+}
+
+export const LocalReferencedSourcesContext = createContext(null);
+
+export const usePromptInputReferencedSources = () => {
+ const ctx = useContext(LocalReferencedSourcesContext);
+ if (!ctx) {
+ throw new Error(
+ "usePromptInputReferencedSources must be used within a LocalReferencedSourcesContext.Provider",
+ );
+ }
+ return ctx;
+};
+
+export type PromptInputActionAddAttachmentsProps = ComponentProps & {
+ label?: string;
+};
+
+export const PromptInputActionAddAttachments = ({
+ label = "Add photos or files",
+ ...props
+}: PromptInputActionAddAttachmentsProps) => {
+ const attachments = usePromptInputAttachments();
+
+ const handleSelect = useCallback(
+ (e: Event) => {
+ e.preventDefault();
+ attachments.openFileDialog();
+ },
+ [attachments],
+ );
+
+ return (
+
+ {label}
+
+ );
+};
+
+export type PromptInputActionAddScreenshotProps = ComponentProps & {
+ label?: string;
+};
+
+export const PromptInputActionAddScreenshot = ({
+ label = "Take screenshot",
+ onSelect,
+ ...props
+}: PromptInputActionAddScreenshotProps) => {
+ const attachments = usePromptInputAttachments();
+
+ const handleSelect = useCallback(
+ async (event: Event) => {
+ onSelect?.(event);
+ if (event.defaultPrevented) {
+ return;
+ }
+
+ try {
+ const screenshot = await captureScreenshot();
+ if (screenshot) {
+ attachments.add([screenshot]);
+ }
+ } catch (error) {
+ if (
+ error instanceof DOMException &&
+ (error.name === "NotAllowedError" || error.name === "AbortError")
+ ) {
+ return;
+ }
+ throw error;
+ }
+ },
+ [onSelect, attachments],
+ );
+
+ return (
+
+
+ {label}
+
+ );
+};
+
+export interface PromptInputMessage {
+ text: string;
+ files: FileUIPart[];
+}
+
+export type PromptInputProps = Omit, "onSubmit" | "onError"> & {
+ // e.g., "image/*" or leave undefined for any
+ accept?: string;
+ multiple?: boolean;
+ // When true, accepts drops anywhere on document. Default false (opt-in).
+ globalDrop?: boolean;
+ // Render a hidden input with given name and keep it in sync for native form posts. Default false.
+ syncHiddenInput?: boolean;
+ // Minimal constraints
+ maxFiles?: number;
+ // bytes
+ maxFileSize?: number;
+ onError?: (err: { code: "max_files" | "max_file_size" | "accept"; message: string }) => void;
+ onSubmit: (
+ message: PromptInputMessage,
+ event: FormEvent,
+ ) => void | Promise;
+};
+
+export const PromptInput = ({
+ className,
+ accept,
+ multiple,
+ globalDrop,
+ syncHiddenInput,
+ maxFiles,
+ maxFileSize,
+ onError,
+ onSubmit,
+ children,
+ ...props
+}: PromptInputProps) => {
+ // Try to use a provider controller if present
+ const controller = useOptionalPromptInputController();
+ const usingProvider = !!controller;
+
+ // Refs
+ const inputRef = useRef(null);
+ const formRef = useRef(null);
+
+ // ----- Local attachments (only used when no provider)
+ const [items, setItems] = useState<(FileUIPart & { id: string })[]>([]);
+ const files = usingProvider ? controller.attachments.files : items;
+
+ // ----- Local referenced sources (always local to PromptInput)
+ const [referencedSources, setReferencedSources] = useState<
+ (SourceDocumentUIPart & { id: string })[]
+ >([]);
+
+ // Keep a ref to files for cleanup on unmount (avoids stale closure)
+ const filesRef = useRef(files);
+
+ useEffect(() => {
+ filesRef.current = files;
+ }, [files]);
+
+ const openFileDialogLocal = useCallback(() => {
+ inputRef.current?.click();
+ }, []);
+
+ const matchesAccept = useCallback(
+ (f: File) => {
+ if (!accept || accept.trim() === "") {
+ return true;
+ }
+
+ const patterns = accept
+ .split(",")
+ .map((s) => s.trim())
+ .filter(Boolean);
+
+ return patterns.some((pattern) => {
+ if (pattern.endsWith("/*")) {
+ // e.g: image/* -> image/
+ const prefix = pattern.slice(0, -1);
+ return f.type.startsWith(prefix);
+ }
+ return f.type === pattern;
+ });
+ },
+ [accept],
+ );
+
+ const addLocal = useCallback(
+ (fileList: File[] | FileList) => {
+ const incoming = [...fileList];
+ const accepted = incoming.filter((f) => matchesAccept(f));
+ if (incoming.length && accepted.length === 0) {
+ onError?.({
+ code: "accept",
+ message: "No files match the accepted types.",
+ });
+ return;
+ }
+ const withinSize = (f: File) => (maxFileSize ? f.size <= maxFileSize : true);
+ const sized = accepted.filter(withinSize);
+ if (accepted.length > 0 && sized.length === 0) {
+ onError?.({
+ code: "max_file_size",
+ message: "All files exceed the maximum size.",
+ });
+ return;
+ }
+
+ setItems((prev) => {
+ const capacity =
+ typeof maxFiles === "number" ? Math.max(0, maxFiles - prev.length) : undefined;
+ const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized;
+ if (typeof capacity === "number" && sized.length > capacity) {
+ onError?.({
+ code: "max_files",
+ message: "Too many files. Some were not added.",
+ });
+ }
+ const next: (FileUIPart & { id: string })[] = [];
+ for (const file of capped) {
+ next.push({
+ filename: file.name,
+ id: nanoid(),
+ mediaType: file.type,
+ type: "file",
+ url: URL.createObjectURL(file),
+ });
+ }
+ return [...prev, ...next];
+ });
+ },
+ [matchesAccept, maxFiles, maxFileSize, onError],
+ );
+
+ const removeLocal = useCallback(
+ (id: string) =>
+ setItems((prev) => {
+ const found = prev.find((file) => file.id === id);
+ if (found?.url) {
+ URL.revokeObjectURL(found.url);
+ }
+ return prev.filter((file) => file.id !== id);
+ }),
+ [],
+ );
+
+ // Wrapper that validates files before calling provider's add
+ const addWithProviderValidation = useCallback(
+ (fileList: File[] | FileList) => {
+ const incoming = [...fileList];
+ const accepted = incoming.filter((f) => matchesAccept(f));
+ if (incoming.length && accepted.length === 0) {
+ onError?.({
+ code: "accept",
+ message: "No files match the accepted types.",
+ });
+ return;
+ }
+ const withinSize = (f: File) => (maxFileSize ? f.size <= maxFileSize : true);
+ const sized = accepted.filter(withinSize);
+ if (accepted.length > 0 && sized.length === 0) {
+ onError?.({
+ code: "max_file_size",
+ message: "All files exceed the maximum size.",
+ });
+ return;
+ }
+
+ const currentCount = files.length;
+ const capacity =
+ typeof maxFiles === "number" ? Math.max(0, maxFiles - currentCount) : undefined;
+ const capped = typeof capacity === "number" ? sized.slice(0, capacity) : sized;
+ if (typeof capacity === "number" && sized.length > capacity) {
+ onError?.({
+ code: "max_files",
+ message: "Too many files. Some were not added.",
+ });
+ }
+
+ if (capped.length > 0) {
+ controller?.attachments.add(capped);
+ }
+ },
+ [matchesAccept, maxFileSize, maxFiles, onError, files.length, controller],
+ );
+
+ const clearAttachments = useCallback(
+ () =>
+ usingProvider
+ ? controller?.attachments.clear()
+ : setItems((prev) => {
+ for (const file of prev) {
+ if (file.url) {
+ URL.revokeObjectURL(file.url);
+ }
+ }
+ return [];
+ }),
+ [usingProvider, controller],
+ );
+
+ const clearReferencedSources = useCallback(() => setReferencedSources([]), []);
+
+ const add = usingProvider ? addWithProviderValidation : addLocal;
+ const remove = usingProvider ? controller.attachments.remove : removeLocal;
+ const openFileDialog = usingProvider
+ ? controller.attachments.openFileDialog
+ : openFileDialogLocal;
+
+ const clear = useCallback(() => {
+ clearAttachments();
+ clearReferencedSources();
+ }, [clearAttachments, clearReferencedSources]);
+
+ // Let provider know about our hidden file input so external menus can call openFileDialog()
+ useEffect(() => {
+ if (!usingProvider) {
+ return;
+ }
+ controller.__registerFileInput(inputRef, () => inputRef.current?.click());
+ }, [usingProvider, controller]);
+
+ // Note: File input cannot be programmatically set for security reasons
+ // The syncHiddenInput prop is no longer functional
+ useEffect(() => {
+ if (syncHiddenInput && inputRef.current && files.length === 0) {
+ inputRef.current.value = "";
+ }
+ }, [files, syncHiddenInput]);
+
+ // Attach drop handlers on nearest form and document (opt-in)
+ useEffect(() => {
+ const form = formRef.current;
+ if (!form) {
+ return;
+ }
+ if (globalDrop) {
+ // when global drop is on, let the document-level handler own drops
+ return;
+ }
+
+ const onDragOver = (e: DragEvent) => {
+ if (e.dataTransfer?.types?.includes("Files")) {
+ e.preventDefault();
+ }
+ };
+ const onDrop = (e: DragEvent) => {
+ if (e.dataTransfer?.types?.includes("Files")) {
+ e.preventDefault();
+ }
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
+ add(e.dataTransfer.files);
+ }
+ };
+ form.addEventListener("dragover", onDragOver);
+ form.addEventListener("drop", onDrop);
+ return () => {
+ form.removeEventListener("dragover", onDragOver);
+ form.removeEventListener("drop", onDrop);
+ };
+ }, [add, globalDrop]);
+
+ useEffect(() => {
+ if (!globalDrop) {
+ return;
+ }
+
+ const onDragOver = (e: DragEvent) => {
+ if (e.dataTransfer?.types?.includes("Files")) {
+ e.preventDefault();
+ }
+ };
+ const onDrop = (e: DragEvent) => {
+ if (e.dataTransfer?.types?.includes("Files")) {
+ e.preventDefault();
+ }
+ if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
+ add(e.dataTransfer.files);
+ }
+ };
+ document.addEventListener("dragover", onDragOver);
+ document.addEventListener("drop", onDrop);
+ return () => {
+ document.removeEventListener("dragover", onDragOver);
+ document.removeEventListener("drop", onDrop);
+ };
+ }, [add, globalDrop]);
+
+ useEffect(
+ () => () => {
+ if (!usingProvider) {
+ for (const f of filesRef.current) {
+ if (f.url) {
+ URL.revokeObjectURL(f.url);
+ }
+ }
+ }
+ },
+ [usingProvider],
+ );
+
+ const handleChange: ChangeEventHandler = useCallback(
+ (event) => {
+ if (event.currentTarget.files) {
+ add(event.currentTarget.files);
+ }
+ // Reset input value to allow selecting files that were previously removed
+ event.currentTarget.value = "";
+ },
+ [add],
+ );
+
+ const attachmentsCtx = useMemo(
+ () => ({
+ add,
+ clear: clearAttachments,
+ fileInputRef: inputRef,
+ files: files.map((item) => ({ ...item, id: item.id })),
+ openFileDialog,
+ remove,
+ }),
+ [files, add, remove, clearAttachments, openFileDialog],
+ );
+
+ const refsCtx = useMemo(
+ () => ({
+ add: (incoming: SourceDocumentUIPart[] | SourceDocumentUIPart) => {
+ const array = Array.isArray(incoming) ? incoming : [incoming];
+ setReferencedSources((prev) => [...prev, ...array.map((s) => ({ ...s, id: nanoid() }))]);
+ },
+ clear: clearReferencedSources,
+ remove: (id: string) => {
+ setReferencedSources((prev) => prev.filter((s) => s.id !== id));
+ },
+ sources: referencedSources,
+ }),
+ [referencedSources, clearReferencedSources],
+ );
+
+ const handleSubmit: FormEventHandler = useCallback(
+ async (event) => {
+ event.preventDefault();
+
+ const form = event.currentTarget;
+ const text = usingProvider
+ ? controller.textInput.value
+ : (() => {
+ const formData = new FormData(form);
+ return (formData.get("message") as string) || "";
+ })();
+
+ // Reset form immediately after capturing text to avoid race condition
+ // where user input during async blob conversion would be lost
+ if (!usingProvider) {
+ form.reset();
+ }
+
+ try {
+ // Convert blob URLs to data URLs asynchronously
+ const convertedFiles: FileUIPart[] = await Promise.all(
+ files.map(async ({ id: _id, ...item }) => {
+ if (item.url?.startsWith("blob:")) {
+ const dataUrl = await convertBlobUrlToDataUrl(item.url);
+ // If conversion failed, keep the original blob URL
+ return {
+ ...item,
+ url: dataUrl ?? item.url,
+ };
+ }
+ return item;
+ }),
+ );
+
+ const result = onSubmit({ files: convertedFiles, text }, event);
+
+ // Handle both sync and async onSubmit
+ if (result instanceof Promise) {
+ try {
+ await result;
+ clear();
+ if (usingProvider) {
+ controller.textInput.clear();
+ }
+ } catch {
+ // Don't clear on error - user may want to retry
+ }
+ } else {
+ // Sync function completed without throwing, clear inputs
+ clear();
+ if (usingProvider) {
+ controller.textInput.clear();
+ }
+ }
+ } catch {
+ // Don't clear on error - user may want to retry
+ }
+ },
+ [usingProvider, controller, files, onSubmit, clear],
+ );
+
+ // Render with or without local provider
+ const inner = (
+ <>
+
+
+ >
+ );
+
+ const withReferencedSources = (
+
+ {inner}
+
+ );
+
+ // Always provide LocalAttachmentsContext so children get validated add function
+ return (
+
+ {withReferencedSources}
+
+ );
+};
+
+export type PromptInputBodyProps = HTMLAttributes;
+
+export const PromptInputBody = ({ className, ...props }: PromptInputBodyProps) => (
+
+);
+
+export type PromptInputTextareaProps = ComponentProps;
+
+export const PromptInputTextarea = ({
+ onChange,
+ onKeyDown,
+ className,
+ placeholder = "What would you like to know?",
+ ...props
+}: PromptInputTextareaProps) => {
+ const controller = useOptionalPromptInputController();
+ const attachments = usePromptInputAttachments();
+ const [isComposing, setIsComposing] = useState(false);
+
+ const handleKeyDown: KeyboardEventHandler = useCallback(
+ (e) => {
+ // Call the external onKeyDown handler first
+ onKeyDown?.(e);
+
+ // If the external handler prevented default, don't run internal logic
+ if (e.defaultPrevented) {
+ return;
+ }
+
+ if (e.key === "Enter") {
+ if (isComposing || e.nativeEvent.isComposing) {
+ return;
+ }
+ if (e.shiftKey) {
+ return;
+ }
+ e.preventDefault();
+
+ // Check if the submit button is disabled before submitting
+ const { form } = e.currentTarget;
+ const submitButton = form?.querySelector(
+ 'button[type="submit"]',
+ ) as HTMLButtonElement | null;
+ if (submitButton?.disabled) {
+ return;
+ }
+
+ form?.requestSubmit();
+ }
+
+ // Remove last attachment when Backspace is pressed and textarea is empty
+ if (e.key === "Backspace" && e.currentTarget.value === "" && attachments.files.length > 0) {
+ e.preventDefault();
+ const lastAttachment = attachments.files.at(-1);
+ if (lastAttachment) {
+ attachments.remove(lastAttachment.id);
+ }
+ }
+ },
+ [onKeyDown, isComposing, attachments],
+ );
+
+ const handlePaste: ClipboardEventHandler = useCallback(
+ (event) => {
+ const items = event.clipboardData?.items;
+
+ if (!items) {
+ return;
+ }
+
+ const files: File[] = [];
+
+ for (const item of items) {
+ if (item.kind === "file") {
+ const file = item.getAsFile();
+ if (file) {
+ files.push(file);
+ }
+ }
+ }
+
+ if (files.length > 0) {
+ event.preventDefault();
+ attachments.add(files);
+ }
+ },
+ [attachments],
+ );
+
+ const handleCompositionEnd = useCallback(() => setIsComposing(false), []);
+ const handleCompositionStart = useCallback(() => setIsComposing(true), []);
+
+ const controlledProps = controller
+ ? {
+ onChange: (e: ChangeEvent) => {
+ controller.textInput.setInput(e.currentTarget.value);
+ onChange?.(e);
+ },
+ value: controller.textInput.value,
+ }
+ : {
+ onChange,
+ };
+
+ return (
+
+ );
+};
+
+export type PromptInputHeaderProps = Omit, "align">;
+
+export const PromptInputHeader = ({ className, ...props }: PromptInputHeaderProps) => (
+
+);
+
+export type PromptInputFooterProps = Omit, "align">;
+
+export const PromptInputFooter = ({ className, ...props }: PromptInputFooterProps) => (
+
+);
+
+export type PromptInputToolsProps = HTMLAttributes;
+
+export const PromptInputTools = ({ className, ...props }: PromptInputToolsProps) => (
+
+);
+
+export type PromptInputButtonTooltip =
+ | string
+ | {
+ content: ReactNode;
+ shortcut?: string;
+ side?: ComponentProps["side"];
+ };
+
+export type PromptInputButtonProps = ComponentProps & {
+ tooltip?: PromptInputButtonTooltip;
+};
+
+export const PromptInputButton = ({
+ variant = "ghost",
+ className,
+ size,
+ tooltip,
+ ...props
+}: PromptInputButtonProps) => {
+ const newSize = size ?? (Children.count(props.children) > 1 ? "sm" : "icon-sm");
+
+ const button = (
+
+ );
+
+ if (!tooltip) {
+ return button;
+ }
+
+ const tooltipContent = typeof tooltip === "string" ? tooltip : tooltip.content;
+ const shortcut = typeof tooltip === "string" ? undefined : tooltip.shortcut;
+ const side = typeof tooltip === "string" ? "top" : (tooltip.side ?? "top");
+
+ return (
+
+ {button}
+
+ {tooltipContent}
+ {shortcut && {shortcut} }
+
+
+ );
+};
+
+export type PromptInputActionMenuProps = ComponentProps;
+export const PromptInputActionMenu = (props: PromptInputActionMenuProps) => (
+
+);
+
+export type PromptInputActionMenuTriggerProps = PromptInputButtonProps;
+
+export const PromptInputActionMenuTrigger = ({
+ className,
+ children,
+ ...props
+}: PromptInputActionMenuTriggerProps) => (
+
+
+ {children ?? }
+
+
+);
+
+export type PromptInputActionMenuContentProps = ComponentProps;
+export const PromptInputActionMenuContent = ({
+ className,
+ ...props
+}: PromptInputActionMenuContentProps) => (
+
+);
+
+export type PromptInputActionMenuItemProps = ComponentProps;
+export const PromptInputActionMenuItem = ({
+ className,
+ ...props
+}: PromptInputActionMenuItemProps) => (
+
+);
+
+// Note: Actions that perform side-effects (like opening a file dialog)
+// are provided in opt-in modules (e.g., prompt-input-attachments).
+
+export type PromptInputSubmitProps = ComponentProps & {
+ status?: ChatStatus;
+ onStop?: () => void;
+};
+
+export const PromptInputSubmit = ({
+ className,
+ variant = "default",
+ size = "icon-sm",
+ status,
+ onStop,
+ onClick,
+ children,
+ ...props
+}: PromptInputSubmitProps) => {
+ const isGenerating = status === "submitted" || status === "streaming";
+
+ let Icon = ;
+
+ if (status === "submitted") {
+ Icon = ;
+ } else if (status === "streaming") {
+ Icon = ;
+ } else if (status === "error") {
+ Icon = ;
+ }
+
+ const handleClick = useCallback(
+ (e: React.MouseEvent) => {
+ if (isGenerating && onStop) {
+ e.preventDefault();
+ onStop();
+ return;
+ }
+ onClick?.(e);
+ },
+ [isGenerating, onStop, onClick],
+ );
+
+ return (
+
+ {children ?? Icon}
+
+ );
+};
+
+export type PromptInputSelectProps = ComponentProps;
+
+export const PromptInputSelect = (props: PromptInputSelectProps) => ;
+
+export type PromptInputSelectTriggerProps = ComponentProps;
+
+export const PromptInputSelectTrigger = ({
+ className,
+ ...props
+}: PromptInputSelectTriggerProps) => (
+
+);
+
+export type PromptInputSelectContentProps = ComponentProps;
+
+export const PromptInputSelectContent = ({
+ className,
+ ...props
+}: PromptInputSelectContentProps) => (
+
+);
+
+export type PromptInputSelectItemProps = ComponentProps;
+
+export const PromptInputSelectItem = ({ className, ...props }: PromptInputSelectItemProps) => (
+
+);
+
+export type PromptInputSelectValueProps = ComponentProps;
+
+export const PromptInputSelectValue = ({ className, ...props }: PromptInputSelectValueProps) => (
+
+);
+
+export type PromptInputHoverCardProps = ComponentProps;
+
+export const PromptInputHoverCard = ({
+ openDelay = 0,
+ closeDelay = 0,
+ ...props
+}: PromptInputHoverCardProps) => (
+
+);
+
+export type PromptInputHoverCardTriggerProps = ComponentProps;
+
+export const PromptInputHoverCardTrigger = (props: PromptInputHoverCardTriggerProps) => (
+
+);
+
+export type PromptInputHoverCardContentProps = ComponentProps;
+
+export const PromptInputHoverCardContent = ({
+ align = "start",
+ ...props
+}: PromptInputHoverCardContentProps) => (
+
+);
+
+export type PromptInputTabsListProps = HTMLAttributes;
+
+export const PromptInputTabsList = ({ className, ...props }: PromptInputTabsListProps) => (
+
+);
+
+export type PromptInputTabProps = HTMLAttributes;
+
+export const PromptInputTab = ({ className, ...props }: PromptInputTabProps) => (
+
+);
+
+export type PromptInputTabLabelProps = HTMLAttributes;
+
+export const PromptInputTabLabel = ({ className, ...props }: PromptInputTabLabelProps) => (
+ // Content provided via children in props
+ // oxlint-disable-next-line eslint-plugin-jsx-a11y(heading-has-content)
+
+);
+
+export type PromptInputTabBodyProps = HTMLAttributes;
+
+export const PromptInputTabBody = ({ className, ...props }: PromptInputTabBodyProps) => (
+
+);
+
+export type PromptInputTabItemProps = HTMLAttributes;
+
+export const PromptInputTabItem = ({ className, ...props }: PromptInputTabItemProps) => (
+
+);
+
+export type PromptInputCommandProps = ComponentProps;
+
+export const PromptInputCommand = ({ className, ...props }: PromptInputCommandProps) => (
+
+);
+
+export type PromptInputCommandInputProps = ComponentProps;
+
+export const PromptInputCommandInput = ({ className, ...props }: PromptInputCommandInputProps) => (
+
+);
+
+export type PromptInputCommandListProps = ComponentProps;
+
+export const PromptInputCommandList = ({ className, ...props }: PromptInputCommandListProps) => (
+
+);
+
+export type PromptInputCommandEmptyProps = ComponentProps;
+
+export const PromptInputCommandEmpty = ({ className, ...props }: PromptInputCommandEmptyProps) => (
+
+);
+
+export type PromptInputCommandGroupProps = ComponentProps;
+
+export const PromptInputCommandGroup = ({ className, ...props }: PromptInputCommandGroupProps) => (
+
+);
+
+export type PromptInputCommandItemProps = ComponentProps;
+
+export const PromptInputCommandItem = ({ className, ...props }: PromptInputCommandItemProps) => (
+
+);
+
+export type PromptInputCommandSeparatorProps = ComponentProps;
+
+export const PromptInputCommandSeparator = ({
+ className,
+ ...props
+}: PromptInputCommandSeparatorProps) => (
+
+);
diff --git a/src/components/ai-elements/reasoning.tsx b/src/components/ai-elements/reasoning.tsx
new file mode 100644
index 0000000..aede30e
--- /dev/null
+++ b/src/components/ai-elements/reasoning.tsx
@@ -0,0 +1,202 @@
+"use client";
+
+import { Shimmer } from "./shimmer";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
+import { cn } from "@/lib/utils";
+import { useControllableState } from "@radix-ui/react-use-controllable-state";
+import { BrainIcon, ChevronDownIcon } from "lucide-react";
+import type { ComponentProps, ReactNode } from "react";
+import {
+ createContext,
+ memo,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+import { Streamdown } from "streamdown";
+
+interface ReasoningContextValue {
+ isStreaming: boolean;
+ isOpen: boolean;
+ setIsOpen: (open: boolean) => void;
+ duration: number | undefined;
+}
+
+const ReasoningContext = createContext(null);
+
+export const useReasoning = () => {
+ const context = useContext(ReasoningContext);
+ if (!context) {
+ throw new Error("Reasoning components must be used within Reasoning");
+ }
+ return context;
+};
+
+export type ReasoningProps = ComponentProps & {
+ isStreaming?: boolean;
+ open?: boolean;
+ defaultOpen?: boolean;
+ onOpenChange?: (open: boolean) => void;
+ duration?: number;
+};
+
+const AUTO_CLOSE_DELAY = 1000;
+const MS_IN_S = 1000;
+
+export const Reasoning = memo(
+ ({
+ className,
+ isStreaming = false,
+ open,
+ defaultOpen,
+ // oxlint-disable-next-line typescript/unbound-method
+ onOpenChange,
+ duration: durationProp,
+ children,
+ ...props
+ }: ReasoningProps) => {
+ const resolvedDefaultOpen = defaultOpen ?? isStreaming;
+ // Track if defaultOpen was explicitly set to false (to prevent auto-open)
+ const isExplicitlyClosed = defaultOpen === false;
+
+ const [isOpen, setIsOpen] = useControllableState({
+ defaultProp: resolvedDefaultOpen,
+ onChange: onOpenChange,
+ prop: open,
+ });
+ const [duration, setDuration] = useControllableState({
+ defaultProp: undefined,
+ prop: durationProp,
+ });
+
+ const hasEverStreamedRef = useRef(isStreaming);
+ const [hasAutoClosed, setHasAutoClosed] = useState(false);
+ const startTimeRef = useRef(null);
+
+ // Track when streaming starts and compute duration
+ useEffect(() => {
+ if (isStreaming) {
+ hasEverStreamedRef.current = true;
+ if (startTimeRef.current === null) {
+ startTimeRef.current = Date.now();
+ }
+ } else if (startTimeRef.current !== null) {
+ setDuration(Math.ceil((Date.now() - startTimeRef.current) / MS_IN_S));
+ startTimeRef.current = null;
+ }
+ }, [isStreaming, setDuration]);
+
+ // Auto-open when streaming starts (unless explicitly closed)
+ useEffect(() => {
+ if (isStreaming && !isOpen && !isExplicitlyClosed) {
+ setIsOpen(true);
+ }
+ }, [isStreaming, isOpen, setIsOpen, isExplicitlyClosed]);
+
+ // Auto-close when streaming ends (once only, and only if it ever streamed)
+ useEffect(() => {
+ if (hasEverStreamedRef.current && !isStreaming && isOpen && !hasAutoClosed) {
+ const timer = setTimeout(() => {
+ setIsOpen(false);
+ setHasAutoClosed(true);
+ }, AUTO_CLOSE_DELAY);
+
+ return () => clearTimeout(timer);
+ }
+ }, [isStreaming, isOpen, setIsOpen, hasAutoClosed]);
+
+ const handleOpenChange = useCallback(
+ (newOpen: boolean) => {
+ setIsOpen(newOpen);
+ },
+ [setIsOpen],
+ );
+
+ const contextValue = useMemo(
+ () => ({ duration, isOpen, isStreaming, setIsOpen }),
+ [duration, isOpen, isStreaming, setIsOpen],
+ );
+
+ return (
+
+
+ {children}
+
+
+ );
+ },
+);
+
+export type ReasoningTriggerProps = ComponentProps & {
+ getThinkingMessage?: (isStreaming: boolean, duration?: number) => ReactNode;
+};
+
+const defaultGetThinkingMessage = (isStreaming: boolean, duration?: number) => {
+ if (isStreaming || duration === 0) {
+ return Thinking... ;
+ }
+ if (duration === undefined) {
+ return Thought for a few seconds
;
+ }
+ return Thought for {duration} seconds
;
+};
+
+export const ReasoningTrigger = memo(
+ ({
+ className,
+ children,
+ getThinkingMessage = defaultGetThinkingMessage,
+ ...props
+ }: ReasoningTriggerProps) => {
+ const { isStreaming, isOpen, duration } = useReasoning();
+
+ return (
+
+ {children ?? (
+ <>
+
+ {getThinkingMessage(isStreaming, duration)}
+
+ >
+ )}
+
+ );
+ },
+);
+
+export type ReasoningContentProps = ComponentProps & {
+ children: string;
+};
+
+export const ReasoningContent = memo(({ className, children, ...props }: ReasoningContentProps) => (
+
+ {children}
+
+));
+
+Reasoning.displayName = "Reasoning";
+ReasoningTrigger.displayName = "ReasoningTrigger";
+ReasoningContent.displayName = "ReasoningContent";
diff --git a/src/components/docs/demos/generated-command-demo.tsx b/src/components/docs/demos/generated-command-demo.tsx
index 392d510..2653895 100644
--- a/src/components/docs/demos/generated-command-demo.tsx
+++ b/src/components/docs/demos/generated-command-demo.tsx
@@ -8,7 +8,7 @@ const sampleTool: Tool = {
info: {
description: "Transfer data to or from a server",
},
- commands: [{ key: "curl", name: "curl", isDefault: true }],
+ commands: [{ key: "curl", name: "curl" }],
parameters: [
{
key: "url",
diff --git a/src/components/docs/demos/json-output-demo.tsx b/src/components/docs/demos/json-output-demo.tsx
index 93bab03..2a9a003 100644
--- a/src/components/docs/demos/json-output-demo.tsx
+++ b/src/components/docs/demos/json-output-demo.tsx
@@ -7,7 +7,7 @@ const sampleTool: Tool = {
info: {
description: "Transfer data to or from a server",
},
- commands: [{ key: "curl", name: "curl", isDefault: true }],
+ commands: [{ key: "curl", name: "curl" }],
parameters: [
{
key: "url",
diff --git a/src/components/docs/demos/tool-renderer-demo.tsx b/src/components/docs/demos/tool-renderer-demo.tsx
index 52dd232..bb9c74f 100644
--- a/src/components/docs/demos/tool-renderer-demo.tsx
+++ b/src/components/docs/demos/tool-renderer-demo.tsx
@@ -8,7 +8,7 @@ const sampleTool: Tool = {
info: {
description: "Transfer data with URLs",
},
- commands: [{ key: "curl", name: "curl", isDefault: true }],
+ commands: [{ key: "curl", name: "curl" }],
parameters: [
{
key: "url",
diff --git a/src/components/tool-card.tsx b/src/components/tool-card.tsx
index c667899..4fc4567 100644
--- a/src/components/tool-card.tsx
+++ b/src/components/tool-card.tsx
@@ -21,9 +21,6 @@ export function ToolCard({
isLocal?: boolean;
onDelete?: (tool: Partial) => void;
}) {
- const description =
- tool.info?.description ?? (tool as Partial & { description?: string }).description;
-
return (
@@ -103,14 +100,16 @@ export function ToolCard({
className="flex min-h-0 flex-1 flex-col overflow-hidden"
>
- {description ? (
+ {tool.info?.description ? (
- {description}
+ {tool.info?.description}
- {description}
+
+ {tool.info?.description}
+
) : (
No description available
diff --git a/src/components/tool-editor/ai-chat-store.ts b/src/components/tool-editor/ai-chat-store.ts
new file mode 100644
index 0000000..e6d5a9f
--- /dev/null
+++ b/src/components/tool-editor/ai-chat-store.ts
@@ -0,0 +1,423 @@
+import type { ApprovalArtifact } from "../ai-chat/ai-chat-message-mapping";
+import {
+ type ChatWithPreview,
+ createChat,
+ getChats,
+ getPersistableMessages,
+ loadChat,
+ truncateMessages,
+ upsertMessage,
+} from "../ai-chat/ai-chat-persistence";
+import { MODEL_GROUPS, type ReasoningEffort } from "../ai-chat/model-picker";
+import type { Tool } from "@/components/commandly/types/flat";
+import type { LanguageModelUsage, UIMessage } from "ai";
+import { isToolUIPart, getToolName, type InferUITools, type Tool as AISDKTool } from "ai";
+
+type AgentUIMessage = UIMessage>>;
+
+export interface ChatStoreSnapshot {
+ chatId: string;
+ input: string;
+ model: string;
+ reasoningEffort: ReasoningEffort | null;
+ schema: object | null;
+ usage: LanguageModelUsage | null;
+ recentSessions: ChatWithPreview[];
+ approvalArtifacts: Record;
+ pendingPreview: Tool | null;
+ lastMessages: AgentUIMessage[];
+}
+
+export class ChatStore {
+ private toolName: string;
+ private currentTool: Tool;
+ private chatId: string;
+ private input: string;
+ private model: string;
+ private reasoningEffort: ReasoningEffort | null;
+ private schema: object | null = null;
+ private usage: LanguageModelUsage | null = null;
+ private recentSessions: ChatWithPreview[] = [];
+ private approvalArtifacts: Record = {};
+ private pendingPreview: Tool | null = null;
+ private lastMessages: AgentUIMessage[] = [];
+
+ private persistedIds: string[] = [];
+ private persistTimer: ReturnType | null = null;
+ private prevGenerating = false;
+ private prevStreamingCleared = true;
+
+ private listeners = new Set<() => void>();
+ private snapshot: ChatStoreSnapshot;
+
+ constructor(toolName: string, currentTool: Tool) {
+ this.toolName = toolName;
+ this.currentTool = currentTool;
+ this.chatId = crypto.randomUUID();
+ this.input = "";
+ this.model = localStorage.getItem("ai-model") ?? MODEL_GROUPS[0].models[0].value;
+ this.reasoningEffort = localStorage.getItem("ai-reasoning-effort") as ReasoningEffort | null;
+ this.snapshot = this.buildSnapshot();
+ this.fetchSchema();
+ this.refreshChats();
+ }
+
+ private buildSnapshot(): ChatStoreSnapshot {
+ return {
+ chatId: this.chatId,
+ input: this.input,
+ model: this.model,
+ reasoningEffort: this.reasoningEffort,
+ schema: this.schema,
+ usage: this.usage,
+ recentSessions: this.recentSessions,
+ approvalArtifacts: this.approvalArtifacts,
+ pendingPreview: this.pendingPreview,
+ lastMessages: this.lastMessages,
+ };
+ }
+
+ private notify(): void {
+ this.snapshot = this.buildSnapshot();
+ for (const listener of this.listeners) {
+ listener();
+ }
+ }
+
+ subscribe = (listener: () => void): (() => void) => {
+ this.listeners.add(listener);
+ return () => {
+ this.listeners.delete(listener);
+ };
+ };
+
+ getSnapshot = (): ChatStoreSnapshot => {
+ return this.snapshot;
+ };
+
+ updateTool(tool: Tool): void {
+ this.currentTool = tool;
+ if (tool.name !== this.toolName) {
+ this.toolName = tool.name;
+ this.refreshChats();
+ }
+ }
+
+ getChatId(): string {
+ return this.chatId;
+ }
+
+ getLastMessages(): AgentUIMessage[] {
+ return this.lastMessages;
+ }
+
+ getPendingPreview(): Tool | null {
+ return this.pendingPreview;
+ }
+
+ getCurrentTool(): Tool {
+ return this.currentTool;
+ }
+
+ setPendingPreview(tool: Tool | null): void {
+ this.pendingPreview = tool;
+ }
+
+ setInput(value: string): void {
+ this.input = value;
+ this.notify();
+ }
+
+ setModel(value: string): void {
+ this.model = value;
+ localStorage.setItem("ai-model", value);
+ const isReasoning =
+ MODEL_GROUPS.flatMap((g) => g.models).find((m) => m.value === value)?.reasoning === true;
+ if (!isReasoning) {
+ this.reasoningEffort = null;
+ localStorage.removeItem("ai-reasoning-effort");
+ }
+ this.notify();
+ }
+
+ setReasoningEffort(effort: ReasoningEffort | null): void {
+ this.reasoningEffort = effort;
+ if (effort === null) {
+ localStorage.removeItem("ai-reasoning-effort");
+ } else {
+ localStorage.setItem("ai-reasoning-effort", effort);
+ }
+ this.notify();
+ }
+
+ setUsage(usage: LanguageModelUsage | null): void {
+ this.usage = usage;
+ this.notify();
+ }
+
+ private fetchSchema(): void {
+ fetch("/specification/flat.json")
+ .then((r) => r.json())
+ .then((data: object) => {
+ this.schema = data;
+ this.notify();
+ })
+ .catch(() => {});
+ }
+
+ refreshChats(): void {
+ getChats(this.toolName)
+ .then((sessions) => {
+ this.recentSessions = sessions;
+ this.notify();
+ })
+ .catch(console.error);
+ }
+
+ syncApprovals(rawMessages: UIMessage[]): void {
+ let changed = false;
+ const next = { ...this.approvalArtifacts };
+
+ for (const message of rawMessages) {
+ for (const part of message.parts) {
+ if (!isToolUIPart(part) || getToolName(part) !== "applyToolDefinition") {
+ continue;
+ }
+ const approvalId = part.approval?.id;
+ if (!approvalId || next[approvalId]) {
+ continue;
+ }
+ next[approvalId] = {
+ approvalId,
+ previewTool: structuredClone(this.pendingPreview ?? this.currentTool),
+ originalTool: structuredClone(this.currentTool),
+ summary:
+ typeof part.input === "object" &&
+ part.input &&
+ "summary" in part.input &&
+ typeof part.input.summary === "string"
+ ? part.input.summary
+ : "Apply AI changes",
+ };
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ this.approvalArtifacts = next;
+ this.notify();
+ }
+ }
+
+ syncPersistence(rawMessages: UIMessage[], isStreaming: boolean): void {
+ const persistableMessages = getPersistableMessages(rawMessages);
+ if (persistableMessages.length === 0) return;
+
+ const currentChatId = this.chatId;
+ const toolName = this.toolName;
+ const snapshotIds = this.persistedIds.slice();
+
+ if (this.persistTimer) {
+ clearTimeout(this.persistTimer);
+ }
+
+ const delay = isStreaming ? 2000 : 0;
+
+ this.persistTimer = setTimeout(() => {
+ this.persistTimer = null;
+
+ let divergeAt = snapshotIds.length;
+ for (let i = 0; i < Math.min(snapshotIds.length, persistableMessages.length); i++) {
+ if (snapshotIds[i] !== persistableMessages[i].id) {
+ divergeAt = i;
+ break;
+ }
+ }
+ if (persistableMessages.length < divergeAt) {
+ divergeAt = persistableMessages.length;
+ }
+
+ const upsertFrom =
+ snapshotIds.length > 0 && divergeAt === snapshotIds.length && divergeAt > 0
+ ? divergeAt - 1
+ : divergeAt;
+ const needsTruncate = divergeAt < snapshotIds.length;
+ const hasUpdates = upsertFrom < persistableMessages.length;
+
+ if (!hasUpdates && !needsTruncate) return;
+
+ const isNewChat = snapshotIds.length === 0;
+ const ensureChat = isNewChat ? createChat(currentChatId, toolName) : Promise.resolve();
+
+ ensureChat
+ .then(async () => {
+ if (needsTruncate) {
+ await truncateMessages(currentChatId, divergeAt);
+ }
+ for (let i = upsertFrom; i < persistableMessages.length; i++) {
+ await upsertMessage(currentChatId, persistableMessages[i], i);
+ }
+ this.persistedIds = persistableMessages.map((m) => m.id);
+ })
+ .then(() => {
+ if (isNewChat) this.refreshChats();
+ })
+ .catch(console.error);
+ }, delay);
+ }
+
+ syncGeneratingState(
+ rawMessages: UIMessage[],
+ isStreaming: boolean,
+ onGeneratingChange?: (isGenerating: boolean) => void,
+ onStreamingTool?: (tool: Tool | null) => void,
+ ): void {
+ const hasPendingApproval = rawMessages.some((message) =>
+ message.parts.some(
+ (part) =>
+ isToolUIPart(part) &&
+ getToolName(part) === "applyToolDefinition" &&
+ part.state === "approval-requested",
+ ),
+ );
+
+ const isApplying = rawMessages.some((message) =>
+ message.parts.some(
+ (part) =>
+ isToolUIPart(part) &&
+ getToolName(part) === "applyToolDefinition" &&
+ (part.state === "input-available" || part.state === "approval-responded"),
+ ),
+ );
+
+ const generating = isStreaming && isApplying;
+ if (generating !== this.prevGenerating) {
+ this.prevGenerating = generating;
+ onGeneratingChange?.(generating);
+ }
+
+ const shouldClear = !hasPendingApproval && !isApplying && !isStreaming;
+ if (shouldClear && !this.prevStreamingCleared) {
+ this.prevStreamingCleared = true;
+ this.pendingPreview = null;
+ onStreamingTool?.(null);
+ } else if (!shouldClear) {
+ this.prevStreamingCleared = false;
+ }
+ }
+
+ prepareForSend(): void {
+ this.pendingPreview = null;
+ this.approvalArtifacts = {};
+ this.notify();
+ }
+
+ prepareForResend(onStreamingTool?: (tool: Tool | null) => void): void {
+ this.pendingPreview = null;
+ this.approvalArtifacts = {};
+ onStreamingTool?.(null);
+ this.notify();
+ }
+
+ clearInput(): void {
+ this.input = "";
+ this.notify();
+ }
+
+ reset(
+ onStreamingTool?: (tool: Tool | null) => void,
+ onGeneratingChange?: (isGenerating: boolean) => void,
+ ): void {
+ if (this.persistTimer) {
+ clearTimeout(this.persistTimer);
+ this.persistTimer = null;
+ }
+ this.lastMessages = [];
+ this.persistedIds = [];
+ this.prevGenerating = false;
+ this.prevStreamingCleared = true;
+ this.chatId = crypto.randomUUID();
+ this.input = "";
+ this.usage = null;
+ this.approvalArtifacts = {};
+ this.pendingPreview = null;
+ onStreamingTool?.(null);
+ onGeneratingChange?.(false);
+ this.notify();
+ }
+
+ flushAndReset(
+ rawMessages: UIMessage[],
+ onStreamingTool?: (tool: Tool | null) => void,
+ onGeneratingChange?: (isGenerating: boolean) => void,
+ ): void {
+ if (this.persistTimer) {
+ clearTimeout(this.persistTimer);
+ this.persistTimer = null;
+ }
+
+ const persistableMessages = getPersistableMessages(rawMessages);
+ if (persistableMessages.length === 0) {
+ this.reset(onStreamingTool, onGeneratingChange);
+ return;
+ }
+
+ const currentChatId = this.chatId;
+ const toolName = this.toolName;
+ const snapshotIds = this.persistedIds.slice();
+
+ let divergeAt = snapshotIds.length;
+ for (let i = 0; i < Math.min(snapshotIds.length, persistableMessages.length); i++) {
+ if (snapshotIds[i] !== persistableMessages[i].id) {
+ divergeAt = i;
+ break;
+ }
+ }
+ if (persistableMessages.length < divergeAt) {
+ divergeAt = persistableMessages.length;
+ }
+
+ const upsertFrom =
+ snapshotIds.length > 0 && divergeAt === snapshotIds.length && divergeAt > 0
+ ? divergeAt - 1
+ : divergeAt;
+ const needsTruncate = divergeAt < snapshotIds.length;
+
+ const ensureChat =
+ snapshotIds.length === 0 ? createChat(currentChatId, toolName) : Promise.resolve();
+
+ ensureChat
+ .then(async () => {
+ if (needsTruncate) {
+ await truncateMessages(currentChatId, divergeAt);
+ }
+ for (let i = upsertFrom; i < persistableMessages.length; i++) {
+ await upsertMessage(currentChatId, persistableMessages[i], i);
+ }
+ })
+ .then(() => this.refreshChats())
+ .catch(console.error)
+ .finally(() => this.reset(onStreamingTool, onGeneratingChange));
+ }
+
+ loadSession(
+ session: ChatWithPreview,
+ onStreamingTool?: (tool: Tool | null) => void,
+ onGeneratingChange?: (isGenerating: boolean) => void,
+ ): void {
+ loadChat(session.id)
+ .then((messages) => {
+ this.lastMessages = messages;
+ this.persistedIds = messages.map((m) => m.id);
+ this.chatId = session.id;
+ this.usage = null;
+ this.input = "";
+ this.approvalArtifacts = {};
+ this.pendingPreview = null;
+ onStreamingTool?.(null);
+ onGeneratingChange?.(false);
+ this.notify();
+ })
+ .catch(console.error);
+ }
+}
diff --git a/src/components/tool-editor/ai-chat.tsx b/src/components/tool-editor/ai-chat.tsx
index 693b504..ae3cbb5 100644
--- a/src/components/tool-editor/ai-chat.tsx
+++ b/src/components/tool-editor/ai-chat.tsx
@@ -1,5 +1,49 @@
+import {
+ type ChatMessage,
+ countCompletedToolCalls,
+ findPendingApproval,
+ toChatMessage,
+} from "../ai-chat/ai-chat-message-mapping";
+import { type ChatWithPreview } from "../ai-chat/ai-chat-persistence";
+import { ApiKeySettings } from "../ai-chat/api-key-settings";
+import { DiffView } from "../ai-chat/diff-view";
+import { ExtractedContent, type ExtractResult } from "../ai-chat/extracted-content";
+import {
+ ModelPicker,
+ ReasoningEffortPicker,
+ getProviderOptions,
+ providerForModel,
+ MODEL_GROUPS,
+} from "../ai-chat/model-picker";
+import { WebSearchResults, type WebSearchResult } from "../ai-chat/web-search-results";
+import { ChatStore } from "./ai-chat-store";
import { generatePrompt } from "./prompt";
import { useToolBuilder } from "./tool-editor.context";
+import {
+ createApplyToolDefinitionTool,
+ createEditTool,
+ createReadTool,
+ createTavilyExtractTool,
+ createTavilySearchTool,
+} from "./tools";
+import {
+ ChainOfThought,
+ ChainOfThoughtContent,
+ ChainOfThoughtHeader,
+ ChainOfThoughtStep,
+} from "@/components/ai-elements/chain-of-thought";
+import {
+ Context,
+ ContextCacheUsage,
+ ContextContent,
+ ContextContentBody,
+ ContextContentFooter,
+ ContextContentHeader,
+ ContextInputUsage,
+ ContextOutputUsage,
+ ContextReasoningUsage,
+ ContextTrigger,
+} from "@/components/ai-elements/context";
import {
Conversation,
ConversationContent,
@@ -13,23 +57,22 @@ import {
MessageContent,
MessageResponse,
} from "@/components/ai-elements/message";
+import {
+ PromptInput,
+ PromptInputBody,
+ PromptInputFooter,
+ PromptInputHeader,
+ PromptInputSubmit,
+ PromptInputTextarea,
+ PromptInputTools,
+} from "@/components/ai-elements/prompt-input";
+import { Reasoning, ReasoningContent, ReasoningTrigger } from "@/components/ai-elements/reasoning";
import { Shimmer } from "@/components/ai-elements/shimmer";
import { Suggestion, Suggestions } from "@/components/ai-elements/suggestion";
-import {
- Tool as ToolCard,
- ToolContent,
- ToolHeader,
- ToolInput,
- ToolOutput,
-} from "@/components/ai-elements/tool";
+import { Tool as ToolCard, ToolContent, ToolHeader } from "@/components/ai-elements/tool";
import { Tool } from "@/components/commandly/types/flat";
-import { exportToStructuredJSON } from "@/components/commandly/utils/flat";
import { Button } from "@/components/ui/button";
-import { Checkbox } from "@/components/ui/checkbox";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
-import { Separator } from "@/components/ui/separator";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Textarea } from "@/components/ui/textarea";
import { useAIKeys, type AIProvider } from "@/lib/ai-keys";
import { cn, replaceKey } from "@/lib/utils";
@@ -38,44 +81,34 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { createGroq } from "@ai-sdk/groq";
import { createMistral } from "@ai-sdk/mistral";
import { createOpenAI } from "@ai-sdk/openai";
+import { Chat, useChat } from "@ai-sdk/react";
import { createXai } from "@ai-sdk/xai";
-import { stepCountIs, streamText, tool } from "ai";
+import {
+ DirectChatTransport,
+ ToolLoopAgent,
+ lastAssistantMessageIsCompleteWithApprovalResponses,
+ type InferUITools,
+ type LanguageModelUsage,
+ type Tool as AISDKTool,
+ type UIMessage,
+} from "ai";
import {
CheckIcon,
- ChevronsUpDownIcon,
+ ChevronRightIcon,
CopyIcon,
- GlobeIcon,
+ FlagIcon,
+ HashIcon,
+ LinkIcon,
PencilIcon,
RefreshCcwIcon,
- SendIcon,
- Settings2Icon,
+ SearchIcon,
SparklesIcon,
- SquareIcon,
SquarePenIcon,
+ TerminalIcon,
XIcon,
} from "lucide-react";
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useMemo, useState, useSyncExternalStore } from "react";
import { toast } from "sonner";
-import { z } from "zod";
-
-interface ToolCallEntry {
- toolCallId: string;
- toolName: string;
- title: string;
- input: Record;
- state: "input-available" | "output-available" | "output-error";
- output?: unknown;
- errorText?: string;
-}
-
-interface ChatMessage {
- role: "user" | "assistant";
- content: string;
- toolApplied?: boolean;
- toolCalls?: ToolCallEntry[];
- isEditing?: boolean;
- editingContent?: string;
-}
const PROMPT_PILLS = [
{
@@ -92,109 +125,31 @@ const PROMPT_PILLS = [
},
];
-const MODEL_GROUPS: {
- provider: AIProvider;
- label: string;
- logoUrl: string;
- keyPlaceholder: string;
- models: { value: string; label: string }[];
-}[] = [
- {
- provider: "openai",
- label: "OpenAI",
- logoUrl: "https://www.google.com/s2/favicons?domain=openai.com&sz=32",
- keyPlaceholder: "sk-...",
- models: [
- { value: "gpt-5-2025-08-07", label: "GPT-5" },
- { value: "gpt-5.4-2026-03-05", label: "GPT-5.4" },
- { value: "gpt-5-mini-2025-08-07", label: "GPT-5 mini" },
- { value: "gpt-5-nano-2025-08-07", label: "GPT-5 nano" },
- { value: "gpt-5-codex", label: "GPT-5 Codex" },
- { value: "gpt-5.3-codex", label: "GPT-5.3 Codex" },
- { value: "gpt-5.2-codex", label: "GPT-5.2 Codex" },
- { value: "gpt-5.1-codex", label: "GPT-5.1 Codex" },
- { value: "gpt-5.1-codex-mini", label: "GPT-5.1 Codex mini" },
- { value: "o4-mini-2025-04-16", label: "o4-mini" },
- { value: "o3-2025-04-16", label: "o3" },
- { value: "gpt-4.1-2025-04-14", label: "GPT-4.1" },
- { value: "gpt-4.1-mini-2025-04-14", label: "GPT-4.1 mini" },
- { value: "gpt-4.1-nano-2025-04-14", label: "GPT-4.1 nano" },
- ],
- },
- {
- provider: "anthropic",
- label: "Anthropic",
- logoUrl: "https://www.google.com/s2/favicons?domain=anthropic.com&sz=32",
- keyPlaceholder: "sk-ant-...",
- models: [
- { value: "claude-opus-4-6", label: "Claude Opus 4.6" },
- { value: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
- { value: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
- { value: "claude-opus-4-5", label: "Claude Opus 4.5" },
- { value: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" },
- ],
- },
- {
- provider: "google",
- label: "Google",
- logoUrl: "https://www.google.com/s2/favicons?domain=google.com&sz=32",
- keyPlaceholder: "AIza...",
- models: [
- { value: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro" },
- { value: "gemini-3-flash-preview", label: "Gemini 3 Flash" },
- { value: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
- { value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
- { value: "gemini-2.5-flash-lite", label: "Gemini 2.5 Flash Lite" },
- ],
- },
- {
- provider: "groq",
- label: "Groq",
- logoUrl: "https://www.google.com/s2/favicons?domain=groq.com&sz=32",
- keyPlaceholder: "gsk_...",
- models: [
- { value: "meta-llama/llama-4-maverick-17b-128e-instruct", label: "Llama 4 Maverick" },
- { value: "meta-llama/llama-4-scout-17b-16e-instruct", label: "Llama 4 Scout" },
- { value: "qwen/qwen3-32b", label: "Qwen 3 32B" },
- { value: "llama-3.3-70b-versatile", label: "Llama 3.3 70B" },
- { value: "llama-3.1-8b-instant", label: "Llama 3.1 8B" },
- ],
- },
- {
- provider: "mistral",
- label: "Mistral",
- logoUrl: "https://www.google.com/s2/favicons?domain=mistral.ai&sz=32",
- keyPlaceholder: "...",
- models: [
- { value: "mistral-large-latest", label: "Mistral Large" },
- { value: "magistral-medium-latest", label: "Magistral Medium" },
- { value: "codestral-latest", label: "Codestral" },
- { value: "mistral-medium-latest", label: "Mistral Medium" },
- { value: "mistral-small-latest", label: "Mistral Small" },
- ],
- },
- {
- provider: "xai",
- label: "xAI",
- logoUrl: "https://www.google.com/s2/favicons?domain=x.ai&sz=32",
- keyPlaceholder: "xai-...",
- models: [
- { value: "grok-4-0709", label: "Grok 4" },
- { value: "grok-3", label: "Grok 3" },
- { value: "grok-3-mini", label: "Grok 3 mini" },
- ],
- },
-];
-
-function providerForModel(model: string): AIProvider {
- if (model.startsWith("claude")) return "anthropic";
- if (model.startsWith("gemini")) return "google";
- if (model.startsWith("llama") || model.startsWith("meta-llama") || model.startsWith("qwen"))
- return "groq";
- if (model.startsWith("mistral") || model.startsWith("codestral") || model.startsWith("magistral"))
- return "mistral";
- if (model.startsWith("grok")) return "xai";
- return "openai";
+const MODEL_MAX_TOKENS: Record = {
+ "claude-opus-4-5": 200000,
+ "claude-sonnet-4-5": 200000,
+ "claude-haiku-3-5": 200000,
+ "gpt-4o": 128000,
+ "gpt-4o-mini": 128000,
+ o1: 200000,
+ o3: 200000,
+ "o4-mini": 200000,
+ "gemini-2.0-flash": 1048576,
+ "gemini-2.5-pro": 1048576,
+ "grok-3": 131072,
+ "llama-3.3-70b-versatile": 128000,
+};
+const DEFAULT_MAX_TOKENS = 128_000;
+
+function formatRelativeTime(ts: number): string {
+ const diff = Date.now() - ts;
+ const mins = Math.floor(diff / 60_000);
+ if (mins < 1) return "Just now";
+ if (mins < 60) return `${mins}m ago`;
+ const hours = Math.floor(mins / 60);
+ if (hours < 24) return `${hours}h ago`;
+ const days = Math.floor(hours / 24);
+ return `${days}d ago`;
}
function createModelInstance(provider: AIProvider, key: string, model: string) {
@@ -214,71 +169,50 @@ function createModelInstance(provider: AIProvider, key: string, model: string) {
}
}
-function tryParsePartialTool(json: string): Tool | null {
- if (!json.trim()) return null;
- try {
- const parsed = JSON.parse(json) as Tool;
- if (parsed?.name && Array.isArray(parsed?.commands) && Array.isArray(parsed?.parameters))
- return parsed;
- } catch {
- /* continue */
- }
-
- const lines = json.split("\n");
- let candidate = lines.slice(0, -1).join("\n").trimEnd().replace(/,\s*$/, "");
- if (!candidate) return null;
-
- const stack: string[] = [];
- let inStr = false;
- let escaped = false;
- for (const ch of candidate) {
- if (escaped) {
- escaped = false;
- continue;
- }
- if (ch === "\\" && inStr) {
- escaped = true;
- continue;
- }
- if (ch === '"') {
- inStr = !inStr;
- continue;
- }
- if (inStr) continue;
- if (ch === "{") stack.push("}");
- else if (ch === "[") stack.push("]");
- else if (ch === "}" || ch === "]") stack.pop();
- }
- candidate += stack.reverse().join("");
-
- try {
- const parsed = JSON.parse(candidate) as Tool;
- if (parsed?.name && Array.isArray(parsed?.commands) && Array.isArray(parsed?.parameters))
- return parsed;
- } catch {
- /* ignore */
- }
-
- return null;
+type ChatProviderOptions = Record<
+ string,
+ Record<
+ string,
+ string | number | boolean | null | Record
+ >
+>;
+type AgentUIMessage = UIMessage>>;
+
+function createToolLoopAgent({
+ model,
+ instructions,
+ tools,
+ providerOptions,
+ onFinish,
+}: {
+ model: ReturnType;
+ instructions: string;
+ tools: Record;
+ providerOptions?: ChatProviderOptions;
+ onFinish: ({ usage }: { usage: LanguageModelUsage }) => void;
+}) {
+ return new ToolLoopAgent({
+ model,
+ instructions,
+ tools,
+ providerOptions,
+ onFinish,
+ });
}
function useAIChat(
currentTool: Tool,
onApply: (tool: Tool) => void,
- context: { selectedCommandName?: string; selectedParameterName?: string },
onStreamingTool?: (tool: Tool | null) => void,
onGeneratingChange?: (isGenerating: boolean) => void,
) {
- const [messages, setMessages] = useState([]);
- const [input, setInput] = useState("");
- const [isStreaming, setIsStreaming] = useState(false);
- const [model, setModel] = useState(MODEL_GROUPS[0].models[0].value);
- const schemaRef = useRef(null);
- const abortControllerRef = useRef(null);
- const toolSnapshotRef = useRef(null);
- const isToolAppliedRef = useRef(false);
+ const { contextSelection } = useToolBuilder();
+ const [store] = useState(() => new ChatStore(currentTool.name, currentTool));
+ const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot);
+
+ store.updateTool(currentTool);
- const provider = providerForModel(model);
+ const provider = providerForModel(snapshot.model);
const openAIKeys = useAIKeys("openai");
const anthropicKeys = useAIKeys("anthropic");
const googleKeys = useAIKeys("google");
@@ -297,287 +231,241 @@ function useAIChat(
};
const currentKeys = allProviderKeys[provider as Exclude];
- useEffect(() => {
- fetch("/specification/flat.json")
- .then((r) => r.json())
- .then((s) => {
- schemaRef.current = s;
- })
- .catch(() => {});
- }, []);
-
- const runStream = useCallback(
- async (userText: string, history: ChatMessage[]) => {
- abortControllerRef.current = new AbortController();
- toolSnapshotRef.current = currentTool;
- isToolAppliedRef.current = false;
-
- try {
- const toolJson = JSON.stringify(exportToStructuredJSON(currentTool), null, 2);
- const schema = schemaRef.current ? JSON.stringify(schemaRef.current, null, 2) : "{}";
- const systemPrompt = generatePrompt(schema, {
- currentToolJson: toolJson,
- context: {
- selectedCommand: context.selectedCommandName,
- selectedParameter: context.selectedParameterName,
- },
- });
- const aiModel = createModelInstance(provider, currentKeys.key, model);
- const userMessage: ChatMessage = { role: "user", content: userText };
-
- const editToolDef = tool({
- description: "Apply the updated CLI tool definition to the editor.",
- inputSchema: z.object({
- name: z.string(),
- displayName: z.string().optional(),
- description: z.string().optional(),
- version: z.string().optional(),
- url: z.string().optional(),
- category: z.string().optional(),
- tags: z.array(z.string()).optional(),
- commands: z.array(z.any()),
- parameters: z.array(z.any()),
- }),
- execute: async (input) => {
- try {
- isToolAppliedRef.current = true;
- onApply(replaceKey(input as unknown as Tool) as Tool);
- return { success: true };
- } catch {
- return { success: false };
- }
- },
- });
-
- const webSearchTool =
- tavilyKeys.isSaved && tavilyKeys.key
- ? tool({
- description:
- "Search the web for CLI tool documentation, help text, or related information.",
- inputSchema: z.object({ query: z.string().describe("The search query") }),
- execute: async ({ query }: { query: string }) => {
- const resp = await fetch("https://api.tavily.com/search", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- api_key: tavilyKeys.key,
- query,
- search_depth: "basic",
- max_results: 5,
- }),
- });
- if (!resp.ok) throw new Error("Web search failed");
- const data = (await resp.json()) as {
- results?: { title: string; url: string; content: string }[];
- };
- return (
- data.results?.map((r) => ({
- title: r.title,
- url: r.url,
- content: r.content,
- })) ?? []
- );
- },
- })
- : undefined;
-
- const { fullStream } = streamText({
- model: aiModel,
- system: systemPrompt,
- messages: [...history, userMessage].map((m) => ({ role: m.role, content: m.content })),
- tools: {
- editTool: editToolDef,
- ...(webSearchTool ? { webSearch: webSearchTool } : {}),
- },
- stopWhen: stepCountIs(5),
- abortSignal: abortControllerRef.current.signal,
- });
-
- let fullText = "";
- let editToolCallId: string | null = null;
- let editToolBuffer = "";
+ const providerOptions = useMemo(
+ function computeProviderOptions() {
+ return (
+ snapshot.reasoningEffort &&
+ MODEL_GROUPS.flatMap((g) => g.models).find((m) => m.value === snapshot.model)?.reasoning ===
+ true
+ ? getProviderOptions(provider, snapshot.model, snapshot.reasoningEffort)
+ : undefined
+ ) as ChatProviderOptions | undefined;
+ },
+ [provider, snapshot.model, snapshot.reasoningEffort],
+ );
- for await (const part of fullStream) {
- if (part.type === "text-delta") {
- fullText += part.text;
- setMessages((prev) => {
- const updated = [...prev];
- updated[updated.length - 1] = {
- role: "assistant",
- content: fullText,
- toolCalls: updated[updated.length - 1].toolCalls,
- toolApplied: updated[updated.length - 1].toolApplied,
- };
- return updated;
- });
- } else if (part.type === "tool-input-start" && part.toolName === "editTool") {
- editToolCallId = part.id;
- onGeneratingChange?.(true);
- setMessages((prev) => {
- const updated = [...prev];
- const last = updated[updated.length - 1];
- const entry: ToolCallEntry = {
- toolCallId: part.id,
- toolName: "editTool",
- title: "Applying changes",
- input: {},
- state: "input-available",
- };
- updated[updated.length - 1] = {
- ...last,
- toolCalls: [...(last.toolCalls ?? []), entry],
- };
- return updated;
- });
- } else if (
- part.type === "tool-input-delta" &&
- editToolCallId &&
- part.id === editToolCallId
- ) {
- editToolBuffer += part.delta;
- const partial = tryParsePartialTool(editToolBuffer);
- if (partial) onStreamingTool?.(partial);
- } else if (part.type === "tool-call" && part.toolName === "webSearch") {
- const query = (part.input as { query: string }).query;
- setMessages((prev) => {
- const updated = [...prev];
- const last = updated[updated.length - 1];
- const entry: ToolCallEntry = {
- toolCallId: part.toolCallId,
- toolName: "webSearch",
- title: "Web Search",
- input: { query },
- state: "input-available",
- };
- updated[updated.length - 1] = {
- ...last,
- toolCalls: [...(last.toolCalls ?? []), entry],
- };
- return updated;
- });
- } else if (part.type === "tool-result" && part.toolName === "webSearch") {
- setMessages((prev) => {
- const updated = [...prev];
- const last = updated[updated.length - 1];
- const toolCalls = (last.toolCalls ?? []).map((tc) =>
- tc.toolCallId === part.toolCallId
- ? { ...tc, state: "output-available" as const, output: part.output }
- : tc,
- );
- updated[updated.length - 1] = { ...last, toolCalls };
- return updated;
- });
- } else if (part.type === "tool-result" && part.toolName === "editTool") {
- onGeneratingChange?.(false);
- onStreamingTool?.(null);
- setMessages((prev) => {
- const updated = [...prev];
- const last = updated[updated.length - 1];
- const toolCalls = (last.toolCalls ?? []).map((tc) =>
- tc.toolCallId === editToolCallId
- ? { ...tc, state: "output-available" as const, output: part.output }
- : tc,
- );
- updated[updated.length - 1] = { ...last, toolCalls, toolApplied: true };
- return updated;
- });
- }
- }
+ const systemPrompt = useMemo(
+ function computeSystemPrompt() {
+ const serializedSchema = snapshot.schema ? JSON.stringify(snapshot.schema, null, 2) : "{}";
+ const contextCommands = currentTool.commands.filter((command) =>
+ contextSelection.commandKeys.includes(command.key),
+ );
+ const contextParameters = currentTool.parameters.filter((parameter) =>
+ contextSelection.parameterKeys.includes(parameter.key),
+ );
+ return generatePrompt(serializedSchema, {
+ context: {
+ selectedCommands: contextCommands.map((command) => ({
+ key: command.key,
+ name: command.name,
+ })),
+ selectedParameters: contextParameters.map((parameter) => ({
+ key: parameter.key,
+ name: parameter.name,
+ longFlag: parameter.longFlag,
+ shortFlag: parameter.shortFlag,
+ })),
+ },
+ });
+ },
+ [snapshot.schema, currentTool, contextSelection],
+ );
- onGeneratingChange?.(false);
- onStreamingTool?.(null);
- } catch (error) {
- onGeneratingChange?.(false);
- onStreamingTool?.(null);
- const isAbort = error instanceof Error && error.name === "AbortError";
- if (isAbort) {
- if (isToolAppliedRef.current && toolSnapshotRef.current) {
- onApply(toolSnapshotRef.current);
+ const agent = useMemo(
+ function createAgent() {
+ const aiModel = createModelInstance(provider, currentKeys.key ?? "", snapshot.model);
+
+ const editToolDef = createEditTool(
+ () => store.getPendingPreview() ?? store.getCurrentTool(),
+ function onEditPreview(tool) {
+ store.setPendingPreview(tool);
+ onStreamingTool?.(tool);
+ },
+ );
+
+ const applyToolDefinitionDef = createApplyToolDefinitionTool(
+ () => {},
+ function onApplyExecuted() {
+ const preview = store.getPendingPreview();
+ if (preview) {
+ onApply(replaceKey(preview) as Tool);
+ store.setPendingPreview(null);
}
- } else {
- toast.error(error instanceof Error ? error.message : "AI request failed");
- }
- setMessages((prev) => prev.slice(0, -2));
- } finally {
- setIsStreaming(false);
- }
+ onStreamingTool?.(null);
+ },
+ );
+
+ const tools = {
+ editTool: editToolDef,
+ applyToolDefinition: applyToolDefinitionDef,
+ readTool: createReadTool(() => store.getPendingPreview() ?? store.getCurrentTool()),
+ ...(tavilyKeys.isSaved && tavilyKeys.key
+ ? { tavilySearch: createTavilySearchTool(tavilyKeys.key) }
+ : {}),
+ ...(tavilyKeys.isSaved && tavilyKeys.key
+ ? { tavilyExtract: createTavilyExtractTool(tavilyKeys.key) }
+ : {}),
+ };
+
+ return createToolLoopAgent({
+ model: aiModel,
+ instructions: systemPrompt,
+ tools,
+ providerOptions,
+ onFinish: function handleFinishUsage({ usage }) {
+ store.setUsage(usage);
+ },
+ });
},
[
- currentTool,
- context,
provider,
currentKeys.key,
- model,
- tavilyKeys,
+ snapshot.model,
+ systemPrompt,
+ providerOptions,
+ tavilyKeys.isSaved,
+ tavilyKeys.key,
+ store,
onApply,
- onGeneratingChange,
onStreamingTool,
],
);
- const sendMessage = useCallback(
- async (textOverride?: string) => {
- const userText = (textOverride ?? input).trim();
- if (!userText || isStreaming || !model) return;
+ const transport = useMemo(
+ function createTransport() {
+ return new DirectChatTransport({ agent, sendReasoning: true });
+ },
+ [agent],
+ );
+
+ const chatInstance = useMemo(
+ function createChatInstance() {
+ return new Chat({
+ id: snapshot.chatId,
+ messages: store.getLastMessages(),
+ transport,
+ sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
+ onFinish: function handleChatFinish() {
+ onGeneratingChange?.(false);
+ },
+ onError: function handleChatError(error) {
+ onGeneratingChange?.(false);
+ onStreamingTool?.(null);
+ if (error.name !== "AbortError") {
+ toast.error(error.message || "AI request failed");
+ }
+ },
+ });
+ },
+ [snapshot.chatId, transport, store, onGeneratingChange, onStreamingTool],
+ );
+
+ const chat = useChat({ chat: chatInstance });
+ const rawMessages = chat.messages;
+ const isStreaming = chat.status === "submitted" || chat.status === "streaming";
- if (!currentKeys.key) {
- toast.error("API key not configured", {
- description: "Open settings to add your API key.",
- });
- return;
- }
+ store.syncApprovals(rawMessages);
+ store.syncPersistence(rawMessages, isStreaming);
+ store.syncGeneratingState(rawMessages, isStreaming, onGeneratingChange, onStreamingTool);
- if (!textOverride) setInput("");
- const history = messages;
- setMessages((prev) => [
- ...prev,
- { role: "user", content: userText },
- { role: "assistant", content: "" },
- ]);
- setIsStreaming(true);
- await runStream(userText, history);
+ const messages = useMemo(
+ function computeMessages() {
+ return rawMessages
+ .filter((message) => message.role === "user" || message.role === "assistant")
+ .map((message) => toChatMessage(message, snapshot.approvalArtifacts));
},
- [input, isStreaming, model, currentKeys.key, messages, runStream],
+ [rawMessages, snapshot.approvalArtifacts],
);
- const stopStreaming = useCallback(() => {
- abortControllerRef.current?.abort();
- }, []);
+ const pendingApproval = useMemo(
+ function computePendingApproval() {
+ return findPendingApproval(messages);
+ },
+ [messages],
+ );
- const resendFromIndex = useCallback(
- async (index: number, newContent: string) => {
- if (isStreaming || !newContent.trim() || !model || !currentKeys.key) return;
- const history = messages.slice(0, index);
- setMessages([
- ...history,
- { role: "user", content: newContent },
- { role: "assistant", content: "" },
- ]);
- setIsStreaming(true);
- await runStream(newContent, history);
+ const toolCallCount = useMemo(
+ function computeToolCallCount() {
+ return countCompletedToolCalls(rawMessages);
},
- [isStreaming, model, currentKeys.key, messages, runStream],
+ [rawMessages],
);
- const clearMessages = useCallback(() => {
- setMessages([]);
- setInput("");
- }, []);
+ function sendMessage(textOverride?: string) {
+ const userText = (textOverride ?? snapshot.input).trim();
+ if (!userText || isStreaming || !snapshot.model) return;
+ if (!currentKeys.key) {
+ toast.error("API key not configured", {
+ description: "Open settings to add your API key.",
+ });
+ return;
+ }
+ store.prepareForSend();
+ if (!textOverride) {
+ store.clearInput();
+ }
+ chat.sendMessage({ text: userText });
+ }
+
+ function resendFromIndex(index: number, newContent: string) {
+ if (isStreaming || !newContent.trim() || !snapshot.model || !currentKeys.key) return;
+ store.prepareForResend(onStreamingTool);
+ chat.setMessages(rawMessages.slice(0, index));
+ chat.sendMessage({ text: newContent });
+ }
+
+ function clearMessages() {
+ store.flushAndReset(rawMessages, onStreamingTool, onGeneratingChange);
+ }
+
+ function confirmPatch() {
+ if (!pendingApproval) return;
+ chat.addToolApprovalResponse({
+ id: pendingApproval.approvalId,
+ approved: true,
+ reason:
+ "Applied successfully. Provide a concise summary of all the changes you made to the tool.",
+ });
+ }
+
+ function rejectPatch() {
+ if (!pendingApproval) return;
+ chat.addToolApprovalResponse({
+ id: pendingApproval.approvalId,
+ approved: false,
+ reason: "User rejected the changes",
+ });
+ store.setPendingPreview(null);
+ onStreamingTool?.(null);
+ }
+
+ function loadSession(session: ChatWithPreview) {
+ store.loadSession(session, onStreamingTool, onGeneratingChange);
+ }
return {
messages,
- input,
- setInput,
+ input: snapshot.input,
+ setInput: store.setInput.bind(store),
isStreaming,
- model,
- setModel,
+ model: snapshot.model,
+ setModel: store.setModel.bind(store),
+ reasoningEffort: snapshot.reasoningEffort,
+ setReasoningEffort: store.setReasoningEffort.bind(store),
provider,
sendMessage,
- stopStreaming,
+ stopStreaming: chat.stop,
resendFromIndex,
clearMessages,
allProviderKeys,
tavilyKeys,
+ pendingApproval,
+ confirmPatch,
+ rejectPatch,
+ usage: snapshot.usage,
+ toolCallCount,
+ recentSessions: snapshot.recentSessions,
+ loadSession,
};
}
@@ -592,6 +480,15 @@ interface MessagePartProps {
setEditingValue: (v: string) => void;
onResend: (index: number, content: string) => void;
onRetry: () => void;
+ pendingApproval: {
+ approvalId: string;
+ previewTool: Tool;
+ originalTool: Tool;
+ summary: string;
+ messageIndex: number;
+ } | null;
+ onConfirmPatch: () => void;
+ onRejectPatch: () => void;
}
function MessagePart({
@@ -605,33 +502,142 @@ function MessagePart({
setEditingValue,
onResend,
onRetry,
+ pendingApproval,
+ onConfirmPatch,
+ onRejectPatch,
}: MessagePartProps) {
+ const chainCalls = msg.toolCalls?.filter((tc) => tc.toolName !== "applyToolDefinition") ?? [];
+ const applyToolCall = msg.toolCalls?.find((tc) => tc.toolName === "applyToolDefinition");
+ const isPending = applyToolCall && pendingApproval?.messageIndex === index;
+
return (
- {msg.toolCalls?.map((tc) => (
+ {chainCalls.length > 0 && (
+
+
+
+ {chainCalls.map((tc) => (
+ {
+ const urls = (tc.input.urls as string[] | undefined) ?? [];
+ try {
+ return `Extracted from ${new URL(urls[0]).hostname}`;
+ } catch {
+ return "Extracted content";
+ }
+ })()
+ : "Extracting content…"
+ : tc.toolName === "readTool"
+ ? (() => {
+ const jsonPath = tc.input.jsonPath as string | undefined;
+ const summary = tc.input.summary as string | undefined;
+ const pathLabel =
+ jsonPath && jsonPath !== "$" ? jsonPath : "whole tool";
+ if (tc.state !== "output-available")
+ return summary
+ ? `${summary} (${pathLabel})`
+ : `Reading ${pathLabel}…`;
+ return summary ? `${summary} (${pathLabel})` : `Read ${pathLabel}`;
+ })()
+ : (tc.title ?? "Editing…")
+ }
+ status={tc.state === "output-available" ? "complete" : "active"}
+ >
+ {tc.toolName === "tavilySearch" && tc.state === "output-available" && (
+
+ )}
+ {tc.toolName === "tavilyExtract" && tc.state === "output-available" && (
+
+ )}
+ {tc.toolName === "editTool" &&
+ tc.state === "output-available" &&
+ (() => {
+ const patch = tc.input.patch as Record | undefined;
+ if (!patch) return null;
+ const keys = Object.keys(patch);
+ return (
+
+
+
+ {keys.length} field{keys.length !== 1 ? "s" : ""} modified
+
+
+
+ {JSON.stringify(patch, null, 2)}
+
+
+
+ );
+ })()}
+
+ ))}
+
+
+ )}
+ {applyToolCall && (
- {tc.toolName !== "editTool" && (
-
-
- {tc.state === "output-available" && (
-
- )}
-
- )}
+
+ {applyToolCall.originalTool && applyToolCall.previewTool && (
+
+ )}
+ {isPending && (
+
+
{pendingApproval.summary}
+
+
+
+ Apply
+
+
+
+ Dismiss
+
+
+
+ )}
+
- ))}
+ )}
{msg.role === "user" ? (
editingIndex === index ? (
@@ -674,7 +680,7 @@ function MessagePart({
) : (
<>
- {msg.content}
+ {msg.content}
{!isStreaming && (
+ {msg.reasoningContent && (
+
+
+ {msg.reasoningContent}
+
+ )}
{msg.content ? (
{msg.content}
- ) : (
+ ) : isStreaming && isLast && !applyToolCall ? (
Generating response...
- )}
+ ) : null}
{msg.content && !isStreaming && isLast && (
@@ -744,29 +756,18 @@ export function AIChatPanel({
onStreamingTool,
onGeneratingChange,
}: AIChatPanelProps) {
- const { selectedCommand, selectedParameter } = useToolBuilder();
- const chat = useAIChat(
- tool,
- onApply,
- {
- selectedCommandName: selectedCommand?.name,
- selectedParameterName: selectedParameter?.name ?? undefined,
- },
- onStreamingTool,
- onGeneratingChange,
- );
- const [modelOpen, setModelOpen] = useState(false);
- const [selectedProvider, setSelectedProvider] = useState(chat.provider);
+ const { contextSelection, clearContextSelection, tool: currentTool } = useToolBuilder();
+ const chat = useAIChat(tool, onApply, onStreamingTool, onGeneratingChange);
const [editingIndex, setEditingIndex] = useState(null);
const [editingValue, setEditingValue] = useState("");
- const currentGroup = MODEL_GROUPS.find((g) => g.provider === chat.provider);
- const currentModelLabel =
- MODEL_GROUPS.flatMap((g) => g.models).find((m) => m.value === chat.model)?.label ?? chat.model;
- const selectedGroup =
- MODEL_GROUPS.find((g) => g.provider === selectedProvider) ?? MODEL_GROUPS[0];
- const selectedProviderKeys =
- chat.allProviderKeys[selectedProvider as Exclude];
+ const contextCommands = currentTool.commands.filter((c) =>
+ contextSelection.commandKeys.includes(c.key),
+ );
+ const contextParameters = currentTool.parameters.filter((p) =>
+ contextSelection.parameterKeys.includes(p.key),
+ );
+ const hasContextItems = contextCommands.length > 0 || contextParameters.length > 0;
return (
-
-
-
-
-
-
-
- API Keys
-
- {MODEL_GROUPS.map((g, gi) => {
- const keys = chat.allProviderKeys[g.provider as Exclude
];
- return (
-
- {gi > 0 &&
}
-
-
-
-
{g.label}
- {keys.isSaved &&
}
-
-
keys.setKey(e.target.value)}
- className="h-8 text-xs"
- placeholder={g.keyPlaceholder}
- />
-
-
- keys.setSaved(c === "indeterminate" ? false : c)
- }
- />
-
- Save key locally
-
-
-
-
- );
- })}
-
-
-
-
- Tavily (Web Search)
- {chat.tavilyKeys.isSaved && (
-
- )}
-
-
chat.tavilyKeys.setKey(e.target.value)}
- className="h-8 text-xs"
- placeholder="tvly-..."
- />
-
-
- chat.tavilyKeys.setSaved(c === "indeterminate" ? false : c)
- }
- />
-
- Save key locally
-
-
-
-
-
-
+
@@ -896,19 +808,29 @@ export function AIChatPanel({
Describe what you want or ask to modify the tool
-
- {PROMPT_PILLS.map((pill) => (
-
- {pill.label}
-
- ))}
-
+ {chat.recentSessions.length > 0 && (
+
+
+ Recent sessions
+
+
+ {chat.recentSessions.map((session) => (
+ chat.loadSession(session)}
+ className="flex w-full items-start justify-between gap-2 rounded border border-border/40 bg-muted/30 px-3 py-2 text-left text-xs transition-colors hover:border-border/60 hover:bg-muted/50"
+ >
+
+ {session.preview || "Empty session"}
+
+
+ {formatRelativeTime(session.updatedAt)}
+
+
+ ))}
+
+
+ )}
) : (
chat.messages.map((msg, i) => (
@@ -924,6 +846,9 @@ export function AIChatPanel({
setEditingValue={setEditingValue}
onResend={chat.resendFromIndex}
onRetry={() => chat.resendFromIndex(i - 1, chat.messages[i - 1]?.content ?? "")}
+ pendingApproval={chat.pendingApproval}
+ onConfirmPatch={chat.confirmPatch}
+ onRejectPatch={chat.rejectPatch}
/>
))
)}
@@ -931,127 +856,126 @@ export function AIChatPanel({
-