diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 0000000..a4ce6d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,32 @@ +name: 🐞 Bug Report +description: File a bug report +title: "[Bug]: " +type: "Bug" +body: + - type: markdown + attributes: + value: | + Thanks for stopping by to let us know something could be better! + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us what you expected to happen and how to reproduce the issue. + placeholder: Tell us what you see! + value: "A bug happened!" + validations: + required: true + - type: textarea + id: logs + attributes: + label: Relevant log output + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 0000000..883f4b4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,40 @@ +name: 💡 Feature Request +description: Suggest an idea for this repository +title: "[Feat]: " +type: "Feature" +body: + - type: markdown + attributes: + value: | + Thanks for stopping by to let us know something could be better! + - type: textarea + id: problem + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. + placeholder: Ex. I'm always frustrated when [...] + - type: textarea + id: describe + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/linters/.markdownlint.json b/.github/linters/.markdownlint.json new file mode 100644 index 0000000..38fb124 --- /dev/null +++ b/.github/linters/.markdownlint.json @@ -0,0 +1,10 @@ +{ + "default": true, + "MD013": false, + "MD007": { + "indent": 4 + }, + "MD033": false, + "MD046": false, + "MD024": false +} diff --git a/.github/linters/.yamllint.yml b/.github/linters/.yamllint.yml new file mode 100644 index 0000000..c99862c --- /dev/null +++ b/.github/linters/.yamllint.yml @@ -0,0 +1,4 @@ +rules: + line-length: + max: 80 + level: warning diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..8edd7ea --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,91 @@ + + +# Description + +Please include a summary of the changes and the related issue. Please also +include relevant motivation and context. + +## Type of change + +Please delete options that are not relevant. + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] **Breaking change** (fix or feature that would cause existing + functionality to not work as expected, **including removal of schema files + or fields**) +- [ ] Documentation update + +--- + +### Is this a Breaking Change or Removal? + +If you checked "Breaking change" above, or if you are removing **any** schema +files or fields: + +- [ ] **I have added `!` to my PR title** (e.g., `feat!: remove field`). +- [ ] **I have added justification below.** + +## Breaking Changes / Removal Justification + +(Please provide a detailed technical and strategic rationale here for why this +breaking change or removal is necessary.) + +--- + +## Checklist + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published in downstream modules + +--- + +### Pull Request Title + +This repository enforces **Conventional Commits**. Your PR title must follow +this format: `type: description` or `type!: description` for breaking changes. + +**Types:** + +- `feat`: A new feature +- `fix`: A bug fix +- `docs`: Documentation only changes +- `style`: Changes that do not affect the meaning of the code (white-space, + formatting, etc) +- `refactor`: A code change that neither fixes a bug nor adds a feature +- `perf`: A code change that improves performance +- `test`: Adding missing tests or correcting existing tests +- `chore`: Changes to the build process or auxiliary tools and libraries + +**Breaking Changes:** + +If your change is a breaking change (e.g., removing a field or file), you +**must** add `!` before the colon in your title: +`type!: description` + +**Examples:** + +- `feat: add new payment gateway` +- `fix: resolve crash on checkout` +- `docs: update setup guide` +- `feat!: remove deprecated buyer field from checkout` diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 0000000..ceb70a5 --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,40 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "Conventional Commits" + +on: + pull_request: + types: + - opened + - edited + - synchronize + +permissions: + contents: read + +jobs: + main: + permissions: + pull-requests: read + statuses: write + name: Validate PR Title + runs-on: ubuntu-latest + steps: + - name: semantic-pull-request + uses: amannn/action-semantic-pull-request@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + validateSingleCommit: false diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml new file mode 100644 index 0000000..f0ae511 --- /dev/null +++ b/.github/workflows/linter.yaml @@ -0,0 +1,67 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Lint Code Base + +on: + pull_request: + branches: [main] + +permissions: + contents: read # Required to checkout the code + packages: read # Required to pull the Super-Linter docker image + statuses: write # Required to fix the 403 error (updating status checks) + +jobs: + build: + name: Lint Code Base + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Lint Code Base + uses: super-linter/super-linter/slim@v8 + env: + DEFAULT_BRANCH: origin/main + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MARKDOWN_CONFIG_FILE: ".markdownlint.json" + PYTHON_RUFF_CONFIG_FILE: ../../pyproject.toml + PYTHON_RUFF_FORMAT_CONFIG_FILE: ../../pyproject.toml + PYTHON_RUFF_FORMAT_CHECK_ONLY_MODE_OPTIONS: "--check --diff" + LOG_LEVEL: INFO + SHELLCHECK_OPTS: -e SC1091 -e 2086 + VALIDATE_ALL_CODEBASE: false + FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/).*|CODE_OF_CONDUCT.md|CHANGELOG.md" + VALIDATE_BIOME_FORMAT: false + VALIDATE_PYTHON_BLACK: false + VALIDATE_PYTHON_FLAKE8: false + VALIDATE_PYTHON_ISORT: false + VALIDATE_PYTHON_MYPY: false + VALIDATE_PYTHON_PYLINT: false + VALIDATE_CHECKOV: false + VALIDATE_NATURAL_LANGUAGE: false + VALIDATE_MARKDOWN_PRETTIER: false + VALIDATE_JAVASCRIPT_PRETTIER: false + VALIDATE_JSON_PRETTIER: false + VALIDATE_YAML_PRETTIER: false + VALIDATE_GIT_COMMITLINT: false + VALIDATE_GITHUB_ACTIONS_ZIZMOR: false + VALIDATE_JSCPD: false + VALIDATE_TYPESCRIPT_ES: false + VALIDATE_TYPESCRIPT_PRETTIER: false + VALIDATE_TSX: false diff --git a/.github/workflows/spellcheck.yaml b/.github/workflows/spellcheck.yaml new file mode 100644 index 0000000..ec44922 --- /dev/null +++ b/.github/workflows/spellcheck.yaml @@ -0,0 +1,29 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "Check Spelling" +on: + pull_request: + +jobs: + spellcheck: + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + steps: + - uses: actions/checkout@v4 + - uses: streetsidesoftware/cspell-action@v7 + with: + incremental_files_only: true diff --git a/a2a/chat-client/App.tsx b/a2a/chat-client/App.tsx index a7081b8..0886e2c 100644 --- a/a2a/chat-client/App.tsx +++ b/a2a/chat-client/App.tsx @@ -13,26 +13,44 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, {useEffect, useRef, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import ChatInput from './components/ChatInput'; import ChatMessageComponent from './components/ChatMessage'; import Header from './components/Header'; import {appConfig} from './config'; import {CredentialProviderProxy} from './mocks/credentialProviderProxy'; -import {ChatMessage, PaymentInstrument, Product, Sender} from './types'; +import {type ChatMessage, type PaymentInstrument, type Product, Sender, type Checkout, type PaymentHandler} from './types'; + +type RequestPart = + | {type: 'text'; text: string} + | {type: 'data'; data: Record}; + +function createChatMessage( + sender: Sender, + text: string, + props: Partial = {}, +): ChatMessage { + return { + id: crypto.randomUUID(), + sender, + text, + ...props, + }; +} -const initialMessage: ChatMessage = { - sender: Sender.MODEL, - text: appConfig.defaultMessage, -}; +const initialMessage: ChatMessage = createChatMessage( + Sender.MODEL, + appConfig.defaultMessage, + {id: 'initial'}, +); /** * An example A2A chat client that demonstrates consuming a business's A2A Agent with UCP Extension. * Only for demo purposes, not intended for production use. */ function App() { - const [user_email, setUserEmail] = useState('foo@example.com'); + const [user_email, _setUserEmail] = useState('foo@example.com'); const [messages, setMessages] = useState([initialMessage]); const [isLoading, setIsLoading] = useState(false); const [contextId, setContextId] = useState(null); @@ -41,6 +59,7 @@ function App() { const chatContainerRef = useRef(null); // Scroll to the bottom when new messages are added + // biome-ignore lint/correctness/useExhaustiveDependencies: Scroll when messages change useEffect(() => { if (chatContainerRef.current) { chatContainerRef.current.scrollTop = @@ -64,25 +83,25 @@ function App() { }); }; - const handlePaymentMethodSelection = async (checkout: any) => { + const handlePaymentMethodSelection = async (checkout: Checkout) => { if (!checkout || !checkout.payment || !checkout.payment.handlers) { - const errorMessage: ChatMessage = { - sender: Sender.MODEL, - text: "Sorry, I couldn't retrieve payment methods.", - }; + const errorMessage = createChatMessage( + Sender.MODEL, + "Sorry, I couldn't retrieve payment methods.", + ); setMessages((prev) => [...prev, errorMessage]); return; } //find the handler with id "example_payment_provider" const handler = checkout.payment.handlers.find( - (handler: any) => handler.id === 'example_payment_provider', + (handler: PaymentHandler) => handler.id === 'example_payment_provider', ); if (!handler) { - const errorMessage: ChatMessage = { - sender: Sender.MODEL, - text: "Sorry, I couldn't find the supported payment handler.", - }; + const errorMessage = createChatMessage( + Sender.MODEL, + "Sorry, I couldn't find the supported payment handler.", + ); setMessages((prev) => [...prev, errorMessage]); return; } @@ -95,18 +114,16 @@ function App() { ); const paymentMethods = paymentResponse.payment_method_aliases; - const paymentSelectorMessage: ChatMessage = { - sender: Sender.MODEL, - text: '', + const paymentSelectorMessage = createChatMessage(Sender.MODEL, '', { paymentMethods, - }; + }); setMessages((prev) => [...prev, paymentSelectorMessage]); } catch (error) { console.error('Failed to resolve mandate:', error); - const errorMessage: ChatMessage = { - sender: Sender.MODEL, - text: "Sorry, I couldn't retrieve payment methods.", - }; + const errorMessage = createChatMessage( + Sender.MODEL, + "Sorry, I couldn't retrieve payment methods.", + ); setMessages((prev) => [...prev, errorMessage]); } }; @@ -116,11 +133,11 @@ function App() { setMessages((prev) => prev.filter((msg) => !msg.paymentMethods)); // Add a temporary user message - const userActionMessage: ChatMessage = { - sender: Sender.USER, - text: `User selected payment method: ${selectedMethod}`, - isUserAction: true, - }; + const userActionMessage = createChatMessage( + Sender.USER, + `User selected payment method: ${selectedMethod}`, + {isUserAction: true}, + ); setMessages((prev) => [...prev, userActionMessage]); try { @@ -138,29 +155,27 @@ function App() { throw new Error('Failed to retrieve payment credential'); } - const paymentInstrumentMessage: ChatMessage = { - sender: Sender.MODEL, - text: '', + const paymentInstrumentMessage = createChatMessage(Sender.MODEL, '', { paymentInstrument, - }; + }); setMessages((prev) => [...prev, paymentInstrumentMessage]); } catch (error) { console.error('Failed to process payment mandate:', error); - const errorMessage: ChatMessage = { - sender: Sender.MODEL, - text: "Sorry, I couldn't process the payment. Please try again.", - }; + const errorMessage = createChatMessage( + Sender.MODEL, + "Sorry, I couldn't process the payment. Please try again.", + ); setMessages((prev) => [...prev, errorMessage]); } }; const handleConfirmPayment = async (paymentInstrument: PaymentInstrument) => { // Hide the payment confirmation component - const userActionMessage: ChatMessage = { - sender: Sender.USER, - text: `User confirmed payment.`, - isUserAction: true, - }; + const userActionMessage = createChatMessage( + Sender.USER, + `User confirmed payment.`, + {isUserAction: true}, + ); // Let handleSendMessage manage the loading indicator setMessages((prev) => [ ...prev.filter((msg) => !msg.paymentInstrument), @@ -168,7 +183,7 @@ function App() { ]); try { - const parts = [ + const parts: RequestPart[] = [ {type: 'data', data: {'action': 'complete_checkout'}}, { type: 'data', @@ -184,10 +199,10 @@ function App() { }); } catch (error) { console.error('Error confirming payment:', error); - const errorMessage: ChatMessage = { - sender: Sender.MODEL, - text: 'Sorry, there was an issue confirming your payment.', - }; + const errorMessage = createChatMessage( + Sender.MODEL, + 'Sorry, there was an issue confirming your payment.', + ); // If handleSendMessage wasn't called, we might need to manually update state // In this case, we remove the loading indicator that handleSendMessage would have added setMessages((prev) => [...prev.slice(0, -1), errorMessage]); // This assumes handleSendMessage added a loader @@ -196,26 +211,26 @@ function App() { }; const handleSendMessage = async ( - messageContent: string | any[], + messageContent: string | RequestPart[], options?: {isUserAction?: boolean; headers?: Record}, ) => { if (isLoading) return; - const userMessage: ChatMessage = { - sender: Sender.USER, - text: options?.isUserAction + const userMessage = createChatMessage( + Sender.USER, + options?.isUserAction ? '' : typeof messageContent === 'string' ? messageContent : 'Sent complex data', - }; + ); if (userMessage.text) { // Only add if there's text setMessages((prev) => [...prev, userMessage]); } setMessages((prev) => [ ...prev, - {sender: Sender.MODEL, text: '', isLoading: true}, + createChatMessage(Sender.MODEL, '', {isLoading: true}), ]); setIsLoading(true); @@ -225,7 +240,19 @@ function App() { ? [{type: 'text', text: messageContent}] : messageContent; - const requestParams: any = { + const requestParams: { + message: { + role: string; + parts: RequestPart[]; + messageId: string; + kind: string; + contextId?: string; + taskId?: string; + }; + configuration: { + historyLength: number; + }; + } = { message: { role: 'user', parts: requestParts, @@ -284,10 +311,7 @@ function App() { setTaskId(undefined); } - const combinedBotMessage: ChatMessage = { - sender: Sender.MODEL, - text: '', - }; + const combinedBotMessage = createChatMessage(Sender.MODEL, ''); const responseParts = data.result?.parts || data.result?.status?.message?.parts || []; @@ -326,15 +350,15 @@ function App() { "Sorry, I received a response I couldn't understand."; setMessages((prev) => [ ...prev.slice(0, -1), - {sender: Sender.MODEL, text: fallbackResponse}, + createChatMessage(Sender.MODEL, fallbackResponse), ]); } } catch (error) { console.error('Error sending message:', error); - const errorMessage: ChatMessage = { - sender: Sender.MODEL, - text: 'Sorry, something went wrong. Please try again.', - }; + const errorMessage = createChatMessage( + Sender.MODEL, + 'Sorry, something went wrong. Please try again.', + ); // Replace the placeholder with the error message setMessages((prev) => [...prev.slice(0, -1), errorMessage]); } finally { @@ -352,7 +376,7 @@ function App() { className="flex-grow overflow-y-auto p-4 md:p-6 space-y-2"> {messages.map((msg, index) => ( ) => ( void; @@ -23,6 +24,8 @@ interface ChatInputProps { function SendIcon(props: React.SVGProps) { return (
- + logo
{appConfig.name}
@@ -102,12 +102,9 @@ function ChatMessageComponent({
{message.text && (
-
'), - }} - /> +
+ {message.text} +
)} @@ -121,7 +118,7 @@ function ChatMessageComponent({ {message.paymentInstrument && onConfirmPayment && ( onConfirmPayment(message.paymentInstrument!)} + onConfirm={() => onConfirmPayment(message.paymentInstrument)} /> )} diff --git a/a2a/chat-client/components/Checkout.tsx b/a2a/chat-client/components/Checkout.tsx index 8c83084..607c2ba 100644 --- a/a2a/chat-client/components/Checkout.tsx +++ b/a2a/chat-client/components/Checkout.tsx @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, {useState} from 'react'; +import type React from 'react'; +import {useState} from 'react'; -import {Checkout} from '../types'; +import type {Checkout, CheckoutItem} from '../types'; interface CheckoutProps { checkout: Checkout; @@ -42,7 +43,7 @@ const CheckoutComponent: React.FC = ({ return checkout.totals.find((t) => t.type === type); }; - const getItemTotal = (lineItem: any) => { + const getItemTotal = (lineItem: CheckoutItem) => { return lineItem.totals.find((t) => t.type === 'total'); }; @@ -53,6 +54,8 @@ const CheckoutComponent: React.FC = ({

= ({

)}
- {itemsToShow.map((lineItem: any) => ( + {itemsToShow.map((lineItem: CheckoutItem) => (
= ({ {checkout.line_items.length > 5 && (
diff --git a/a2a/chat-client/components/Header.tsx b/a2a/chat-client/components/Header.tsx index ebd3066..8d5c8a9 100644 --- a/a2a/chat-client/components/Header.tsx +++ b/a2a/chat-client/components/Header.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ import {appConfig} from '@/config'; -import React from 'react'; function Header() { return ( diff --git a/a2a/chat-client/components/PaymentConfirmation.tsx b/a2a/chat-client/components/PaymentConfirmation.tsx index e7372e1..592c6a9 100644 --- a/a2a/chat-client/components/PaymentConfirmation.tsx +++ b/a2a/chat-client/components/PaymentConfirmation.tsx @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, {useState} from 'react'; -import {PaymentInstrument} from '../types'; +import type React from 'react'; +import {useState} from 'react'; +import type {PaymentInstrument} from '../types'; interface PaymentConfirmationProps { paymentInstrument: PaymentInstrument; @@ -49,12 +50,16 @@ const PaymentConfirmationComponent: React.FC = ({ Please confirm to complete your purchase.

- {paymentMethods.map((method, index) => ( + {paymentMethods.map((method) => (