Showing preview only (1,638K chars total). Download the full file or copy to clipboard to get everything.
Repository: jonesphillip/weft
Branch: main
Commit: 93efd90d3c68
Files: 171
Total size: 1.5 MB
Directory structure:
gitextract_09zdyiuf/
├── .env.example
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── eslint.config.js
├── index.html
├── package.json
├── src/
│ ├── App.css
│ ├── App.tsx
│ ├── api/
│ │ └── client.ts
│ ├── components/
│ │ ├── Approval/
│ │ │ ├── Approval.css
│ │ │ ├── ApprovalFooter.tsx
│ │ │ ├── ApprovalViewRegistry.tsx
│ │ │ ├── DefaultApproval.tsx
│ │ │ ├── EmailApproval.css
│ │ │ ├── EmailApproval.tsx
│ │ │ ├── GitHubPRApproval.css
│ │ │ ├── GitHubPRApproval.tsx
│ │ │ ├── GitHubPRReviewApproval.css
│ │ │ ├── GitHubPRReviewApproval.tsx
│ │ │ ├── GoogleDocsApproval.css
│ │ │ ├── GoogleDocsApproval.tsx
│ │ │ ├── GoogleSheetsApproval.css
│ │ │ ├── GoogleSheetsApproval.tsx
│ │ │ └── index.ts
│ │ ├── Board/
│ │ │ ├── Board.css
│ │ │ └── Board.tsx
│ │ ├── Column/
│ │ │ ├── Column.css
│ │ │ └── Column.tsx
│ │ ├── CommandPalette/
│ │ │ ├── CommandPalette.css
│ │ │ ├── CommandPalette.tsx
│ │ │ └── index.ts
│ │ ├── CommentableText/
│ │ │ ├── CommentableText.css
│ │ │ └── CommentableText.tsx
│ │ ├── DiffViewer/
│ │ │ ├── DiffViewer.css
│ │ │ ├── DiffViewer.tsx
│ │ │ └── index.ts
│ │ ├── GitHubCallback.tsx
│ │ ├── GoogleCallback.tsx
│ │ ├── Header/
│ │ │ ├── Header.css
│ │ │ ├── Header.tsx
│ │ │ └── WeftLogo.tsx
│ │ ├── Home/
│ │ │ ├── Home.css
│ │ │ └── Home.tsx
│ │ ├── MCP/
│ │ │ ├── MCP.css
│ │ │ ├── MCPOAuthCallback.css
│ │ │ ├── MCPOAuthCallback.tsx
│ │ │ ├── MCPServerConnect.css
│ │ │ ├── MCPServerConnect.tsx
│ │ │ └── index.ts
│ │ ├── Settings/
│ │ │ ├── AccountsSection.css
│ │ │ ├── AccountsSection.tsx
│ │ │ ├── BoardSettings.css
│ │ │ ├── BoardSettings.tsx
│ │ │ ├── MCPSection.css
│ │ │ ├── MCPSection.tsx
│ │ │ ├── index.ts
│ │ │ └── sections/
│ │ │ ├── CredentialsSection.tsx
│ │ │ ├── DangerSection.tsx
│ │ │ ├── GeneralSection.tsx
│ │ │ ├── IntegrationsSection.tsx
│ │ │ └── index.ts
│ │ ├── Task/
│ │ │ ├── AgentSection.css
│ │ │ ├── AgentSection.tsx
│ │ │ ├── RunHistory.css
│ │ │ ├── RunHistory.tsx
│ │ │ ├── TaskCard.css
│ │ │ ├── TaskCard.tsx
│ │ │ ├── TaskModal.css
│ │ │ └── TaskModal.tsx
│ │ ├── Toast/
│ │ │ ├── Toast.css
│ │ │ ├── Toast.tsx
│ │ │ ├── ToastContainer.tsx
│ │ │ ├── WorkflowToastListener.tsx
│ │ │ └── index.ts
│ │ ├── Workflow/
│ │ │ ├── EmailViewerModal.css
│ │ │ ├── EmailViewerModal.tsx
│ │ │ ├── PlanReviewView.css
│ │ │ ├── PlanReviewView.tsx
│ │ │ ├── Workflow.css
│ │ │ ├── WorkflowProgress.tsx
│ │ │ └── index.ts
│ │ └── common/
│ │ ├── Button.css
│ │ ├── Button.tsx
│ │ ├── ErrorBoundary.css
│ │ ├── ErrorBoundary.tsx
│ │ ├── Input.css
│ │ ├── Input.tsx
│ │ ├── McpIcon.tsx
│ │ ├── Modal.css
│ │ ├── Modal.tsx
│ │ ├── RichTextEditor.css
│ │ ├── RichTextEditor.tsx
│ │ └── index.ts
│ ├── constants.ts
│ ├── context/
│ │ ├── AuthContext.tsx
│ │ ├── BoardContext.tsx
│ │ ├── ToastContext.tsx
│ │ └── boardReducer.ts
│ ├── hooks/
│ │ ├── index.ts
│ │ ├── useApprovalComments.ts
│ │ ├── useIsMobile.ts
│ │ └── useUrlDetection.ts
│ ├── index.css
│ ├── main.tsx
│ ├── types/
│ │ └── index.ts
│ ├── utils/
│ │ └── diffParser.ts
│ └── vite-env.d.ts
├── tests/
│ ├── mocks/
│ │ └── cloudflare-sandbox.ts
│ └── worker/
│ ├── auth.test.ts
│ ├── board-access.integration.test.ts
│ └── user-isolation.test.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.worker.json
├── vite.config.ts
├── vitest.config.ts
├── vitest.config.workers.ts
├── worker/
│ ├── BoardDO.ts
│ ├── UserDO.ts
│ ├── auth.ts
│ ├── constants.ts
│ ├── db/
│ │ ├── index.ts
│ │ └── schema.ts
│ ├── github/
│ │ ├── GitHubMCP.ts
│ │ ├── githubTools.ts
│ │ ├── index.ts
│ │ └── oauth.ts
│ ├── google/
│ │ ├── DocsMCP.ts
│ │ ├── GmailMCP.ts
│ │ ├── SheetsMCP.ts
│ │ ├── docsTools.ts
│ │ ├── gmailTools.ts
│ │ ├── index.ts
│ │ ├── markdownToDocs.ts
│ │ ├── oauth.ts
│ │ └── sheetsTools.ts
│ ├── handlers/
│ │ ├── boards.ts
│ │ ├── oauth.ts
│ │ └── workflows.ts
│ ├── index.ts
│ ├── mcp/
│ │ ├── AccountMCPRegistry.ts
│ │ ├── MCPBridge.ts
│ │ ├── MCPClient.ts
│ │ ├── SchemaConverter.ts
│ │ ├── index.ts
│ │ └── oauth/
│ │ ├── discovery.ts
│ │ ├── flow.ts
│ │ ├── index.ts
│ │ ├── pkce.ts
│ │ └── types.ts
│ ├── sandbox/
│ │ ├── SandboxMCP.ts
│ │ └── sandboxTools.ts
│ ├── services/
│ │ ├── BoardService.ts
│ │ ├── CredentialService.ts
│ │ ├── MCPOAuthService.ts
│ │ ├── MCPService.ts
│ │ ├── ScheduleService.ts
│ │ ├── WorkflowService.ts
│ │ └── index.ts
│ ├── utils/
│ │ ├── crypto.ts
│ │ ├── logger.ts
│ │ ├── oauth-state.ts
│ │ ├── response.ts
│ │ ├── transformations.ts
│ │ └── zodTools.ts
│ └── workflows/
│ └── AgentWorkflow.ts
├── worker-configuration.d.ts
└── wrangler.jsonc
================================================
FILE CONTENTS
================================================
================================================
FILE: .env.example
================================================
# OAuth Credentials
# Copy this file to .env and fill in your values
# GitHub OAuth App credentials
# Create at: https://github.com/settings/developers
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
# Google OAuth credentials
# Create at: https://console.cloud.google.com/apis/credentials
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
# Encryption key for stored credentials (min 32 characters)
# Generate with: openssl rand -base64 32
ENCRYPTION_KEY=your_encryption_key_min_32_chars
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# wrangler files
.wrangler
.dev.vars*
!.dev.vars.example
# Environment files (secrets)
.env
.env.local
.env.*.local
!.env.example
# Claude Code state
.claude/
================================================
FILE: Dockerfile
================================================
FROM docker.io/cloudflare/sandbox:0.6.7
# Install Claude Code CLI
RUN npm install -g @anthropic-ai/claude-code
# Longer timeout for Claude Code operations
ENV COMMAND_TIMEOUT_MS=300000
# Port 3000 is used by the sandbox SDK internally
EXPOSE 3000
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2025 Phillip Jones
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.
================================================
FILE: README.md
================================================
# Weft
Task management, but AI agents do your tasks.

## What is Weft?
Weft is a personal task board where AI agents work on your tasks. Create a task, assign it to an agent, and it gets to work. Agents can read your emails, draft responses, update spreadsheets, create PRs, and write code.
Run as many agents in parallel as you want. Schedule recurring tasks to run daily or weekly. Get notified when tasks complete or need your approval. All actions that mutate state (sending emails, creating PRs, modifying documents) require your approval before committing.
**Built-in integrations:**
- Gmail (read, draft, send)
- Google Docs (create, edit)
- Google Sheets (create, update)
- GitHub (issues, PRs, code)
- [Cloudflare Sandbox](https://developers.cloudflare.com/sandbox/) (isolated containers for code execution and coding agents)
- [Remote MCP servers](https://modelcontextprotocol.io/) (bring your own tools)
**Self-hosted and private.** Deploy to your own Cloudflare account. Your data stays in your account.
## Architecture
Weft runs entirely on [Cloudflare's Developer Platform](https://workers.cloudflare.com/):
```
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ Browser ◄──────► Worker ◄──────► Durable Objects ◄──────► Workflows │
│ │ (HTTP) (BoardDO, UserDO) (AgentWorkflow)
│ │ │ │ │
│ │ │ ▼ │
│ └─────── WebSocket ──────────────────►│ ┌────────────┐ │
│ (real-time updates) │ Anthropic │ │
│ │ + Tools │ │
│ └────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```
| Component | Purpose |
|-----------|---------|
| **[Workers](https://developers.cloudflare.com/workers/)** | HTTP routing, auth, serves React frontend |
| **[Durable Objects](https://developers.cloudflare.com/durable-objects/)** | Persistent state (boards, tasks, credentials) and real-time WebSocket updates |
| **[Workflows](https://developers.cloudflare.com/workflows/)** | Durable agent loop with automatic checkpointing and retry |
## Setup
### Prerequisites
- [Node.js](https://nodejs.org/) 18+
- [Docker](https://docs.docker.com/desktop/) running (required for [Cloudflare Sandboxes](https://developers.cloudflare.com/sandbox/))
- [Cloudflare account](https://dash.cloudflare.com/sign-up) with [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/)
- OAuth credentials for integrations you want to use:
- [Google](#google-gmail-docs-sheets) for Gmail/Docs/Sheets
- [GitHub](#github) for GitHub
### 1. Clone and install
```bash
git clone https://github.com/jonesphillip/weft.git
cd weft
npm install
```
### 2. Configure environment
Copy the example environment file:
```bash
cp .env.example .env
```
Edit `.env` with your secrets:
```bash
# Encryption key for stored credentials (generate with: openssl rand -base64 32)
ENCRYPTION_KEY=your-generated-key
# OAuth credentials (add the ones you need)
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
```
See [Setting up OAuth credentials](#setting-up-oauth-credentials) for detailed instructions on creating Google and GitHub OAuth apps.
### 3. Run locally
```bash
npm run dev
```
This starts both the Vite dev server (frontend) and Wrangler (worker). Open http://localhost:5174.
By default, Weft runs without authentication. Boards and tasks are stored under the user defined by `USER_ID` in `wrangler.jsonc`.
### 4. Configure your board
After creating your first board, go to **Settings** and add your [Anthropic API key](https://console.anthropic.com/). This key is stored per-board and is required for agents to run.
### 5. Deploy to Cloudflare
Set your secrets (only set the ones you need):
| Secret | Required | Description |
|--------|----------|-------------|
| `ENCRYPTION_KEY` | Yes | Encrypts stored credentials. Generate with `openssl rand -base64 32` |
| `GOOGLE_CLIENT_ID` | For Google integrations | From [Google Cloud Console](#google-gmail-docs-sheets) |
| `GOOGLE_CLIENT_SECRET` | For Google integrations | From [Google Cloud Console](#google-gmail-docs-sheets) |
| `GITHUB_CLIENT_ID` | For GitHub integration | From [GitHub Developer Settings](#github) |
| `GITHUB_CLIENT_SECRET` | For GitHub integration | From [GitHub Developer Settings](#github) |
```bash
npx wrangler secret put ENCRYPTION_KEY --env production
npx wrangler secret put GOOGLE_CLIENT_ID --env production
npx wrangler secret put GOOGLE_CLIENT_SECRET --env production
npx wrangler secret put GITHUB_CLIENT_ID --env production
npx wrangler secret put GITHUB_CLIENT_SECRET --env production
npm run deploy:prod
```
### 6. Configure authentication
Authentication is controlled by the `AUTH_MODE` variable in `wrangler.jsonc`:
| AUTH_MODE | Description | Use case |
|-----------|-------------|----------|
| `none` | No authentication. Uses `USER_ID`/`USER_EMAIL` from config as singleton user. | Personal/single-user deployments |
| `access` | Cloudflare Access JWT verification. Requires `ACCESS_AUD`/`ACCESS_TEAM` secrets. | Multi-user or login-protected deployments |
By default, `AUTH_MODE` is set to `"none"` for both development and production.
#### Enabling Cloudflare Access authentication
For multi-user deployments or if you want login protection, enable [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/):
1. In the Cloudflare dashboard, go to **Zero Trust > Access > Applications**
2. Create a new **Self-hosted application** with your Weft URL
3. Add an **Access policy** (e.g., allow only your email address)
4. After saving, find the **Application Audience (AUD) Tag** in the application settings
5. Your **team name** is in your Zero Trust dashboard URL: `https://<team>.cloudflareaccess.com`
Set the secrets:
```bash
npx wrangler secret put ACCESS_AUD --env production
npx wrangler secret put ACCESS_TEAM --env production
```
Then in `wrangler.jsonc`, change `AUTH_MODE` to `"access"` in the production environment:
```jsonc
"env": {
"production": {
"vars": {
"AUTH_MODE": "access",
// ...
}
}
}
```
## Project Structure
```
worker/
├── index.ts # HTTP routing and auth
├── handlers/ # Request handlers
├── services/ # Business logic
├── workflows/ # Cloudflare Workflows (agent loop)
├── mcp/ # MCP server registry
├── google/ # Google integrations (Gmail, Docs, Sheets)
└── github/ # GitHub integration
src/
├── components/ # React components
├── context/ # React context providers
├── hooks/ # Custom hooks
└── api/ # API client
```
## Scheduled Tasks
Tasks can be configured to run on a schedule (daily, weekly, or cron). When a scheduled task runs, it operates differently from regular tasks:
<p align="center">
<img src="assets/scheduled-tasks-diagram.png" alt="Scheduled Tasks" width="600">
</p>
1. A Durable Object alarm triggers at the scheduled time
2. The parent agent runs with read-only tool access. It cannot send emails, create PRs, or modify anything directly.
3. The parent agent calls `create_task` to fork child tasks for each piece of work it finds
4. Each child task gets its own agent context with full tool access and runs until it needs human approval
This separation means scheduled agents can analyze your inbox or repos overnight, but actual changes still require your review.
| Schedule Type | Example |
|--------------|---------|
| Daily | 8:00 AM in your timezone |
| Weekly | Monday and Thursday at 9:00 AM |
| Custom | Cron expression (`0 */6 * * *`) |
## Adding Tools to Your Board
<p align="center">
<img src="assets/board-settings.png" alt="Board Settings" width="600">
</p>
Each board has its own configuration in **Settings**, organized into tabs:
- **Credentials** - Add your Anthropic API key (required for agents) and connect OAuth accounts like Google and GitHub.
- **Integrations** - Enable built-in tools (Gmail, Docs, Sheets, GitHub) or add any [remote MCP server](https://modelcontextprotocol.io/) for custom tools.
## Building New Integrations
To add a built-in integration, Weft uses a registry-driven architecture:
1. Create your MCP server in `worker/` (see `worker/google/GmailMCP.ts` for example)
2. Register it in `worker/mcp/AccountMCPRegistry.ts`
3. Add OAuth handler if needed in `worker/handlers/oauth.ts`
The registry defines tool schemas, OAuth configuration, and workflow guidance—no changes needed to the agent workflow itself.
## Setting up OAuth credentials
### Google (Gmail, Docs, Sheets)
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Go to **APIs & Services > Library** and enable the Gmail API, Google Docs API, and Google Sheets API
4. Go to **APIs & Services > OAuth consent screen** and configure it (External is fine, add yourself as a test user)
5. Go to **APIs & Services > Credentials**
6. Click **Create Credentials > OAuth client ID**
7. Select **Web application**
8. Add authorized JavaScript origins:
- `http://localhost:5174` (development)
- `https://weft.<your-subdomain>.workers.dev` (production)
9. Add authorized redirect URIs:
- `http://localhost:5174/google/callback` (development)
- `https://weft.<your-subdomain>.workers.dev/google/callback` (production)
10. Copy the Client ID and Client Secret to your `.env` file
### GitHub
1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
2. Click **New OAuth App**
3. Set the homepage URL to your Weft deployment
4. Set the authorization callback URL:
- `http://localhost:5174/github/callback` (development)
- `https://weft.<your-subdomain>.workers.dev/github/callback` (production)
5. Copy the Client ID and Client Secret to your `.env` file
## License
Apache License 2.0 - see [LICENSE](LICENSE)
================================================
FILE: eslint.config.js
================================================
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'
export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactRefresh.configs.vite,
],
plugins: {
'react-hooks': reactHooks,
},
rules: {
// Standard react-hooks rules (without experimental compiler rules)
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
// Allow underscore-prefixed unused vars (convention for intentionally unused params)
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
},
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
================================================
FILE: index.html
================================================
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Weft</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "weft",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev:wrangler": "wrangler dev",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "npm run build && vite preview",
"deploy": "npm run build && wrangler deploy",
"deploy:prod": "CLOUDFLARE_ENV=production npm run build && wrangler deploy --env production",
"cf-typegen": "wrangler types",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:integration": "vitest run --config vitest.config.workers.ts"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.71.2",
"@cloudflare/sandbox": "^0.6.7",
"jose": "^6.1.3",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.11.0",
"zod": "^4.3.4"
},
"devDependencies": {
"@cloudflare/vite-plugin": "^1.13.13",
"@cloudflare/vitest-pool-workers": "^0.11.1",
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react-swc": "^4.0.0",
"@vitest/coverage-v8": "3.2",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^17.0.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.39.1",
"vite": "^7.1.2",
"vitest": "3.2",
"wrangler": "^4.54.0"
}
}
================================================
FILE: src/App.css
================================================
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-main {
flex: 1;
display: flex;
overflow: hidden;
}
/* Auth loading state */
.app-loading {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-3);
color: var(--color-text-muted);
font-family: var(--font-mono);
}
.app-loading-spinner {
width: 32px;
height: 32px;
border: 3px solid var(--color-border-default);
border-top-color: var(--color-accent-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Auth error state */
.app-error {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-6);
text-align: center;
}
.app-error h1 {
font-size: 20px;
font-weight: 600;
color: var(--color-text-primary);
}
.app-error p {
color: var(--color-text-muted);
font-family: var(--font-mono);
font-size: 14px;
}
================================================
FILE: src/App.tsx
================================================
import { useState, useEffect, useCallback } from 'react';
import { BrowserRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { BoardProvider, useBoard } from './context/BoardContext';
import { ToastProvider } from './context/ToastContext';
import { AuthProvider, useAuth } from './context/AuthContext';
import { Header } from './components/Header/Header';
import { Home } from './components/Home/Home';
import { Board } from './components/Board/Board';
import { GitHubCallback } from './components/GitHubCallback';
import { GoogleCallback } from './components/GoogleCallback';
import { MCPOAuthCallback } from './components/MCP/MCPOAuthCallback';
import { CommandPalette } from './components/CommandPalette';
import { ToastContainer, WorkflowToastListener } from './components/Toast';
import { ErrorBoundary } from './components/common';
import './App.css';
// Full-page callback routes (no header/layout)
const CALLBACK_ROUTES = ['/github/callback', '/google/callback', '/mcp/oauth/callback'];
function AuthGate({ children }: { children: React.ReactNode }) {
const { isLoading, error } = useAuth();
if (isLoading) {
return (
<div className="app-loading">
<div className="app-loading-spinner" />
<p>Loading...</p>
</div>
);
}
if (error) {
return (
<div className="app-error">
<h1>Authentication Required</h1>
<p>{error}</p>
</div>
);
}
return <>{children}</>;
}
function AppContent() {
const [paletteOpen, setPaletteOpen] = useState(false);
const { activeBoard, setAddingToColumn } = useBoard();
const location = useLocation();
const handleNewTask = useCallback((columnIndex: number) => {
if (activeBoard && activeBoard.columns[columnIndex]) {
setAddingToColumn(activeBoard.columns[columnIndex].id);
}
}, [activeBoard, setAddingToColumn]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Don't trigger if in an input or textarea
const target = e.target as HTMLElement;
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
// Cmd/Ctrl + K - open command palette
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setPaletteOpen(true);
return;
}
// Escape - close palette
if (e.key === 'Escape' && paletteOpen) {
setPaletteOpen(false);
return;
}
// Shortcuts that only work when not in an input
if (!isInput && !paletteOpen) {
// n - new task in first column
if (e.key === 'n' && activeBoard) {
e.preventDefault();
handleNewTask(0);
return;
}
// 1, 2, 3 - new task in column 1, 2, or 3
if (['1', '2', '3'].includes(e.key) && activeBoard) {
const colIndex = parseInt(e.key) - 1;
if (activeBoard.columns[colIndex]) {
e.preventDefault();
handleNewTask(colIndex);
}
return;
}
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [paletteOpen, activeBoard, handleNewTask]);
// Render full-page callbacks without the app layout
const isCallbackRoute = CALLBACK_ROUTES.includes(location.pathname);
if (isCallbackRoute) {
return (
<Routes>
<Route path="/github/callback" element={<GitHubCallback />} />
<Route path="/google/callback" element={<GoogleCallback />} />
<Route path="/mcp/oauth/callback" element={<MCPOAuthCallback />} />
</Routes>
);
}
return (
<>
<div className="app">
<Header />
<main className="app-main">
<Routes>
<Route path="/" element={<Home />} />
<Route path="/board/:boardId" element={<Board />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</main>
</div>
<CommandPalette
isOpen={paletteOpen}
onClose={() => setPaletteOpen(false)}
onNewTask={handleNewTask}
/>
{/* Toast notifications */}
<WorkflowToastListener />
<ToastContainer />
</>
);
}
function App() {
return (
<ErrorBoundary>
<BrowserRouter>
<AuthProvider>
<ToastProvider>
<AuthGate>
<BoardProvider>
<AppContent />
</BoardProvider>
</AuthGate>
</ToastProvider>
</AuthProvider>
</BrowserRouter>
</ErrorBoundary>
);
}
export default App;
================================================
FILE: src/api/client.ts
================================================
import type {
Board,
Column,
Task,
ApiResponse,
TaskPriority,
BoardCredential,
MCPServer,
MCPTool,
WorkflowPlan,
WorkflowLog,
User,
ScheduleConfig,
ScheduledRun,
} from '../types';
const API_BASE = '/api';
// ============================================
// AUTH
// ============================================
export async function getMe(): Promise<ApiResponse<User>> {
const response = await fetch(`${API_BASE}/me`);
return response.json();
}
async function request<T>(
path: string,
options: RequestInit = {}
): Promise<ApiResponse<T>> {
const response = await fetch(`${API_BASE}${path}`, {
headers: {
'Content-Type': 'application/json',
...options.headers,
},
...options,
});
const data = await response.json();
if (!response.ok) {
// Server returns { success: false, error: { code, message } }
const errorObj = data.error || {};
return {
success: false,
error: {
code: errorObj.code || String(response.status),
message: errorObj.message || 'Request failed',
},
};
}
return data;
}
// ============================================
// BOARDS
// ============================================
export interface BoardWithDetails extends Board {
columns: Column[];
tasks: Task[];
}
export async function getBoards(): Promise<ApiResponse<Board[]>> {
return request<Board[]>('/boards');
}
export async function getBoard(id: string): Promise<ApiResponse<BoardWithDetails>> {
return request<BoardWithDetails>(`/boards/${id}`);
}
export async function createBoard(name: string): Promise<ApiResponse<BoardWithDetails>> {
return request<BoardWithDetails>('/boards', {
method: 'POST',
body: JSON.stringify({ name }),
});
}
export async function updateBoard(
id: string,
data: { name?: string }
): Promise<ApiResponse<BoardWithDetails>> {
return request<BoardWithDetails>(`/boards/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteBoard(id: string): Promise<ApiResponse<void>> {
return request<void>(`/boards/${id}`, {
method: 'DELETE',
});
}
// ============================================
// COLUMNS
// ============================================
export async function createColumn(
boardId: string,
name: string
): Promise<ApiResponse<Column>> {
return request<Column>(`/boards/${boardId}/columns`, {
method: 'POST',
body: JSON.stringify({ name }),
});
}
export async function updateColumn(
boardId: string,
id: string,
data: { name?: string; position?: number }
): Promise<ApiResponse<Column>> {
return request<Column>(`/boards/${boardId}/columns/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteColumn(boardId: string, id: string): Promise<ApiResponse<void>> {
return request<void>(`/boards/${boardId}/columns/${id}`, {
method: 'DELETE',
});
}
// ============================================
// TASKS
// ============================================
export async function createTask(
boardId: string,
data: {
columnId: string;
title: string;
description?: string;
priority?: TaskPriority;
}
): Promise<ApiResponse<Task>> {
return request<Task>(`/boards/${boardId}/tasks`, {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function getTask(boardId: string, id: string): Promise<ApiResponse<Task>> {
return request<Task>(`/boards/${boardId}/tasks/${id}`);
}
export async function updateTask(
boardId: string,
id: string,
data: {
title?: string;
description?: string;
priority?: TaskPriority;
scheduleConfig?: ScheduleConfig | null;
}
): Promise<ApiResponse<Task>> {
return request<Task>(`/boards/${boardId}/tasks/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteTask(boardId: string, id: string): Promise<ApiResponse<void>> {
return request<void>(`/boards/${boardId}/tasks/${id}`, {
method: 'DELETE',
});
}
export async function moveTask(
boardId: string,
id: string,
columnId: string,
position: number
): Promise<ApiResponse<Task>> {
return request<Task>(`/boards/${boardId}/tasks/${id}/move`, {
method: 'POST',
body: JSON.stringify({ columnId, position }),
});
}
// ============================================
// CREDENTIALS
// ============================================
export async function getCredentials(
boardId: string
): Promise<ApiResponse<BoardCredential[]>> {
return request<BoardCredential[]>(`/boards/${boardId}/credentials`);
}
export async function createCredential(
boardId: string,
data: {
type: 'github_oauth' | 'google_oauth' | 'anthropic_api_key';
name: string;
value: string;
metadata?: Record<string, unknown>;
}
): Promise<ApiResponse<BoardCredential>> {
return request<BoardCredential>(`/boards/${boardId}/credentials`, {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function deleteCredential(
boardId: string,
credentialId: string
): Promise<ApiResponse<void>> {
return request<void>(`/boards/${boardId}/credentials/${credentialId}`, {
method: 'DELETE',
});
}
// ============================================
// GITHUB
// ============================================
export interface GitHubRepo {
id: number;
name: string;
fullName: string;
owner: string;
private: boolean;
defaultBranch: string;
description: string | null;
}
export async function getGitHubOAuthUrl(
boardId: string
): Promise<ApiResponse<{ url: string }>> {
return request<{ url: string }>(`/github/oauth/url?boardId=${encodeURIComponent(boardId)}`);
}
export async function getGitHubRepos(
boardId: string
): Promise<ApiResponse<GitHubRepo[]>> {
return request<GitHubRepo[]>(`/boards/${boardId}/github/repos`);
}
// ============================================
// GOOGLE
// ============================================
export async function getGoogleOAuthUrl(
boardId: string
): Promise<ApiResponse<{ url: string }>> {
return request<{ url: string }>(`/google/oauth/url?boardId=${encodeURIComponent(boardId)}`);
}
// ============================================
// MCP SERVERS
// ============================================
export async function getMCPServers(
boardId: string
): Promise<ApiResponse<MCPServer[]>> {
return request<MCPServer[]>(`/boards/${boardId}/mcp-servers`);
}
export async function getMCPServer(
boardId: string,
serverId: string
): Promise<ApiResponse<MCPServer>> {
return request<MCPServer>(`/boards/${boardId}/mcp-servers/${serverId}`);
}
export async function createMCPServer(
boardId: string,
data: {
name: string;
type: 'remote' | 'hosted';
endpoint?: string;
authType?: 'none' | 'oauth' | 'api_key' | 'bearer';
credentialId?: string;
transportType?: 'streamable-http' | 'sse';
}
): Promise<ApiResponse<MCPServer>> {
return request<MCPServer>(`/boards/${boardId}/mcp-servers`, {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function updateMCPServer(
boardId: string,
serverId: string,
data: {
name?: string;
endpoint?: string;
authType?: 'none' | 'oauth' | 'api_key' | 'bearer';
credentialId?: string;
transportType?: 'streamable-http' | 'sse';
enabled?: boolean;
status?: 'connected' | 'disconnected' | 'error';
}
): Promise<ApiResponse<MCPServer>> {
return request<MCPServer>(`/boards/${boardId}/mcp-servers/${serverId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteMCPServer(
boardId: string,
serverId: string
): Promise<ApiResponse<void>> {
return request<void>(`/boards/${boardId}/mcp-servers/${serverId}`, {
method: 'DELETE',
});
}
export async function getMCPServerTools(
boardId: string,
serverId: string
): Promise<ApiResponse<MCPTool[]>> {
return request<MCPTool[]>(`/boards/${boardId}/mcp-servers/${serverId}/tools`);
}
export async function connectMCPServer(
boardId: string,
serverId: string
): Promise<ApiResponse<{ status: string; toolCount: number; tools: Array<{ name: string; description?: string }> }>> {
return request<{ status: string; toolCount: number; tools: Array<{ name: string; description?: string }> }>(
`/boards/${boardId}/mcp-servers/${serverId}/connect`,
{ method: 'POST' }
);
}
export async function cacheMCPServerTools(
boardId: string,
serverId: string,
tools: Array<{
name: string;
description?: string;
inputSchema: object;
}>
): Promise<ApiResponse<MCPTool[]>> {
return request<MCPTool[]>(`/boards/${boardId}/mcp-servers/${serverId}/tools`, {
method: 'PUT',
body: JSON.stringify({ tools }),
});
}
/**
* Create an account-based MCP server (e.g., Gmail, Google Docs)
* Uses the AccountMCPRegistry to create and initialize the MCP
*/
export async function createAccountMCP(
boardId: string,
accountId: string,
mcpId: string
): Promise<ApiResponse<MCPServer>> {
return request<MCPServer>(`/boards/${boardId}/mcp-servers/account`, {
method: 'POST',
body: JSON.stringify({ accountId, mcpId }),
});
}
// ============================================
// MCP OAUTH
// ============================================
/**
* Discover OAuth endpoints for a remote MCP server
*/
export async function discoverMCPOAuth(
boardId: string,
serverId: string
): Promise<ApiResponse<{
resource: string;
authorizationServer: string;
authorizationEndpoint: string;
tokenEndpoint: string;
scopesSupported?: string[];
}>> {
return request(`/boards/${boardId}/mcp-servers/${serverId}/oauth/discover`, {
method: 'POST',
});
}
/**
* Get OAuth authorization URL for a remote MCP server
*/
export async function getMCPOAuthUrl(
boardId: string,
serverId: string,
redirectUri: string
): Promise<ApiResponse<{ url: string; state: string }>> {
const params = new URLSearchParams({ redirectUri });
return request(`/boards/${boardId}/mcp-servers/${serverId}/oauth/url?${params.toString()}`);
}
/**
* Exchange OAuth authorization code for tokens
*/
export async function exchangeMCPOAuthCode(
boardId: string,
serverId: string,
code: string,
state: string,
redirectUri: string
): Promise<ApiResponse<{ status: string; credentialId: string }>> {
return request(`/boards/${boardId}/mcp-servers/${serverId}/oauth/exchange`, {
method: 'POST',
body: JSON.stringify({ code, state, redirectUri }),
});
}
// ============================================
// WORKFLOW PLANS
// ============================================
export async function getBoardWorkflowPlans(
boardId: string
): Promise<ApiResponse<WorkflowPlan[]>> {
return request<WorkflowPlan[]>(`/boards/${boardId}/workflow-plans`);
}
export async function getTaskWorkflowPlan(
boardId: string,
taskId: string
): Promise<ApiResponse<WorkflowPlan | null>> {
return request<WorkflowPlan | null>(`/boards/${boardId}/tasks/${taskId}/plan`);
}
export async function createWorkflowPlan(
boardId: string,
taskId: string,
data: {
summary?: string;
generatedCode?: string;
steps?: object[];
}
): Promise<ApiResponse<WorkflowPlan>> {
return request<WorkflowPlan>(`/boards/${boardId}/tasks/${taskId}/plan`, {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function getWorkflowPlan(
boardId: string,
planId: string
): Promise<ApiResponse<WorkflowPlan>> {
return request<WorkflowPlan>(`/boards/${boardId}/plans/${planId}`);
}
export async function updateWorkflowPlan(
boardId: string,
planId: string,
data: {
status?: string;
summary?: string;
generatedCode?: string;
steps?: object[];
currentStepIndex?: number;
checkpointData?: object;
}
): Promise<ApiResponse<WorkflowPlan>> {
return request<WorkflowPlan>(`/boards/${boardId}/plans/${planId}`, {
method: 'PUT',
body: JSON.stringify(data),
});
}
export async function deleteWorkflowPlan(
boardId: string,
planId: string
): Promise<ApiResponse<void>> {
return request<void>(`/boards/${boardId}/plans/${planId}`, {
method: 'DELETE',
});
}
export async function approveWorkflowPlan(
boardId: string,
planId: string
): Promise<ApiResponse<WorkflowPlan>> {
return request<WorkflowPlan>(`/boards/${boardId}/plans/${planId}/approve`, {
method: 'POST',
});
}
export async function cancelWorkflow(
boardId: string,
planId: string
): Promise<ApiResponse<WorkflowPlan>> {
return request<WorkflowPlan>(`/boards/${boardId}/plans/${planId}/cancel`, {
method: 'POST',
});
}
export async function resolveWorkflowCheckpoint(
boardId: string,
planId: string,
data: {
action: 'approve' | 'request_changes' | 'cancel';
data?: object;
feedback?: string;
}
): Promise<ApiResponse<WorkflowPlan>> {
return request<WorkflowPlan>(`/boards/${boardId}/plans/${planId}/checkpoint`, {
method: 'POST',
body: JSON.stringify(data),
});
}
export async function getWorkflowLogs(
boardId: string,
planId: string,
options?: { limit?: number; offset?: number }
): Promise<ApiResponse<WorkflowLog[]>> {
const params = new URLSearchParams();
if (options?.limit) params.set('limit', String(options.limit));
if (options?.offset) params.set('offset', String(options.offset));
const query = params.toString() ? `?${params.toString()}` : '';
return request<WorkflowLog[]>(`/boards/${boardId}/plans/${planId}/logs${query}`);
}
export async function generateWorkflowPlan(
boardId: string,
taskId: string
): Promise<ApiResponse<WorkflowPlan>> {
return request<WorkflowPlan>(`/boards/${boardId}/tasks/${taskId}/generate-plan`, {
method: 'POST',
});
}
// ============================================
// LINK METADATA (for link pills)
// ============================================
export interface LinkMetadata {
type: 'google_doc' | 'google_sheet' | 'github_pr' | 'github_issue' | 'github_repo';
title: string;
id: string;
}
export async function getLinkMetadata(
boardId: string,
url: string
): Promise<ApiResponse<LinkMetadata | null>> {
return request<LinkMetadata | null>(`/boards/${boardId}/links/metadata`, {
method: 'POST',
body: JSON.stringify({ url }),
});
}
// ============================================
// SCHEDULED TASKS
// ============================================
/**
* Get scheduled tasks for a board
*/
export async function getScheduledTasks(
boardId: string
): Promise<ApiResponse<Task[]>> {
return request<Task[]>(`/boards/${boardId}/scheduled-tasks`);
}
/**
* Get scheduled runs for a task
*/
export async function getScheduledRuns(
boardId: string,
taskId: string,
limit?: number
): Promise<ApiResponse<ScheduledRun[]>> {
const params = limit ? `?limit=${limit}` : '';
return request<ScheduledRun[]>(`/boards/${boardId}/tasks/${taskId}/runs${params}`);
}
/**
* Get child tasks for a scheduled run
*/
export async function getRunTasks(
boardId: string,
runId: string
): Promise<ApiResponse<Task[]>> {
return request<Task[]>(`/boards/${boardId}/runs/${runId}/tasks`);
}
/**
* Delete a scheduled run from history
*/
export async function deleteScheduledRun(
boardId: string,
runId: string
): Promise<ApiResponse<void>> {
return request<void>(`/boards/${boardId}/runs/${runId}`, {
method: 'DELETE',
});
}
/**
* Get child tasks for a parent scheduled task
*/
export async function getChildTasks(
boardId: string,
parentTaskId: string
): Promise<ApiResponse<Task[]>> {
return request<Task[]>(`/boards/${boardId}/tasks/${parentTaskId}/children`);
}
/**
* Trigger a scheduled run manually ("Run Now")
*/
export async function triggerScheduledRun(
boardId: string,
taskId: string
): Promise<ApiResponse<{ runId: string }>> {
return request<{ runId: string }>(`/boards/${boardId}/tasks/${taskId}/run`, {
method: 'POST',
});
}
================================================
FILE: src/components/Approval/Approval.css
================================================
/**
* Approval View Styles
*
* Shared styles for all approval view components.
* These styles are used by both DefaultApproval and specialized views.
*/
/* Base approval card */
.approval-card {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
/* Header with icon and action title */
.approval-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding-bottom: var(--space-3);
border-bottom: 1px solid var(--color-border-default);
}
.approval-icon {
flex-shrink: 0;
color: var(--color-text-secondary);
}
.approval-title {
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
/* Field display (key-value pairs) */
.approval-fields {
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.approval-field {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.approval-field-label {
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
}
.approval-field-value {
padding: var(--space-3);
background: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
font-size: 13px;
line-height: 1.5;
color: var(--color-text-primary);
white-space: pre-wrap;
word-break: break-word;
max-height: 200px;
overflow-y: auto;
}
/* Actions (approve/reject buttons) */
.approval-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
padding-top: var(--space-4);
border-top: 1px solid var(--color-border-default);
}
/* Generic label styling */
.approval-label {
font-size: 12px;
font-weight: 500;
color: var(--color-text-muted);
margin-right: var(--space-2);
}
/* ============================================
* GitHub Write File Approval Styles
* ============================================ */
.approval-github-write .approval-file-info {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.approval-github-write .approval-file-path {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--color-success-subtle);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
}
.approval-github-write .approval-file-path-icon {
color: var(--color-success-text);
font-weight: 600;
font-size: 14px;
}
.approval-github-write .approval-file-path code {
font-family: var(--font-mono);
font-size: 13px;
color: var(--color-text-primary);
}
.approval-github-write .approval-file-branch,
.approval-github-write .approval-file-message {
display: flex;
align-items: baseline;
gap: var(--space-2);
padding: var(--space-1) 0;
font-size: 13px;
}
.approval-github-write .approval-file-branch code {
font-family: var(--font-mono);
padding: var(--space-1) var(--space-2);
background: var(--color-bg-secondary);
border-radius: var(--radius-sm);
font-size: 12px;
}
/* Code preview section */
.approval-code-preview {
display: flex;
flex-direction: column;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
overflow: hidden;
}
.approval-code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-2) var(--space-3);
background: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border-default);
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
}
.approval-code-lines {
font-size: 11px;
color: var(--color-text-muted);
font-weight: normal;
}
.approval-code-content {
max-height: 400px;
overflow: auto;
background: var(--color-bg-primary);
}
.approval-code-content pre {
margin: 0;
padding: var(--space-3);
}
.approval-code-content code {
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
color: var(--color-text-primary);
white-space: pre;
tab-size: 2;
}
/* ============================================
* Short Field Comments
* ============================================ */
.approval-field-short {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
/* Editable field (input with comment support) */
.approval-field-editable {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.approval-field-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
}
/* Editable field label - fixed width, left-aligned */
.approval-field-editable .approval-field-label {
width: 60px;
flex-shrink: 0;
text-align: left;
}
/* Editable field input */
.approval-field-input {
flex: 1;
padding: var(--space-2);
background: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-size: 13px;
color: var(--color-text-primary);
}
.approval-field-input:focus {
outline: none;
border-color: var(--color-accent-primary);
}
.approval-field-input::placeholder {
color: var(--color-text-muted);
}
.approval-field-input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Show add comment on hover for editable fields */
.approval-field-editable:hover .approval-field-add-comment {
opacity: 1;
}
.approval-field-add-comment {
padding: 2px 8px;
background: none;
border: none;
border-radius: var(--radius-sm);
font-size: 11px;
color: var(--color-text-muted);
cursor: pointer;
opacity: 0;
transition: all 0.15s ease;
}
.approval-field-short:hover .approval-field-add-comment {
opacity: 1;
}
.approval-field-add-comment:hover {
color: var(--color-accent-primary);
background: var(--color-bg-secondary);
}
/* Field comment input */
.approval-field-comment-input {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-2);
background: var(--color-bg-secondary);
border: 1px solid var(--color-accent-primary);
border-radius: var(--radius-md);
}
.approval-field-comment-input input {
width: 100%;
padding: var(--space-2);
background: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
font-family: inherit;
font-size: 13px;
color: var(--color-text-primary);
}
.approval-field-comment-input input:focus {
outline: none;
border-color: var(--color-accent-primary);
}
.approval-field-comment-input input::placeholder {
color: var(--color-text-muted);
}
.approval-field-comment-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
}
.approval-field-comment-actions button {
padding: 4px 10px;
border: none;
border-radius: var(--radius-sm);
font-size: 12px;
cursor: pointer;
background: transparent;
color: var(--color-text-muted);
}
.approval-field-comment-actions button:hover:not(:disabled) {
color: var(--color-text-primary);
}
.approval-field-comment-actions button.primary {
background: var(--color-accent-primary);
color: white;
}
.approval-field-comment-actions button.primary:hover:not(:disabled) {
opacity: 0.9;
}
.approval-field-comment-actions button:disabled {
opacity: 0.4;
cursor: default;
}
/* Existing field comment */
.approval-field-comment {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-default);
border-left: 3px solid #3b82f6;
border-radius: var(--radius-sm);
font-size: 12px;
}
.approval-field-comment-content {
flex: 1;
color: var(--color-text-primary);
}
.approval-field-comment-edit,
.approval-field-comment-remove {
padding: 2px 6px;
border: none;
border-radius: var(--radius-sm);
background: transparent;
font-size: 11px;
color: var(--color-text-muted);
cursor: pointer;
opacity: 0;
transition: all 0.15s ease;
}
.approval-field-comment:hover .approval-field-comment-edit,
.approval-field-comment:hover .approval-field-comment-remove {
opacity: 1;
}
.approval-field-comment-edit:hover {
color: var(--color-accent-primary);
background: var(--color-bg-tertiary);
}
.approval-field-comment-remove:hover {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
/* ============================================
* Approval Footer (shared component)
* ============================================ */
.approval-footer {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding-top: var(--space-3);
border-top: 1px solid var(--color-border-default);
}
/* Footer actions */
.approval-footer-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.approval-footer-primary-actions {
display: flex;
gap: var(--space-2);
}
/* ============================================
Mobile Styles
============================================ */
@media (max-width: 767px) {
.approval-card {
gap: var(--space-3);
}
.approval-header {
padding-bottom: var(--space-2);
}
.approval-title {
font-size: 14px;
}
.approval-fields {
gap: var(--space-3);
}
.approval-field-value {
max-height: 150px;
font-size: 14px;
}
/* Main approval actions - stack with primary on top */
.approval-actions {
flex-direction: column-reverse;
gap: var(--space-2);
}
.approval-actions > * {
width: 100%;
}
/* Footer actions - stack */
.approval-footer-actions {
flex-direction: column-reverse;
gap: var(--space-3);
}
.approval-footer-primary-actions {
flex-direction: column-reverse;
width: 100%;
gap: var(--space-2);
}
.approval-footer-primary-actions > * {
width: 100%;
}
/* Code preview */
.approval-code-content {
max-height: 250px;
}
.approval-code-content code {
font-size: 11px;
}
/* Always show comment buttons on mobile */
.approval-field-add-comment {
opacity: 1;
}
.approval-field-comment-edit,
.approval-field-comment-remove {
opacity: 1;
}
/* Field input */
.approval-field-input {
font-size: 16px; /* Prevent iOS zoom */
}
.approval-field-comment-input input {
font-size: 16px;
}
}
================================================
FILE: src/components/Approval/ApprovalFooter.tsx
================================================
/**
* Approval Footer
*
* Shared footer for all approval views with three actions:
* - Cancel (abort workflow)
* - Request Changes (send feedback, agent iterates)
* - Approve (accept as-is)
*
* Feedback is now collected inline on fields, not in a general textarea.
*/
import { Button } from '../common';
interface ApprovalFooterProps {
onApprove: () => void;
onRequestChanges: () => void;
onCancel: () => void;
isLoading: boolean;
approveLabel?: string;
approveDisabled?: boolean;
commentCount?: number;
requestChangesLabel?: string;
requestChangesDisabled?: boolean;
requestChangesDisabledTitle?: string;
}
export function ApprovalFooter({
onApprove,
onRequestChanges,
onCancel,
isLoading,
approveLabel = 'Approve',
approveDisabled = false,
commentCount = 0,
requestChangesLabel,
requestChangesDisabled,
requestChangesDisabledTitle,
}: ApprovalFooterProps) {
const hasComments = commentCount > 0;
const computedRequestChangesLabel = requestChangesLabel || (hasComments
? `Request Changes (${commentCount})`
: 'Request Changes');
const isRequestChangesDisabled = requestChangesDisabled ?? !hasComments;
const requestChangesTitle = requestChangesDisabledTitle ?? (!hasComments ? 'Add comments to request changes' : undefined);
return (
<div className="approval-footer">
<div className="approval-footer-actions">
<Button
variant="ghost"
onClick={onCancel}
disabled={isLoading}
>
Reject
</Button>
<div className="approval-footer-primary-actions">
<Button
variant="default"
onClick={onRequestChanges}
disabled={isLoading || isRequestChangesDisabled}
title={requestChangesTitle}
>
{isLoading ? 'Sending...' : computedRequestChangesLabel}
</Button>
<Button
variant="primary"
onClick={onApprove}
disabled={approveDisabled || isLoading}
>
{isLoading ? 'Processing...' : approveLabel}
</Button>
</div>
</div>
</div>
);
}
================================================
FILE: src/components/Approval/ApprovalViewRegistry.tsx
================================================
/**
* Approval View Registry
*
* Maps MCP tool names to specialized approval view components.
* This allows different tools to have tool-specific approval UIs.
*/
import { DefaultApproval } from './DefaultApproval';
import { EmailApproval } from './EmailApproval';
import { GitHubPRApproval } from './GitHubPRApproval';
import { GitHubPRReviewApproval } from './GitHubPRReviewApproval';
import { GoogleDocsApproval } from './GoogleDocsApproval';
import { GoogleSheetsApproval } from './GoogleSheetsApproval';
/**
* Props passed to all approval view components
*/
export interface ApprovalViewProps {
tool: string;
action: string;
data: Record<string, unknown>;
toolResults?: Record<string, unknown>;
onApprove: (responseData?: Record<string, unknown>) => void;
onRequestChanges: (feedback: string) => void;
onCancel: () => void;
isLoading: boolean;
}
type ApprovalViewComponent = React.FC<ApprovalViewProps>;
/**
* Registry mapping tool names to their approval view components
*/
const APPROVAL_VIEW_REGISTRY: Record<string, ApprovalViewComponent> = {
'GitHub__createPullRequest': GitHubPRApproval,
'GitHub__create_pr': GitHubPRApproval,
'Sandbox__createPullRequest': GitHubPRApproval,
'GitHub__submitPullRequestReview': GitHubPRReviewApproval,
'GitHub__submit_pr_review': GitHubPRReviewApproval,
'Gmail__sendEmail': EmailApproval,
'Gmail__createDraft': EmailApproval,
'Google_Docs__createDocument': GoogleDocsApproval,
'Google_Docs__appendToDocument': GoogleDocsApproval,
'Google_Docs__replaceDocumentContent': GoogleDocsApproval,
'Google_Sheets__createSpreadsheet': GoogleSheetsApproval,
'Google_Sheets__appendRows': GoogleSheetsApproval,
'Google_Sheets__updateCells': GoogleSheetsApproval,
'Google_Sheets__replaceSheetContent': GoogleSheetsApproval,
};
/**
* Get the approval view component for a given tool name
* Falls back to DefaultApproval if no specialized view exists
*/
export function getApprovalView(toolName: string): ApprovalViewComponent {
return APPROVAL_VIEW_REGISTRY[toolName] || DefaultApproval;
}
================================================
FILE: src/components/Approval/DefaultApproval.tsx
================================================
/**
* Default Approval View
*
* Generic key-value display for tool approvals.
* Uses CommentableText for multi-line text fields to allow inline comments.
* Short fields get a simple "add comment" button on hover.
* Used as fallback when no specialized view exists.
*/
import { useState } from 'react';
import { McpIcon, getIconTypeFromTool } from '../common';
import { ApprovalFooter } from './ApprovalFooter';
import { CommentableText, formatTextComments } from '../CommentableText/CommentableText';
import { useFieldComments } from '../../hooks';
import type { TextComment } from '../../types';
import type { ApprovalViewProps } from './ApprovalViewRegistry';
import './Approval.css';
// Tool-specific configuration for approval UI
const TOOL_CONFIG: Record<string, {
label: string;
buttonLabel: string;
fieldLabels: Record<string, string>;
commentableFields?: string[]; // Fields that support line-level comments
editableFields?: string[]; // Fields that are editable (with comment support)
}> = {
'Google_Docs__createDocument': {
label: 'Create Document',
buttonLabel: 'Create Document',
fieldLabels: { title: 'Document Name', content: 'Content' },
commentableFields: ['content'],
editableFields: ['title'],
},
'Gmail__sendMessage': {
label: 'Send Email',
buttonLabel: 'Send Email',
fieldLabels: { to: 'To', subject: 'Subject', body: 'Message', cc: 'CC', bcc: 'BCC' },
commentableFields: ['body'],
},
'Gmail__createDraft': {
label: 'Create Draft',
buttonLabel: 'Save Draft',
fieldLabels: { to: 'To', subject: 'Subject', body: 'Message', cc: 'CC', bcc: 'BCC' },
commentableFields: ['body'],
},
'Sandbox__runCode': {
label: 'Run Code',
buttonLabel: 'Run Code',
fieldLabels: { code: 'Code', language: 'Language' },
commentableFields: ['code'],
},
};
// Get tool config with fallback to generic labels
function getToolConfig(toolName: string) {
return TOOL_CONFIG[toolName] || {
label: 'Execute Action',
buttonLabel: 'Continue',
fieldLabels: {},
};
}
// Get field label with tool-specific mapping
function getFieldLabel(toolName: string, fieldKey: string): string {
const config = TOOL_CONFIG[toolName];
if (config?.fieldLabels[fieldKey]) {
return config.fieldLabels[fieldKey];
}
// Fallback: convert camelCase to Title Case
return fieldKey
.replace(/([A-Z])/g, ' $1')
.replace(/_/g, ' ')
.trim()
.replace(/^\w/, c => c.toUpperCase());
}
// Text comments state keyed by field name (for multi-line fields)
interface TextCommentsMap {
[fieldKey: string]: TextComment[];
}
export function DefaultApproval({
tool,
action,
data,
onApprove,
onRequestChanges,
onCancel,
isLoading,
}: ApprovalViewProps) {
const toolConfig = getToolConfig(tool);
const iconType = getIconTypeFromTool(tool);
// Text comments for multi-line fields (CommentableText)
const [textComments, setTextComments] = useState<TextCommentsMap>({});
// Field comments using shared hook
const {
commentingField,
commentInput,
setCommentInput,
startFieldComment,
editFieldComment,
submitFieldComment,
cancelFieldComment,
removeFieldComment,
getFieldComment,
commentCount: fieldCommentCount,
} = useFieldComments();
// Edited field values (for editable fields)
const [editedValues, setEditedValues] = useState<Record<string, string>>({});
// Wrap hook functions to match expected signatures
const handleStartFieldComment = (fieldKey: string) => startFieldComment(fieldKey);
const handleEditFieldComment = (fieldKey: string, content: string) => editFieldComment(fieldKey, content);
const handleSubmitFieldComment = (fieldKey: string, fieldLabel: string) => submitFieldComment(fieldKey, fieldLabel);
const handleCancelFieldComment = () => cancelFieldComment();
const handleRemoveFieldComment = (fieldKey: string) => removeFieldComment(fieldKey);
// Extract fields to display from data
const fields: Array<{ key: string; label: string; value: string; isCommentable: boolean; isEditable: boolean }> = [];
// Handle case where data might be a JSON string instead of object
let dataObj = data;
if (typeof dataObj === 'string') {
try {
dataObj = JSON.parse(dataObj);
} catch {
dataObj = {};
}
}
const commentableFields = toolConfig.commentableFields || [];
const editableFields = toolConfig.editableFields || [];
if (dataObj && typeof dataObj === 'object') {
Object.entries(dataObj as Record<string, unknown>).forEach(([key, value]) => {
// Convert non-string values to readable strings
const displayValue = typeof value === 'string'
? value
: typeof value === 'object'
? JSON.stringify(value, null, 2)
: String(value);
if (displayValue.length > 0) {
fields.push({
key,
label: getFieldLabel(tool, key),
value: displayValue,
isCommentable: commentableFields.includes(key),
isEditable: editableFields.includes(key),
});
}
});
}
// Handler for editable field changes
const handleEditableChange = (fieldKey: string, newValue: string) => {
setEditedValues((prev) => ({ ...prev, [fieldKey]: newValue }));
};
// Get current value for an editable field
const getEditableValue = (field: { key: string; value: string }) => {
return editedValues[field.key] ?? field.value;
};
// Text comment handlers (for multi-line fields)
const handleAddTextComment = (fieldKey: string, comment: Omit<TextComment, 'id'>) => {
const newComment: TextComment = {
...comment,
id: `comment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
};
setTextComments((prev) => ({
...prev,
[fieldKey]: [...(prev[fieldKey] || []), newComment],
}));
};
const handleRemoveTextComment = (fieldKey: string, commentId: string) => {
setTextComments((prev) => ({
...prev,
[fieldKey]: (prev[fieldKey] || []).filter((c) => c.id !== commentId),
}));
};
// Get total comment count
const textCommentCount = Object.values(textComments).reduce((sum, arr) => sum + arr.length, 0);
const totalComments = textCommentCount + fieldCommentCount;
// Format all comments as feedback
const formatAllFeedback = (): string => {
const sections: string[] = [];
for (const field of fields) {
if (field.isCommentable) {
// Multi-line field - format text comments
const comments = textComments[field.key] || [];
if (comments.length > 0) {
sections.push(`[${field.label}]\n${formatTextComments(comments)}`);
}
} else {
// Short field - format field comment
const comment = getFieldComment(field.key);
if (comment) {
sections.push(`[${field.label}]: "${comment.content}"`);
}
}
}
return sections.join('\n\n');
};
const handleRequestChanges = () => {
const feedback = formatAllFeedback();
onRequestChanges(feedback);
};
return (
<div className="approval-card">
{/* Header with icon and action */}
<div className="approval-header">
<McpIcon type={iconType} size={24} className="approval-icon" />
<span className="approval-title">
{action || toolConfig.label}
</span>
</div>
{/* Fields */}
<div className="approval-fields">
{fields.map((field) => {
const existingComment = getFieldComment(field.key);
const isCommenting = commentingField === field.key;
return (
<div key={field.key} className="approval-field">
{field.isCommentable ? (
<CommentableText
content={field.value}
label={field.label}
comments={textComments[field.key] || []}
onAddComment={(comment) => handleAddTextComment(field.key, comment)}
onRemoveComment={(id) => handleRemoveTextComment(field.key, id)}
disabled={isLoading}
/>
) : field.isEditable ? (
/* Editable field with input + comment support */
<div className="approval-field-editable">
<div className="approval-field-header">
<label className="approval-field-label">{field.label}:</label>
<input
type="text"
className="approval-field-input"
value={getEditableValue(field)}
onChange={(e) => handleEditableChange(field.key, e.target.value)}
disabled={isLoading}
placeholder={`Enter ${field.label.toLowerCase()}...`}
/>
{!isCommenting && !existingComment && !isLoading && (
<button
className="approval-field-add-comment"
onClick={() => handleStartFieldComment(field.key)}
title="Add comment"
>
+ comment
</button>
)}
</div>
{/* Comment input */}
{isCommenting && (
<div className="approval-field-comment-input">
<input
type="text"
value={commentInput}
onChange={(e) => setCommentInput(e.target.value)}
placeholder="Add your feedback..."
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && commentInput.trim()) {
handleSubmitFieldComment(field.key, field.label);
} else if (e.key === 'Escape') {
handleCancelFieldComment();
}
}}
/>
<div className="approval-field-comment-actions">
<button onClick={handleCancelFieldComment}>Cancel</button>
<button
onClick={() => handleSubmitFieldComment(field.key, field.label)}
disabled={!commentInput.trim()}
className="primary"
>
Add Comment
</button>
</div>
</div>
)}
{/* Existing comment */}
{existingComment && !isCommenting && (
<div className="approval-field-comment">
<span className="approval-field-comment-content">{existingComment.content}</span>
<button
className="approval-field-comment-edit"
onClick={() => handleEditFieldComment(field.key, existingComment.content)}
title="Edit comment"
>
Edit
</button>
<button
className="approval-field-comment-remove"
onClick={() => handleRemoveFieldComment(field.key)}
title="Remove comment"
>
×
</button>
</div>
)}
</div>
) : (
/* Read-only field with comment support */
<div className="approval-field-short">
<div className="approval-field-header">
<label className="approval-field-label">{field.label}</label>
{!isCommenting && !existingComment && !isLoading && (
<button
className="approval-field-add-comment"
onClick={() => handleStartFieldComment(field.key)}
title="Add comment"
>
+ comment
</button>
)}
</div>
<div className="approval-field-value">{field.value}</div>
{/* Comment input */}
{isCommenting && (
<div className="approval-field-comment-input">
<input
type="text"
value={commentInput}
onChange={(e) => setCommentInput(e.target.value)}
placeholder="Add your feedback..."
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && commentInput.trim()) {
handleSubmitFieldComment(field.key, field.label);
} else if (e.key === 'Escape') {
handleCancelFieldComment();
}
}}
/>
<div className="approval-field-comment-actions">
<button onClick={handleCancelFieldComment}>Cancel</button>
<button
onClick={() => handleSubmitFieldComment(field.key, field.label)}
disabled={!commentInput.trim()}
className="primary"
>
Add Comment
</button>
</div>
</div>
)}
{/* Existing comment */}
{existingComment && !isCommenting && (
<div className="approval-field-comment">
<span className="approval-field-comment-content">{existingComment.content}</span>
<button
className="approval-field-comment-edit"
onClick={() => handleEditFieldComment(field.key, existingComment.content)}
title="Edit comment"
>
Edit
</button>
<button
className="approval-field-comment-remove"
onClick={() => handleRemoveFieldComment(field.key)}
title="Remove comment"
>
×
</button>
</div>
)}
</div>
)}
</div>
);
})}
</div>
{/* Footer with actions */}
<ApprovalFooter
onApprove={() => {
// Pass back any edited field values
if (Object.keys(editedValues).length > 0) {
onApprove(editedValues);
} else {
onApprove();
}
}}
onRequestChanges={handleRequestChanges}
onCancel={onCancel}
isLoading={isLoading}
approveLabel={toolConfig.buttonLabel}
commentCount={totalComments}
/>
</div>
);
}
================================================
FILE: src/components/Approval/EmailApproval.css
================================================
/**
* Email Approval View Styles
*
* Email-client style layout for Gmail send/draft approvals.
*/
.email-approval-view {
display: flex;
flex-direction: column;
height: 100%;
min-height: 400px;
}
/* Header */
.email-approval-header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-default);
background: var(--color-bg-secondary);
}
.email-approval-header h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
/* Recipients Section */
.email-approval-recipients {
display: flex;
flex-direction: column;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-default);
background: var(--color-bg-primary);
}
.email-recipient-field {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.email-recipient-field + .email-recipient-field {
margin-top: var(--space-2);
}
.email-recipient-row {
display: flex;
align-items: center;
gap: var(--space-2);
min-height: 28px;
}
.email-recipient-label {
width: 60px;
flex-shrink: 0;
font-size: 12px;
font-weight: 500;
color: var(--color-text-muted);
text-align: left;
}
.email-recipient-input {
flex: 1;
padding: var(--space-1) var(--space-2);
font-size: 13px;
font-family: var(--font-mono);
color: var(--color-text-primary);
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
transition: border-color 0.15s, background 0.15s;
}
.email-recipient-input:hover {
background: var(--color-bg-secondary);
border-color: var(--color-border-default);
}
.email-recipient-input:focus {
outline: none;
background: var(--color-bg-primary);
border-color: var(--color-border-focus);
}
.email-recipient-input:disabled {
color: var(--color-text-muted);
cursor: not-allowed;
}
.email-recipient-add-comment {
padding: 2px 8px;
font-size: 11px;
color: var(--color-text-muted);
background: transparent;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s;
}
.email-recipient-row:hover .email-recipient-add-comment {
opacity: 1;
}
.email-recipient-add-comment:hover {
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
}
/* Field comment input */
.email-recipient-comment-input {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-2);
margin-left: 48px;
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
}
.email-recipient-comment-input input {
padding: var(--space-2);
font-size: 13px;
color: var(--color-text-primary);
background: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
}
.email-recipient-comment-input input:focus {
outline: none;
border-color: var(--color-border-focus);
}
.email-recipient-comment-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
}
.email-recipient-comment-actions button {
padding: 4px 10px;
font-size: 12px;
border-radius: var(--radius-sm);
cursor: pointer;
border: none;
}
.email-recipient-comment-actions button:first-child {
background: transparent;
color: var(--color-text-secondary);
}
.email-recipient-comment-actions button:first-child:hover {
background: var(--color-bg-tertiary);
}
.email-recipient-comment-actions button.primary {
background: var(--color-accent-primary);
color: white;
font-weight: 500;
}
.email-recipient-comment-actions button.primary:hover:not(:disabled) {
opacity: 0.9;
}
.email-recipient-comment-actions button.primary:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Existing field comment */
.email-recipient-comment {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
margin-left: 68px;
margin-top: var(--space-1);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-default);
border-left: 3px solid #3b82f6;
border-radius: var(--radius-md);
font-size: 13px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.email-recipient-comment-content {
flex: 1;
color: var(--color-text-primary);
}
.email-recipient-comment-edit,
.email-recipient-comment-remove {
padding: 2px 6px;
font-size: 11px;
background: transparent;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--color-text-muted);
}
.email-recipient-comment-edit:hover,
.email-recipient-comment-remove:hover {
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
}
/* Subject Section */
.email-approval-subject {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-default);
background: var(--color-bg-primary);
}
.email-approval-subject .email-recipient-input {
font-weight: 600;
}
/* Body Section */
.email-approval-body {
flex: 1;
min-height: 0; /* Required for flex child to scroll */
display: flex;
flex-direction: column;
padding: var(--space-3) var(--space-4);
background: var(--color-bg-primary);
}
.email-approval-body .commentable-text {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.email-approval-body .commentable-text-content {
flex: 1;
min-height: 0;
overflow-y: auto;
}
/* Footer override for edge-to-edge layout */
.email-approval-view .approval-footer {
padding: var(--space-3) var(--space-4);
}
================================================
FILE: src/components/Approval/EmailApproval.tsx
================================================
/**
* Email Approval View
*
* Dedicated approval view for Gmail send/draft operations.
* Displays email in familiar email-client style layout with
* editable subject and commentable body.
*/
import { useState } from 'react';
import { McpIcon } from '../common';
import { ApprovalFooter } from './ApprovalFooter';
import { CommentableText, formatTextComments } from '../CommentableText/CommentableText';
import { useFieldComments, type FieldComment } from '../../hooks';
import type { TextComment } from '../../types';
import type { ApprovalViewProps } from './ApprovalViewRegistry';
import './EmailApproval.css';
interface EmailApprovalData {
to?: string;
cc?: string;
bcc?: string;
subject?: string;
body?: string;
}
export function EmailApproval({
action,
data,
onApprove,
onRequestChanges,
onCancel,
isLoading,
}: ApprovalViewProps) {
// Parse email data - handle JSON string case
let emailData: EmailApprovalData = {};
if (typeof data === 'string') {
try {
emailData = JSON.parse(data);
} catch {
emailData = {};
}
} else {
emailData = data as EmailApprovalData;
}
const {
to: proposedTo = '',
cc: proposedCc = '',
bcc: proposedBcc = '',
subject: proposedSubject = '',
body = '',
} = emailData;
// Editable fields
const [emailTo, setEmailTo] = useState(proposedTo);
const [emailCc, setEmailCc] = useState(proposedCc);
const [emailBcc, setEmailBcc] = useState(proposedBcc);
const [emailSubject, setEmailSubject] = useState(proposedSubject);
// Body comments (line-level via CommentableText)
const [bodyComments, setBodyComments] = useState<TextComment[]>([]);
// Field comments using shared hook
const {
fieldComments,
commentingField,
commentInput,
setCommentInput,
startFieldComment,
editFieldComment,
submitFieldComment,
cancelFieldComment,
removeFieldComment,
getFieldComment,
commentCount: fieldCommentCount,
} = useFieldComments();
// Body comment handlers
const handleAddBodyComment = (comment: Omit<TextComment, 'id'>) => {
const newComment: TextComment = {
...comment,
id: `comment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
};
setBodyComments((prev) => [...prev, newComment]);
};
const handleRemoveBodyComment = (id: string) => {
setBodyComments((prev) => prev.filter((c) => c.id !== id));
};
// Wrapper functions for the hook (to match component expectations)
const handleStartFieldComment = (fieldKey: string) => startFieldComment(fieldKey);
const handleEditFieldComment = (fieldKey: string, content: string) => editFieldComment(fieldKey, content);
const handleSubmitFieldComment = (fieldKey: string, fieldLabel: string) => submitFieldComment(fieldKey, fieldLabel);
const handleCancelFieldComment = () => cancelFieldComment();
const handleRemoveFieldComment = (fieldKey: string) => removeFieldComment(fieldKey);
// Total comments count
const totalComments = bodyComments.length + fieldCommentCount;
// Format all comments as feedback
const formatAllFeedback = (): string => {
const sections: string[] = [];
// Field comments
for (const fc of fieldComments) {
sections.push(`[${fc.fieldLabel}]: "${fc.content}"`);
}
// Body comments
if (bodyComments.length > 0) {
sections.push(`[Message Body]\n${formatTextComments(bodyComments)}`);
}
return sections.join('\n\n');
};
const handleApprove = () => {
// Pass back edited fields
(onApprove as (responseData?: Record<string, unknown>) => void)({
to: emailTo.trim() || proposedTo,
cc: emailCc.trim() || undefined,
bcc: emailBcc.trim() || undefined,
subject: emailSubject.trim() || proposedSubject,
});
};
const handleRequestChanges = () => {
const feedback = formatAllFeedback();
onRequestChanges(feedback);
};
// Determine action label
const isSend = action?.toLowerCase().includes('send');
const approveLabel = isSend ? 'Send Email' : 'Save Draft';
return (
<div className="email-approval-view">
{/* Header */}
<div className="email-approval-header">
<McpIcon type="gmail" size={20} />
<h3>{action || (isSend ? 'Send Email' : 'Create Draft')}</h3>
</div>
{/* Recipients Section */}
<div className="email-approval-recipients">
<RecipientField
label="To"
value={emailTo}
onChange={setEmailTo}
fieldKey="to"
isCommenting={commentingField === 'to'}
existingComment={getFieldComment('to')}
commentInput={commentInput}
onStartComment={() => handleStartFieldComment('to')}
onCommentChange={setCommentInput}
onSubmitComment={() => handleSubmitFieldComment('to', 'To')}
onCancelComment={handleCancelFieldComment}
onEditComment={(content) => handleEditFieldComment('to', content)}
onRemoveComment={() => handleRemoveFieldComment('to')}
disabled={isLoading}
/>
{(emailCc || proposedCc) && (
<RecipientField
label="CC"
value={emailCc}
onChange={setEmailCc}
fieldKey="cc"
isCommenting={commentingField === 'cc'}
existingComment={getFieldComment('cc')}
commentInput={commentInput}
onStartComment={() => handleStartFieldComment('cc')}
onCommentChange={setCommentInput}
onSubmitComment={() => handleSubmitFieldComment('cc', 'CC')}
onCancelComment={handleCancelFieldComment}
onEditComment={(content) => handleEditFieldComment('cc', content)}
onRemoveComment={() => handleRemoveFieldComment('cc')}
disabled={isLoading}
/>
)}
{(emailBcc || proposedBcc) && (
<RecipientField
label="BCC"
value={emailBcc}
onChange={setEmailBcc}
fieldKey="bcc"
isCommenting={commentingField === 'bcc'}
existingComment={getFieldComment('bcc')}
commentInput={commentInput}
onStartComment={() => handleStartFieldComment('bcc')}
onCommentChange={setCommentInput}
onSubmitComment={() => handleSubmitFieldComment('bcc', 'BCC')}
onCancelComment={handleCancelFieldComment}
onEditComment={(content) => handleEditFieldComment('bcc', content)}
onRemoveComment={() => handleRemoveFieldComment('bcc')}
disabled={isLoading}
/>
)}
</div>
{/* Subject (editable with comment) */}
<div className="email-approval-subject">
<RecipientField
label="Subject"
value={emailSubject}
onChange={setEmailSubject}
fieldKey="subject"
isCommenting={commentingField === 'subject'}
existingComment={getFieldComment('subject')}
commentInput={commentInput}
onStartComment={() => handleStartFieldComment('subject')}
onCommentChange={setCommentInput}
onSubmitComment={() => handleSubmitFieldComment('subject', 'Subject')}
onCancelComment={handleCancelFieldComment}
onEditComment={(content) => handleEditFieldComment('subject', content)}
onRemoveComment={() => handleRemoveFieldComment('subject')}
disabled={isLoading}
/>
</div>
{/* Body (commentable) */}
<div className="email-approval-body">
<CommentableText
content={body}
label="Message"
comments={bodyComments}
onAddComment={handleAddBodyComment}
onRemoveComment={handleRemoveBodyComment}
disabled={isLoading}
variant="prose"
/>
</div>
{/* Footer */}
<ApprovalFooter
onApprove={handleApprove}
onRequestChanges={handleRequestChanges}
onCancel={onCancel}
isLoading={isLoading}
approveLabel={approveLabel}
approveDisabled={!emailSubject.trim()}
commentCount={totalComments}
/>
</div>
);
}
/**
* Recipient field with comment support
*/
interface RecipientFieldProps {
label: string;
value: string;
onChange: (value: string) => void;
fieldKey: string;
isCommenting: boolean;
existingComment?: FieldComment;
commentInput: string;
onStartComment: () => void;
onCommentChange: (value: string) => void;
onSubmitComment: () => void;
onCancelComment: () => void;
onEditComment: (content: string) => void;
onRemoveComment: () => void;
disabled: boolean;
}
function RecipientField({
label,
value,
onChange,
isCommenting,
existingComment,
commentInput,
onStartComment,
onCommentChange,
onSubmitComment,
onCancelComment,
onEditComment,
onRemoveComment,
disabled,
}: RecipientFieldProps) {
return (
<div className="email-recipient-field">
<div className="email-recipient-row">
<span className="email-recipient-label">{label}:</span>
<input
type="text"
className="email-recipient-input"
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
placeholder={`Enter ${label.toLowerCase()}...`}
/>
{!isCommenting && !existingComment && !disabled && (
<button
className="email-recipient-add-comment"
onClick={onStartComment}
title="Add comment"
>
+ comment
</button>
)}
</div>
{/* Comment input */}
{isCommenting && (
<div className="email-recipient-comment-input">
<input
type="text"
value={commentInput}
onChange={(e) => onCommentChange(e.target.value)}
placeholder="Add your feedback..."
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && commentInput.trim()) {
onSubmitComment();
} else if (e.key === 'Escape') {
onCancelComment();
}
}}
/>
<div className="email-recipient-comment-actions">
<button onClick={onCancelComment}>Cancel</button>
<button
onClick={onSubmitComment}
disabled={!commentInput.trim()}
className="primary"
>
Add Comment
</button>
</div>
</div>
)}
{/* Existing comment */}
{existingComment && !isCommenting && (
<div className="email-recipient-comment">
<span className="email-recipient-comment-content">{existingComment.content}</span>
<button
className="email-recipient-comment-edit"
onClick={() => onEditComment(existingComment.content)}
title="Edit comment"
>
Edit
</button>
<button
className="email-recipient-comment-remove"
onClick={onRemoveComment}
title="Remove comment"
>
×
</button>
</div>
)}
</div>
);
}
================================================
FILE: src/components/Approval/GitHubPRApproval.css
================================================
/**
* GitHub PR Approval View Styles
*
* Full-width diff viewer layout for PR approval.
* Similar to ExecutionReviewView but integrated as an approval component.
*/
.pr-approval-view {
display: flex;
flex-direction: column;
height: 100%;
min-height: 500px;
}
/* Header */
.pr-approval-header {
display: flex;
align-items: flex-start;
gap: var(--space-4);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-default);
background: var(--color-bg-secondary);
}
.pr-approval-title {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.pr-approval-title-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.pr-approval-title h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
.pr-approval-repo {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 12px;
color: var(--color-text-secondary);
}
.pr-approval-repo code {
font-family: var(--font-mono);
padding: 2px 6px;
background: var(--color-bg-tertiary);
border-radius: var(--radius-sm);
}
.pr-approval-branch-arrow {
color: var(--color-text-muted);
}
.pr-approval-branch-into {
color: var(--color-text-muted);
}
.pr-approval-branch {
color: var(--color-primary);
}
.pr-approval-stats {
display: flex;
gap: var(--space-3);
font-family: var(--font-mono);
font-size: 12px;
}
.pr-approval-stats .stat-files {
color: var(--color-text-secondary);
}
.pr-approval-stats .stat-additions {
color: #22c55e;
}
.pr-approval-stats .stat-deletions {
color: #ef4444;
}
/* Content */
.pr-approval-content {
flex: 1;
display: flex;
overflow: hidden;
min-height: 0;
}
/* Empty state when no diff */
.pr-approval-content-empty {
flex: 0;
min-height: 120px;
}
.pr-approval-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-4);
color: var(--color-text-muted);
}
.pr-approval-empty-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--color-warning-subtle);
color: var(--color-warning-text);
font-weight: 600;
font-size: 16px;
}
.pr-approval-empty-title {
font-size: 14px;
font-weight: 500;
color: var(--color-text-secondary);
}
.pr-approval-empty-message {
font-size: 12px;
color: var(--color-text-muted);
text-align: center;
max-width: 300px;
}
.pr-approval-sidebar {
width: 220px;
border-right: 1px solid var(--color-border-default);
overflow-y: auto;
background: var(--color-bg-secondary);
}
.pr-approval-diff {
flex: 1;
overflow-y: auto;
padding: var(--space-3);
}
/* Footer */
.pr-approval-footer {
padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--color-border-default);
background: var(--color-bg-secondary);
}
.pr-approval-form {
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.pr-approval-form-fields {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.pr-approval-form-body {
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.pr-approval-textarea {
padding: var(--space-2) var(--space-3);
background: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
font-family: var(--font-mono);
font-size: 13px;
color: var(--color-text-primary);
resize: vertical;
min-height: 60px;
}
.pr-approval-textarea:focus {
outline: none;
border-color: var(--color-border-focus);
}
.pr-approval-textarea:disabled {
background: var(--color-bg-tertiary);
color: var(--color-text-muted);
cursor: not-allowed;
}
/* Agent feedback (collapsible) */
.pr-approval-agent-feedback {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.pr-approval-agent-feedback-bar {
display: flex;
align-items: center;
justify-content: space-between;
}
.pr-approval-agent-feedback-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--color-text-muted);
}
.pr-approval-agent-feedback-dismiss {
border: none;
background: transparent;
font-size: 12px;
color: var(--color-text-muted);
cursor: pointer;
padding: 0;
}
.pr-approval-agent-feedback-dismiss:hover {
color: var(--color-text-primary);
}
.pr-approval-agent-feedback-count {
font-weight: 400;
}
.pr-approval-add-note-link {
align-self: flex-start;
border: none;
background: transparent;
font-size: 12px;
color: var(--color-text-muted);
cursor: pointer;
padding: 0;
}
.pr-approval-add-note-link:hover {
color: var(--color-text-primary);
}
.pr-approval-agent-textarea {
width: 100%;
padding: var(--space-2) var(--space-3);
border: 1px dashed var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 13px;
font-family: var(--font-mono);
resize: vertical;
}
.pr-approval-agent-textarea:focus {
outline: none;
border-color: var(--color-border-focus);
}
.pr-approval-agent-textarea:disabled {
opacity: 0.6;
cursor: not-allowed;
}
================================================
FILE: src/components/Approval/GitHubPRApproval.tsx
================================================
/**
* GitHub PR Approval View
*
* Full diff viewer for approving pull request creation.
* Shows the complete diff with file tree, editable PR title/body.
*/
import { useState, useRef, useMemo } from 'react';
import { Input, McpIcon } from '../common';
import { ApprovalFooter } from './ApprovalFooter';
import { DiffViewer, FileTree } from '../DiffViewer/DiffViewer';
import { parseDiff } from '../../utils/diffParser';
import type { ApprovalViewProps } from './ApprovalViewRegistry';
import type { DiffComment } from '../../types';
import './GitHubPRApproval.css';
interface PRApprovalData {
owner?: string;
repo?: string;
baseBranch?: string; // GitHub tool
headBranch?: string;
base?: string; // Sandbox tool
branch?: string;
title?: string;
body?: string;
diff?: string;
stats?: {
files?: number;
additions?: number;
deletions?: number;
};
}
export function GitHubPRApproval({
action,
data,
toolResults,
onApprove,
onRequestChanges,
onCancel,
isLoading,
}: ApprovalViewProps) {
const diffViewerRef = useRef<HTMLDivElement>(null);
const [comments, setComments] = useState<DiffComment[]>([]);
// Parse data - handle JSON string case
let prData: PRApprovalData = {};
if (typeof data === 'string') {
try {
prData = JSON.parse(data);
} catch {
prData = {};
}
} else {
prData = data as PRApprovalData;
}
// Prefer cached getDiff result for diff/stats (agent may truncate large diffs)
const diffResult = toolResults?.['Sandbox__getDiff'] as PRApprovalData | undefined;
if (diffResult) {
if (diffResult.diff) prData.diff = diffResult.diff;
if (diffResult.stats) prData.stats = diffResult.stats;
}
const {
owner = '',
repo = '',
baseBranch,
headBranch,
base,
branch,
title: proposedTitle = '',
body: proposedBody = '',
diff = '',
stats,
} = prData;
const targetBranch = baseBranch || base || 'main';
const sourceBranch = headBranch || branch || '';
// Parse the diff content
const files = useMemo(() => parseDiff(diff), [diff]);
// Editable PR title and body
const [prTitle, setPrTitle] = useState(proposedTitle);
const [prBody, setPrBody] = useState(proposedBody);
const [selectedFile, setSelectedFile] = useState<string | undefined>(
files.length > 0 ? files[0].path : undefined
);
const [showAgentFeedback, setShowAgentFeedback] = useState(false);
const [revisionNote, setRevisionNote] = useState('');
// Calculate stats from parsed diff if not provided
const totalFiles = stats?.files ?? files.length;
const totalAdditions = stats?.additions ?? files.reduce((sum, f) => sum + f.additions, 0);
const totalDeletions = stats?.deletions ?? files.reduce((sum, f) => sum + f.deletions, 0);
const handleFileSelect = (path: string) => {
setSelectedFile(path);
// Scroll the file into view in the diff viewer
if (diffViewerRef.current) {
const fileElement = diffViewerRef.current.querySelector(`[data-file-path="${CSS.escape(path)}"]`);
if (fileElement) {
fileElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
};
// Comment handlers for diff
const handleAddComment = (comment: Omit<DiffComment, 'id'>) => {
const newComment: DiffComment = {
...comment,
id: `comment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
};
setComments((prev) => [...prev, newComment]);
};
const handleRemoveComment = (id: string) => {
setComments((prev) => prev.filter((c) => c.id !== id));
};
// Format comments as feedback for the agent
const formatCommentsFeedback = (): string => {
if (comments.length === 0) return '';
const lines: string[] = ['DIFF COMMENTS:'];
// Group by file
const byFile = comments.reduce((acc, c) => {
if (!acc[c.filePath]) acc[c.filePath] = [];
acc[c.filePath].push(c);
return acc;
}, {} as Record<string, DiffComment[]>);
for (const [filePath, fileComments] of Object.entries(byFile)) {
lines.push(`\nFile: ${filePath}`);
for (const c of fileComments) {
const lineRef = c.endLine && c.endLine !== c.lineNumber
? `Lines ${c.lineNumber}-${c.endLine}`
: `Line ${c.lineNumber}`;
lines.push(` ${lineRef}: "${c.content}"`);
}
}
return lines.join('\n');
};
const handleApprove = () => {
// Pass back user-edited title/body
(onApprove as (responseData?: Record<string, unknown>) => void)({
title: prTitle.trim() || proposedTitle,
body: prBody.trim(),
});
};
const hasRevisionNote = revisionNote.trim().length > 0;
const hasComments = comments.length > 0;
const handleReviseOrExpand = () => {
// If expanded or has comments: send immediately
if (showAgentFeedback || hasComments) {
handleRequestChanges();
return;
}
// Otherwise expand the feedback textarea
setShowAgentFeedback(true);
};
const handleRequestChanges = () => {
const parts: string[] = [];
if (revisionNote.trim()) {
parts.push(revisionNote.trim());
}
const commentsFeedback = formatCommentsFeedback();
if (commentsFeedback) {
parts.push(commentsFeedback);
}
onRequestChanges(parts.join('\n\n'));
};
const reviseLabel = showAgentFeedback
? 'Send to Agent'
: hasComments
? `Send to Agent (${comments.length})`
: 'Revise with Agent';
const reviseDisabled = showAgentFeedback
? !hasRevisionNote && !hasComments
: false;
return (
<div className="pr-approval-view">
{/* Header */}
<div className="pr-approval-header">
<div className="pr-approval-title">
<div className="pr-approval-title-row">
<McpIcon type="github" size={20} />
<h3>{action || 'Create Pull Request'}</h3>
</div>
<div className="pr-approval-repo">
{owner && repo && <><code>{owner}/{repo}</code><span className="pr-approval-branch-arrow">←</span></>}
<code className="pr-approval-branch">{sourceBranch}</code>
<span className="pr-approval-branch-into">into</span>
<code className="pr-approval-branch">{targetBranch}</code>
</div>
</div>
<div className="pr-approval-stats">
<span className="stat-files">{totalFiles} files</span>
<span className="stat-additions">+{totalAdditions}</span>
<span className="stat-deletions">-{totalDeletions}</span>
</div>
</div>
{/* Content */}
<div className={`pr-approval-content ${files.length === 0 ? 'pr-approval-content-empty' : ''}`}>
{files.length === 0 ? (
<div className="pr-approval-empty">
<div className="pr-approval-empty-icon">!</div>
<div className="pr-approval-empty-title">No diff provided</div>
<div className="pr-approval-empty-message">
The agent should use Sandbox to make changes and include the diff in the approval request.
</div>
</div>
) : (
<>
{/* Sidebar */}
<div className="pr-approval-sidebar">
<FileTree
files={files}
selectedFile={selectedFile}
onSelect={handleFileSelect}
/>
</div>
{/* Diff View */}
<div className="pr-approval-diff" ref={diffViewerRef}>
<DiffViewer
files={files}
selectedFile={selectedFile}
onFileSelect={setSelectedFile}
comments={comments}
onAddComment={handleAddComment}
onRemoveComment={handleRemoveComment}
/>
</div>
</>
)}
</div>
{/* Footer with PR form */}
<div className="pr-approval-footer">
<div className="pr-approval-form">
<div className="pr-approval-form-fields">
<Input
label="PR Title"
value={prTitle}
onChange={(e) => setPrTitle(e.target.value)}
placeholder="Enter PR title..."
disabled={isLoading}
/>
<div className="pr-approval-form-body">
<label className="input-label">Description</label>
<textarea
className="pr-approval-textarea"
value={prBody}
onChange={(e) => setPrBody(e.target.value)}
placeholder="Describe the changes..."
rows={3}
disabled={isLoading}
/>
</div>
</div>
{showAgentFeedback && (
<div className="pr-approval-agent-feedback">
<div className="pr-approval-agent-feedback-bar">
<span className="pr-approval-agent-feedback-label">
Agent feedback
{hasComments && <span className="pr-approval-agent-feedback-count"> · {comments.length} inline comment{comments.length !== 1 ? 's' : ''} included</span>}
</span>
<button
className="pr-approval-agent-feedback-dismiss"
onClick={() => { setShowAgentFeedback(false); setRevisionNote(''); }}
>
Cancel
</button>
</div>
<textarea
className="pr-approval-agent-textarea"
value={revisionNote}
onChange={(e) => setRevisionNote(e.target.value)}
placeholder="Tell the agent what to change..."
rows={2}
autoFocus
disabled={isLoading}
/>
</div>
)}
{!showAgentFeedback && hasComments && (
<button
className="pr-approval-add-note-link"
onClick={() => setShowAgentFeedback(true)}
>
+ Add general feedback
</button>
)}
<ApprovalFooter
onApprove={handleApprove}
onRequestChanges={handleReviseOrExpand}
onCancel={onCancel}
isLoading={isLoading}
approveLabel="Create Pull Request"
approveDisabled={!prTitle.trim()}
commentCount={comments.length}
requestChangesLabel={reviseLabel}
requestChangesDisabled={reviseDisabled}
requestChangesDisabledTitle="Add a revision note or diff comments"
/>
</div>
</div>
</div>
);
}
================================================
FILE: src/components/Approval/GitHubPRReviewApproval.css
================================================
.pr-review-approval-view {
display: flex;
flex-direction: column;
height: 100%;
min-height: 520px;
}
.pr-review-approval-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-default);
background: var(--color-bg-secondary);
}
.pr-review-approval-title {
display: flex;
flex-direction: column;
gap: var(--space-2);
min-width: 0;
}
.pr-review-approval-title-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.pr-review-approval-title-row h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.pr-review-approval-pr-title {
font-size: 13px;
color: var(--color-text-primary);
}
.pr-review-approval-meta {
display: flex;
align-items: center;
gap: var(--space-2);
flex-wrap: wrap;
font-size: 12px;
color: var(--color-text-secondary);
}
.pr-review-repo-code,
.pr-review-branch-code {
font-family: var(--font-mono);
padding: 2px 6px;
border-radius: var(--radius-sm);
background: var(--color-bg-tertiary);
}
.pr-review-approval-branch-arrow {
color: var(--color-text-muted);
}
.pr-review-approval-branch-into {
color: var(--color-text-muted);
}
.pr-review-approval-author {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 12px;
color: var(--color-text-secondary);
}
.pr-review-author-label {
color: var(--color-text-muted);
}
.pr-review-author-handle {
color: var(--color-text-primary);
font-weight: 500;
}
.pr-review-approval-stats {
display: flex;
gap: var(--space-3);
font-family: var(--font-mono);
font-size: 12px;
white-space: nowrap;
}
.pr-review-approval-stats .stat-files {
color: var(--color-text-secondary);
}
.pr-review-approval-stats .stat-additions {
color: #22c55e;
}
.pr-review-approval-stats .stat-deletions {
color: #ef4444;
}
.pr-review-approval-stats .stat-comments {
color: var(--color-text-primary);
}
.pr-review-approval-content {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.pr-review-approval-main {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
}
.pr-review-approval-main-empty {
grid-template-columns: minmax(0, 1fr);
}
.pr-review-approval-files {
border-right: 1px solid var(--color-border-default);
background: var(--color-bg-secondary);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.pr-review-approval-diff {
min-width: 0;
overflow-y: auto;
padding: var(--space-3);
}
/* ── Footer ── */
.pr-review-approval-footer {
border-top: 1px solid var(--color-border-default);
background: var(--color-bg-secondary);
padding: var(--space-3) var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-3);
}
.pr-review-footer-top {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
/* Decision pill buttons */
.pr-review-decision-pills {
display: flex;
width: fit-content;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
overflow: hidden;
}
.pr-review-decision-pill {
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
border: none;
background: var(--color-bg-primary);
color: var(--color-text-secondary);
cursor: pointer;
transition: background-color 0.15s, color 0.15s;
white-space: nowrap;
}
.pr-review-decision-pill:not(:last-child) {
border-right: 1px solid var(--color-border-default);
}
.pr-review-decision-pill:hover:not(.active):not(:disabled) {
background: var(--color-bg-tertiary);
}
.pr-review-decision-pill.active {
background: var(--color-accent-primary);
color: #fff;
}
.pr-review-decision-pill:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Review body textarea (posted to PR) */
.pr-review-body-textarea {
flex: 1;
min-width: 0;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 13px;
resize: vertical;
}
.pr-review-body-textarea:focus {
outline: none;
border-color: var(--color-border-focus);
}
.pr-review-body-textarea:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Agent feedback (collapsible) */
.pr-review-agent-feedback {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.pr-review-agent-feedback-bar {
display: flex;
align-items: center;
justify-content: space-between;
}
.pr-review-agent-feedback-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--color-text-muted);
}
.pr-review-agent-feedback-dismiss {
border: none;
background: transparent;
font-size: 12px;
color: var(--color-text-muted);
cursor: pointer;
padding: 0;
}
.pr-review-agent-feedback-dismiss:hover {
color: var(--color-text-primary);
}
.pr-review-textarea {
width: 100%;
padding: var(--space-2) var(--space-3);
border: 1px dashed var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-bg-primary);
color: var(--color-text-primary);
font-size: 13px;
font-family: var(--font-mono);
resize: vertical;
}
.pr-review-textarea:focus {
outline: none;
border-color: var(--color-border-focus);
}
.pr-review-textarea:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.pr-review-approval-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-2);
height: 100%;
color: var(--color-text-muted);
}
.pr-review-approval-empty-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--color-warning-subtle);
color: var(--color-warning-text);
font-weight: 600;
}
.pr-review-approval-empty-title {
font-size: 14px;
color: var(--color-text-primary);
}
.pr-review-approval-empty-message {
max-width: 320px;
text-align: center;
font-size: 12px;
}
@media (max-width: 1200px) {
.pr-review-approval-main {
grid-template-columns: 220px minmax(0, 1fr);
}
}
@media (max-width: 900px) {
.pr-review-approval-main {
display: flex;
flex-direction: column;
}
.pr-review-approval-files {
border-right: none;
border-top: 1px solid var(--color-border-default);
}
.pr-review-decision-pills {
align-self: stretch;
}
.pr-review-decision-pill {
flex: 1;
text-align: center;
}
}
================================================
FILE: src/components/Approval/GitHubPRReviewApproval.tsx
================================================
/**
* GitHub PR Review Approval View
*
* Review-focused approval UI for posting PR reviews:
* - Shows PR metadata and author
* - Displays full diff with inline notes
* - Supports review decision, summary, comments, and suggestions
* - Supports "Revise Review" loop with revision notes
*/
import { useMemo, useRef, useState } from 'react';
import { McpIcon } from '../common';
import { ApprovalFooter } from './ApprovalFooter';
import { DiffViewer, FileTree } from '../DiffViewer/DiffViewer';
import { parseDiff } from '../../utils/diffParser';
import type { ApprovalViewProps } from './ApprovalViewRegistry';
import type { DiffComment } from '../../types';
import './GitHubPRReviewApproval.css';
type ReviewDecision = 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT';
type ReviewCommentSide = 'LEFT' | 'RIGHT';
interface ReviewCommentDraft {
path: string;
line: number;
side: ReviewCommentSide;
body?: string;
startLine?: number;
startSide?: ReviewCommentSide;
suggestion?: string;
}
interface PRReviewApprovalData {
owner?: string;
repo?: string;
pullNumber?: number;
prTitle?: string;
title?: string;
author?: string;
authorLogin?: string;
baseBranch?: string;
headBranch?: string;
base?: string;
head?: string;
diff?: string;
stats?: {
files?: number;
additions?: number;
deletions?: number;
};
event?: ReviewDecision;
body?: string;
comments?: ReviewCommentDraft[];
}
function serializeComments(comments: DiffComment[]): string {
const normalized = comments.map((comment) => ({
filePath: comment.filePath,
lineNumber: comment.lineNumber,
endLine: comment.endLine ?? null,
lineType: comment.lineType,
content: comment.content.trim(),
kind: comment.kind || 'comment',
suggestion: (comment.suggestion || '').trimEnd(),
}));
return JSON.stringify(normalized);
}
/** Strip wrapping code fences the agent may add — our backend already wraps in ```suggestion. */
function stripCodeFences(text: string): string {
return text.replace(/^```\w*\n?/, '').replace(/\n?```\s*$/, '');
}
function toDiffComment(comment: ReviewCommentDraft): DiffComment {
const rawSuggestion = comment.suggestion?.trimEnd();
const cleanSuggestion = rawSuggestion ? stripCodeFences(rawSuggestion) : undefined;
const isSuggestion = !!cleanSuggestion;
const lineType = comment.side === 'LEFT' ? 'deletion' : 'addition';
const startLine = comment.startLine ?? comment.line;
const endLine = comment.line;
const fallbackContent = isSuggestion ? 'Suggested change' : 'Review note';
return {
id: `comment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
filePath: comment.path,
lineNumber: Math.min(startLine, endLine),
endLine: startLine !== endLine ? Math.max(startLine, endLine) : undefined,
lineType,
content: comment.body?.trim() || fallbackContent,
kind: isSuggestion ? 'suggestion' : 'comment',
suggestion: cleanSuggestion,
};
}
function toReviewComment(comment: DiffComment): ReviewCommentDraft | null {
const side: ReviewCommentSide = comment.lineType === 'deletion' ? 'LEFT' : 'RIGHT';
const startLine = comment.lineNumber;
const endLine = comment.endLine ?? comment.lineNumber;
const normalizedStartLine = Math.min(startLine, endLine);
const normalizedEndLine = Math.max(startLine, endLine);
const baseBody = comment.content.trim();
const suggestion = comment.kind === 'suggestion' ? (comment.suggestion?.trimEnd() || '') : undefined;
if (!baseBody && !suggestion) {
return null;
}
// GitHub suggestions are only supported on RIGHT side comments.
if (comment.kind === 'suggestion' && side === 'LEFT') {
return {
path: comment.filePath,
line: normalizedEndLine,
side,
startLine: normalizedStartLine !== normalizedEndLine ? normalizedStartLine : undefined,
startSide: normalizedStartLine !== normalizedEndLine ? side : undefined,
body: [baseBody, suggestion].filter(Boolean).join('\n\n'),
};
}
return {
path: comment.filePath,
line: normalizedEndLine,
side,
startLine: normalizedStartLine !== normalizedEndLine ? normalizedStartLine : undefined,
startSide: normalizedStartLine !== normalizedEndLine ? side : undefined,
body: baseBody,
suggestion,
};
}
function formatLineRef(comment: DiffComment): string {
if (comment.endLine && comment.endLine !== comment.lineNumber) {
return `L${comment.lineNumber}-${comment.endLine}`;
}
return `L${comment.lineNumber}`;
}
export function GitHubPRReviewApproval({
action,
data,
toolResults,
onApprove,
onRequestChanges,
onCancel,
isLoading,
}: ApprovalViewProps) {
const diffViewerRef = useRef<HTMLDivElement>(null);
let reviewData: PRReviewApprovalData = {};
if (typeof data === 'string') {
try {
reviewData = JSON.parse(data);
} catch {
reviewData = {};
}
} else {
reviewData = data as PRReviewApprovalData;
}
// Prefer cached getPullRequest result for diff/stats (agent may truncate large diffs)
const prResult = toolResults?.['GitHub__getPullRequest'] as PRReviewApprovalData | undefined;
if (prResult) {
if (prResult.diff) reviewData.diff = prResult.diff;
if (prResult.stats) reviewData.stats = prResult.stats;
if (!reviewData.author && prResult.author) reviewData.author = prResult.author;
if (!reviewData.baseBranch && prResult.base) reviewData.baseBranch = prResult.base;
if (!reviewData.headBranch && prResult.head) reviewData.headBranch = prResult.head;
}
const {
owner = '',
repo = '',
pullNumber,
prTitle = reviewData.title || '',
author = reviewData.authorLogin || reviewData.author || '',
baseBranch,
headBranch,
base = 'main',
head = '',
diff = '',
stats,
event = 'COMMENT',
body = '',
comments: proposedComments = [],
} = reviewData;
const files = useMemo(() => parseDiff(diff), [diff]);
const [selectedFile, setSelectedFile] = useState<string | undefined>(
files.length > 0 ? files[0].path : undefined
);
const [reviewDecision, setReviewDecision] = useState<ReviewDecision>(event);
const [reviewBody, setReviewBody] = useState(body);
const [revisionNote, setRevisionNote] = useState('');
const [showAgentFeedback, setShowAgentFeedback] = useState(false);
const [activeInlineNoteId, setActiveInlineNoteId] = useState<string | null>(null);
const [comments, setComments] = useState<DiffComment[]>(
proposedComments.map(toDiffComment)
);
const initialCommentsSnapshot = useMemo(
() => serializeComments(proposedComments.map(toDiffComment)),
[proposedComments]
);
const totalFiles = stats?.files ?? files.length;
const totalAdditions = stats?.additions ?? files.reduce((sum, f) => sum + f.additions, 0);
const totalDeletions = stats?.deletions ?? files.reduce((sum, f) => sum + f.deletions, 0);
const noteCountsByFile = useMemo(() => {
const counts: Record<string, number> = {};
for (const comment of comments) {
counts[comment.filePath] = (counts[comment.filePath] || 0) + 1;
}
return counts;
}, [comments]);
const hasRevisionNote = revisionNote.trim().length > 0;
const commentsChanged = serializeComments(comments) !== initialCommentsSnapshot;
const handleFileSelect = (path: string) => {
setSelectedFile(path);
if (diffViewerRef.current) {
const fileElement = diffViewerRef.current.querySelector(`[data-file-path="${CSS.escape(path)}"]`);
if (fileElement) {
fileElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
};
const handleAddComment = (comment: Omit<DiffComment, 'id'>) => {
const next: DiffComment = {
...comment,
id: `comment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
kind: comment.kind || 'comment',
};
setComments((prev) => [...prev, next]);
setActiveInlineNoteId(next.id);
};
const handleRemoveComment = (id: string) => {
setComments((prev) => prev.filter((c) => c.id !== id));
setActiveInlineNoteId((prev) => (prev === id ? null : prev));
};
const handleApprove = () => {
const reviewComments = comments
.map(toReviewComment)
.filter((comment): comment is ReviewCommentDraft => comment !== null);
onApprove({
owner,
repo,
pullNumber,
event: reviewDecision,
body: reviewBody.trim(),
comments: reviewComments,
});
};
const handleReviseOrExpand = () => {
if (!showAgentFeedback) {
setShowAgentFeedback(true);
return;
}
handleReviseReview();
};
const handleReviseReview = () => {
const lines: string[] = ['REVISE REVIEW:'];
if (revisionNote.trim()) {
lines.push('\nGeneral revision notes:');
lines.push(revisionNote.trim());
}
if (commentsChanged && comments.length > 0) {
lines.push('\nUser-edited inline review notes:');
for (const comment of comments) {
const typeLabel = comment.kind === 'suggestion' ? 'Suggestion' : 'Comment';
const location = `${comment.filePath} ${formatLineRef(comment)}`;
lines.push(`- [${typeLabel}] ${location}: "${comment.content.trim()}"`);
if (comment.kind === 'suggestion' && comment.suggestion?.trim()) {
lines.push(` Suggested change: "${comment.suggestion.trim()}"`);
}
}
}
onRequestChanges(lines.join('\n'));
};
return (
<div className="pr-review-approval-view">
<div className="pr-review-approval-header">
<div className="pr-review-approval-title">
<div className="pr-review-approval-title-row">
<McpIcon type="github" size={20} />
<h3>{action || 'Review Pull Request'}</h3>
</div>
<div className="pr-review-approval-pr-title">{prTitle || 'Pull Request'}</div>
<div className="pr-review-approval-meta">
{owner && repo && (
<code className="pr-review-repo-code">
{owner}/{repo}
{typeof pullNumber === 'number' ? ` PR #${pullNumber}` : ''}
</code>
)}
<span className="pr-review-approval-branch-arrow">←</span>
<code className="pr-review-branch-code">{headBranch || head || 'feature'}</code>
<span className="pr-review-approval-branch-into">into</span>
<code className="pr-review-branch-code">{baseBranch || base || 'main'}</code>
</div>
{author && (
<div className="pr-review-approval-author">
<span className="pr-review-author-label">Author</span>
<span className="pr-review-author-handle">@{author}</span>
</div>
)}
</div>
<div className="pr-review-approval-stats">
<span className="stat-files">{totalFiles} files</span>
<span className="stat-additions">+{totalAdditions}</span>
<span className="stat-deletions">-{totalDeletions}</span>
<span className="stat-comments">{comments.length} notes</span>
</div>
</div>
<div className="pr-review-approval-content">
<div className={`pr-review-approval-main ${files.length === 0 ? 'pr-review-approval-main-empty' : ''}`}>
{files.length > 0 && (
<div className="pr-review-approval-files">
<FileTree
files={files}
selectedFile={selectedFile}
onSelect={handleFileSelect}
noteCounts={noteCountsByFile}
/>
</div>
)}
<div className="pr-review-approval-diff" ref={diffViewerRef}>
{files.length === 0 ? (
<div className="pr-review-approval-empty">
<div className="pr-review-approval-empty-icon">!</div>
<div className="pr-review-approval-empty-title">No diff provided</div>
<div className="pr-review-approval-empty-message">
Include a unified diff in the approval payload to review inline comments and suggestions.
</div>
</div>
) : (
<DiffViewer
files={files}
selectedFile={selectedFile}
onFileSelect={handleFileSelect}
comments={comments}
onAddComment={handleAddComment}
onRemoveComment={handleRemoveComment}
activeCommentId={activeInlineNoteId}
commentMode="comment-and-suggestion"
/>
)}
</div>
</div>
</div>
<div className="pr-review-approval-footer">
<div className="pr-review-footer-top">
<div className="pr-review-decision-pills">
{(['COMMENT', 'REQUEST_CHANGES', 'APPROVE'] as const).map((value) => (
<button
key={value}
className={`pr-review-decision-pill ${reviewDecision === value ? 'active' : ''}`}
onClick={() => setReviewDecision(value)}
disabled={isLoading}
>
{value === 'COMMENT' ? 'Comment' : value === 'REQUEST_CHANGES' ? 'Request Changes' : 'Approve'}
</button>
))}
</div>
<textarea
className="pr-review-body-textarea"
value={reviewBody}
onChange={(e) => setReviewBody(e.target.value)}
placeholder="Review comment (posted to the pull request)..."
rows={4}
disabled={isLoading}
/>
</div>
{showAgentFeedback && (
<div className="pr-review-agent-feedback">
<div className="pr-review-agent-feedback-bar">
<span className="pr-review-agent-feedback-label">Agent feedback</span>
<button
className="pr-review-agent-feedback-dismiss"
onClick={() => { setShowAgentFeedback(false); setRevisionNote(''); }}
>
Cancel
</button>
</div>
<textarea
id="pr-review-revision-textarea"
className="pr-review-textarea"
value={revisionNote}
onChange={(e) => setRevisionNote(e.target.value)}
placeholder="Tell the agent how to revise this review..."
rows={2}
autoFocus
disabled={isLoading}
/>
</div>
)}
<ApprovalFooter
onApprove={handleApprove}
onRequestChanges={handleReviseOrExpand}
onCancel={onCancel}
isLoading={isLoading}
approveLabel="Submit Review"
commentCount={comments.length}
requestChangesLabel={showAgentFeedback ? 'Send to Agent' : 'Revise with Agent'}
requestChangesDisabled={showAgentFeedback && !hasRevisionNote}
requestChangesDisabledTitle="Add a revision note"
/>
</div>
</div>
);
}
================================================
FILE: src/components/Approval/GoogleDocsApproval.css
================================================
/**
* Google Docs Approval View Styles
*
* Full-width side-by-side diff layout for document approval.
* Similar to GitHubPRApproval but styled for prose content.
*/
.docs-approval-view {
display: flex;
flex-direction: column;
height: 100%;
min-height: 500px;
}
/* Header */
.docs-approval-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-default);
background: var(--color-bg-secondary);
}
.docs-approval-title {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.docs-approval-title-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.docs-approval-title h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
.docs-approval-doc-info {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 13px;
}
.docs-approval-doc-name {
color: var(--color-text-secondary);
font-weight: 500;
}
/* Editable title (for create document) */
.docs-approval-editable-title {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.docs-approval-title-label {
font-size: 11px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.docs-approval-title-input-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.docs-approval-title-input {
flex: 1;
min-width: 400px;
padding: var(--space-2) var(--space-3);
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
background: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
transition: border-color 0.15s;
}
.docs-approval-title-input:focus {
outline: none;
border-color: var(--color-accent-primary);
}
.docs-approval-title-input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.docs-approval-add-comment {
padding: 4px 8px;
font-size: 11px;
color: var(--color-text-muted);
background: transparent;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
}
.docs-approval-add-comment:hover {
color: var(--color-text-primary);
border-color: var(--color-text-muted);
}
.docs-approval-title-comment-input {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2);
background: var(--color-bg-primary);
border: 1px solid var(--color-accent-primary);
border-radius: var(--radius-sm);
}
.docs-approval-title-comment-input input {
flex: 1;
padding: var(--space-1) var(--space-2);
font-size: 13px;
color: var(--color-text-primary);
background: transparent;
border: none;
}
.docs-approval-title-comment-input input:focus {
outline: none;
}
.docs-approval-title-comment-actions {
display: flex;
gap: var(--space-1);
}
.docs-approval-title-comment-actions button {
padding: 4px 8px;
font-size: 11px;
border-radius: var(--radius-sm);
cursor: pointer;
border: none;
}
.docs-approval-title-comment-actions button:first-child {
background: transparent;
color: var(--color-text-muted);
}
.docs-approval-title-comment-actions button:first-child:hover {
color: var(--color-text-primary);
}
.docs-approval-title-comment-actions button.primary {
background: var(--color-accent-primary);
color: white;
}
.docs-approval-title-comment-actions button.primary:disabled {
opacity: 0.4;
cursor: default;
}
.docs-approval-title-comment {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-default);
border-left: 3px solid #3b82f6;
border-radius: var(--radius-sm);
font-size: 13px;
}
.docs-approval-title-comment-content {
flex: 1;
color: var(--color-text-primary);
}
.docs-approval-title-comment-edit,
.docs-approval-title-comment-remove {
padding: 2px 6px;
font-size: 11px;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
border-radius: var(--radius-sm);
}
.docs-approval-title-comment-edit:hover {
color: var(--color-accent-primary);
}
.docs-approval-title-comment-remove:hover {
color: #ef4444;
background: rgba(239, 68, 68, 0.1);
}
.docs-approval-stats {
display: flex;
gap: var(--space-3);
font-size: 12px;
}
.docs-approval-stats .stat-current {
color: var(--color-text-muted);
}
.docs-approval-stats .stat-additions {
color: #22c55e;
}
/* Content - side by side */
.docs-approval-content {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
overflow: hidden;
min-height: 0;
}
/* Single panel mode (for create) */
.docs-approval-content-single {
grid-template-columns: 1fr;
}
.docs-approval-content-single .docs-approval-panel {
border-right: none;
}
.docs-approval-content-single .docs-paragraph-content {
font-size: 15px;
line-height: 1.7;
padding: var(--space-2) var(--space-4);
}
.docs-approval-content-single .docs-paragraph-added {
background: transparent;
}
.docs-approval-content-single .docs-paragraph-added:hover {
background: rgba(59, 130, 246, 0.06);
}
/* Panels */
.docs-approval-panel {
display: flex;
flex-direction: column;
overflow: hidden;
border-right: 1px solid var(--color-border-default);
}
.docs-approval-panel:last-child {
border-right: none;
}
.docs-approval-panel-header {
padding: var(--space-2) var(--space-3);
background: var(--color-bg-tertiary);
border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0;
}
.docs-approval-panel-label {
font-size: 11px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.docs-approval-panel-current .docs-approval-panel-header {
background: rgba(239, 68, 68, 0.05);
}
.docs-approval-panel-after .docs-approval-panel-header {
background: rgba(34, 197, 94, 0.05);
}
.docs-approval-panel-after .docs-approval-panel-label {
color: #22c55e;
}
.docs-approval-panel-body {
flex: 1;
overflow-y: auto;
background: var(--color-bg-primary);
}
.docs-approval-empty-panel {
color: var(--color-text-muted);
font-style: italic;
font-size: 13px;
padding: var(--space-4);
text-align: center;
}
/* Paragraphs - continuous like CommentableText */
.docs-paragraph-wrapper {
display: flex;
flex-direction: column;
}
.docs-paragraph {
display: flex;
align-items: flex-start;
min-height: 28px;
line-height: 1.5;
user-select: none;
cursor: pointer;
}
.docs-paragraph:hover {
background: rgba(59, 130, 246, 0.06);
}
.docs-paragraph:hover .docs-comment-indicator.can-comment {
opacity: 1;
}
.docs-paragraph.selected {
background: rgba(59, 130, 246, 0.12);
}
.docs-paragraph-content {
flex: 1;
padding: var(--space-1) var(--space-3);
font-family: var(--font-sans);
font-size: 14px;
color: var(--color-text-primary);
white-space: pre-wrap;
word-wrap: break-word;
}
/* Comment indicator column */
.docs-comment-indicator {
width: 28px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: var(--color-text-muted);
opacity: 0;
transition: opacity 0.15s;
}
.docs-comment-indicator.has-comment {
opacity: 1;
color: var(--color-accent-primary);
}
/* Diff status styling */
.docs-paragraph-added {
background: rgba(34, 197, 94, 0.08);
}
.docs-paragraph-added:hover {
background: rgba(34, 197, 94, 0.14);
}
.docs-paragraph-added .docs-comment-indicator {
color: #22c55e;
}
.docs-paragraph-removed {
background: rgba(239, 68, 68, 0.08);
}
.docs-paragraph-removed:hover {
background: rgba(239, 68, 68, 0.12);
}
.docs-paragraph-removed .docs-paragraph-content {
text-decoration: line-through;
color: var(--color-text-muted);
}
.docs-paragraph-removed .docs-comment-indicator {
color: #ef4444;
}
/* Diff marker for removed items (left side only) */
.docs-diff-marker {
width: 28px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: #ef4444;
}
/* Comment input - matches CommentableText style */
.docs-comment-input {
margin: var(--space-2) var(--space-3);
padding: var(--space-3);
background: var(--color-bg-primary);
border: 1px solid var(--color-accent-primary);
border-left: 3px solid var(--color-accent-primary);
border-radius: var(--radius-sm);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.docs-comment-input textarea {
width: 100%;
padding: var(--space-2);
font-family: var(--font-sans);
font-size: 13px;
line-height: 1.4;
border: none;
background: transparent;
color: var(--color-text-primary);
resize: none;
min-height: 48px;
}
.docs-comment-input textarea::placeholder {
color: var(--color-text-muted);
}
.docs-comment-input textarea:focus {
outline: none;
}
.docs-comment-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--color-border-default);
}
.docs-comment-actions button {
padding: 4px 10px;
font-size: 12px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.15s ease;
border: none;
}
.docs-comment-actions button:first-child {
background: transparent;
color: var(--color-text-muted);
}
.docs-comment-actions button:first-child:hover {
color: var(--color-text-primary);
}
.docs-comment-actions button.primary {
background: var(--color-accent-primary);
color: white;
font-weight: 500;
}
.docs-comment-actions button.primary:hover:not(:disabled) {
opacity: 0.9;
}
.docs-comment-actions button.primary:disabled {
opacity: 0.4;
cursor: default;
}
/* Existing comment display - matches CommentableText style */
.docs-comment {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-2);
margin: var(--space-2) var(--space-3);
padding: var(--space-3);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-default);
border-left: 3px solid #3b82f6;
border-radius: var(--radius-sm);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.docs-comment-paras {
flex-shrink: 0;
font-size: 10px;
color: var(--color-text-muted);
padding: 2px 8px;
background: rgba(59, 130, 246, 0.1);
border-radius: 10px;
align-self: flex-start;
}
.docs-comment-content {
flex: 1;
font-size: 13px;
color: var(--color-text-primary);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.docs-comment-remove {
position: absolute;
top: var(--space-2);
right: var(--space-2);
flex-shrink: 0;
width: 20px;
height: 20px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 14px;
border-radius: var(--radius-sm);
opacity: 0;
transition: all 0.15s ease;
}
.docs-comment:hover .docs-comment-remove {
opacity: 1;
}
.docs-comment-remove:hover {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
/* Footer override for full-width layout */
.docs-approval-view .approval-footer {
padding: var(--space-3) var(--space-4);
}
================================================
FILE: src/components/Approval/GoogleDocsApproval.tsx
================================================
/**
* Google Docs Approval View
*
* For createDocument: Single-panel document preview with commenting support.
* For append/replace: Side-by-side document diff with paragraph-level diff highlighting.
*
* Used for: createDocument, appendToDocument, replaceDocumentContent
*/
import { useState, useCallback, useRef } from 'react';
import { McpIcon } from '../common';
import { ApprovalFooter } from './ApprovalFooter';
import { useTitleEdit } from '../../hooks';
import type { ApprovalViewProps } from './ApprovalViewRegistry';
import './GoogleDocsApproval.css';
interface GoogleDocsApprovalData {
documentId?: string;
title?: string;
currentContent?: string;
newContent?: string;
content?: string;
action?: 'append' | 'replace';
url?: string;
}
interface TextComment {
id: string;
paragraphStart: number;
paragraphEnd: number;
side: 'left' | 'right';
content: string;
}
interface ParagraphSelection {
startIndex: number;
endIndex: number;
side: 'left' | 'right';
}
export function GoogleDocsApproval({
action,
data,
onApprove,
onRequestChanges,
onCancel,
isLoading,
}: ApprovalViewProps) {
const [comments, setComments] = useState<TextComment[]>([]);
const [selection, setSelection] = useState<ParagraphSelection | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [showCommentInput, setShowCommentInput] = useState(false);
const [commentText, setCommentText] = useState('');
const dragStartRef = useRef<{ index: number; side: 'left' | 'right' } | null>(null);
// Editable title (for create document) - using shared hook
const {
editedTitle,
setEditedTitle,
titleComment,
showTitleCommentInput,
titleCommentText,
handleStartTitleComment,
handleEditTitleComment,
handleAddTitleComment,
handleCancelTitleComment,
handleRemoveTitleComment,
setTitleCommentText,
} = useTitleEdit();
// Parse data
let docData: GoogleDocsApprovalData = {};
if (typeof data === 'string') {
try {
docData = JSON.parse(data);
} catch {
docData = {};
}
} else {
docData = data as GoogleDocsApprovalData;
}
const {
title = 'Untitled Document',
currentContent = '',
newContent = docData.content || '',
action: docAction,
} = docData;
// Detect mode: create (no currentContent), append, or replace
const isCreate = !currentContent && !docAction;
const isAppend = docAction === 'append';
const actionLabel = isCreate
? 'Create Document'
: isAppend
? 'Append to Document'
: 'Replace Document Content';
// Split content into paragraphs for diff display
const currentParagraphs = currentContent.split(/\n\n+/).filter(p => p.trim());
const newParagraphs = newContent.split(/\n\n+/).filter(p => p.trim());
// For append, combine current + new
const afterParagraphs = isAppend
? [...currentParagraphs, ...newParagraphs]
: newParagraphs;
// Simple diff: mark paragraphs as added, removed, or unchanged
const getDiffStatus = (para: string, side: 'left' | 'right'): 'added' | 'removed' | 'unchanged' => {
if (side === 'left') {
if (isAppend) return 'unchanged';
const existsInNew = newParagraphs.some(p => p.trim() === para.trim());
return existsInNew ? 'unchanged' : 'removed';
} else {
if (isAppend) {
const isFromCurrent = currentParagraphs.some(p => p.trim() === para.trim());
return isFromCurrent ? 'unchanged' : 'added';
}
const existsInCurrent = currentParagraphs.some(p => p.trim() === para.trim());
return existsInCurrent ? 'unchanged' : 'added';
}
};
// Selection handlers (like CommentableText) - only for right side
const handleParagraphMouseDown = useCallback((index: number, side: 'left' | 'right', e: React.MouseEvent) => {
if (isLoading || side === 'left') return; // Only allow comments on "After Changes" side
e.preventDefault();
dragStartRef.current = { index, side };
setIsDragging(true);
setSelection({ startIndex: index, endIndex: index, side });
setShowCommentInput(false);
}, [isLoading]);
const handleParagraphMouseEnter = useCallback((index: number, side: 'left' | 'right') => {
if (!isDragging || !dragStartRef.current || dragStartRef.current.side !== side) return;
const start = dragStartRef.current.index;
setSelection({
startIndex: Math.min(start, index),
endIndex: Math.max(start, index),
side,
});
}, [isDragging]);
const handleMouseUp = useCallback(() => {
if (isDragging && selection) {
setShowCommentInput(true);
}
setIsDragging(false);
dragStartRef.current = null;
}, [isDragging, selection]);
const handleAddComment = () => {
if (!selection || !commentText.trim()) return;
const newComment: TextComment = {
id: `comment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
paragraphStart: selection.startIndex,
paragraphEnd: selection.endIndex,
side: selection.side,
content: commentText.trim(),
};
setComments(prev => [...prev, newComment]);
setSelection(null);
setShowCommentInput(false);
setCommentText('');
};
const handleRemoveComment = (id: string) => {
setComments(prev => prev.filter(c => c.id !== id));
};
const handleCancelComment = () => {
setSelection(null);
setShowCommentInput(false);
setCommentText('');
};
// Check if paragraph is in selection
const isParagraphSelected = (index: number, side: 'left' | 'right') => {
if (!selection || selection.side !== side) return false;
return index >= selection.startIndex && index <= selection.endIndex;
};
// Check if paragraph has a comment
const hasComment = useCallback((index: number, side: 'left' | 'right') => {
return comments.some(c =>
c.side === side && index >= c.paragraphStart && index <= c.paragraphEnd
);
}, [comments]);
// Get comments that end on a specific paragraph
const getCommentsEndingAt = (index: number, side: 'left' | 'right') => {
return comments.filter(c => c.paragraphEnd === index && c.side === side);
};
// Format comments as feedback
const formatCommentsFeedback = (): string => {
const hasTitleComment = titleComment !== null;
const hasParaComments = comments.length > 0;
if (!hasTitleComment && !hasParaComments) return '';
const lines: string[] = ['DOCUMENT COMMENTS:'];
// Title comment
if (hasTitleComment) {
lines.push(`\n[Document Title]: "${titleComment}"`);
}
// Paragraph comments
for (const c of comments) {
const sideLabel = c.side === 'left' ? 'Current' : 'New';
const paraRef = c.paragraphEnd !== c.paragraphStart
? `Paragraphs ${c.paragraphStart + 1}-${c.paragraphEnd + 1}`
: `Paragraph ${c.paragraphStart + 1}`;
lines.push(`\n[${sideLabel} - ${paraRef}]: "${c.content}"`);
}
return lines.join('\n');
};
const handleApprove = () => {
// Pass back edited title if changed (for create document)
if (isCreate && editedTitle !== null && editedTitle !== title) {
onApprove({ title: editedTitle });
} else {
onApprove();
}
};
const handleRequestChanges = () => {
const feedback = formatCommentsFeedback();
onRequestChanges(feedback);
};
// Render a single panel
const renderPanel = (paragraphs: string[], side: 'left' | 'right', emptyMessage: string) => (
<div
className={`docs-approval-panel docs-approval-panel-${side === 'left' ? 'current' : 'after'}`}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
{!isCreate && (
<div className="docs-approval-panel-header">
<span className="docs-approval-panel-label">
{side === 'left' ? 'Current' : 'After Changes'}
</span>
</div>
)}
<div className="docs-approval-panel-body">
{paragraphs.length === 0 ? (
<div className="docs-approval-empty-panel">{emptyMessage}</div>
) : (
paragraphs.map((para, idx) => {
const status = getDiffStatus(para, side);
const isSelected = isParagraphSelected(idx, side);
const paragraphHasComment = hasComment(idx, side);
const paragraphComments = getCommentsEndingAt(idx, side);
const showInputAfterPara = showCommentInput && selection?.endIndex === idx && selection?.side === side;
return (
<div key={idx} className="docs-paragraph-wrapper">
<div
className={`docs-paragraph docs-paragraph-${status} ${isSelected ? 'selected' : ''}`}
onMouseDown={(e) => handleParagraphMouseDown(idx, side, e)}
onMouseEnter={() => handleParagraphMouseEnter(idx, side)}
>
<span className="docs-paragraph-content">{para}</span>
{side === 'right' && (
<span className={`docs-comment-indicator ${paragraphHasComment ? 'has-comment' : ''} ${!isLoading ? 'can-comment' : ''}`}>
{status === 'added' ? '+' : '+'}
</span>
)}
{side === 'left' && status === 'removed' && (
<span className="docs-diff-marker">−</span>
)}
</div>
{/* Comment input */}
{showInputAfterPara && (
<div className="docs-comment-input">
<textarea
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder={selection && selection.startIndex !== selection.endIndex
? `Add feedback on paragraphs ${selection.startIndex + 1}-${selection.endIndex + 1}...`
: 'Add your feedback on this paragraph...'
}
autoFocus
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter' && e.metaKey) handleAddComment();
if (e.key === 'Escape') handleCancelComment();
}}
/>
<div className="docs-comment-actions">
<button onClick={handleCancelComment}>Cancel</button>
<button
className="primary"
onClick={handleAddComment}
disabled={!commentText.trim()}
>
Add Comment
</button>
</div>
</div>
)}
{/* Existing comments that end on this paragraph */}
{paragraphComments.map(comment => (
<div key={comment.id} className="docs-comment">
{comment.paragraphEnd !== comment.paragraphStart && (
<span className="docs-comment-paras">
Paragraphs {comment.paragraphStart + 1}-{comment.paragraphEnd + 1}
</span>
)}
<span className="docs-comment-content">{comment.content}</span>
<button
className="docs-comment-remove"
onClick={(e) => { e.stopPropagation(); handleRemoveComment(comment.id); }}
title="Remove comment"
>
×
</button>
</div>
))}
</div>
);
})
)}
</div>
</div>
);
return (
<div className="docs-approval-view">
{/* Header */}
<div className="docs-approval-header">
<div className="docs-approval-title">
<div className="docs-approval-title-row">
<McpIcon type="google-docs" size={20} />
<h3>{action || actionLabel}</h3>
</div>
{isCreate ? (
/* Editable title for create document */
<div className="docs-approval-editable-title">
<label className="docs-approval-title-label">Document Title:</label>
<div className="docs-approval-title-input-row">
<input
type="text"
className="docs-approval-title-input"
value={editedTitle ?? title}
onChange={(e) => setEditedTitle(e.target.value)}
disabled={isLoading}
placeholder="Enter document title..."
/>
{!showTitleCommentInput && !titleComment && !isLoading && (
<button
className="docs-approval-add-comment"
onClick={handleStartTitleComment}
title="Add comment"
>
+ comment
</button>
)}
</div>
{/* Title comment input */}
{showTitleCommentInput && (
<div className="docs-approval-title-comment-input">
<input
type="text"
value={titleCommentText}
onChange={(e) => setTitleCommentText(e.target.value)}
placeholder="Add feedback on the document title..."
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && titleCommentText.trim()) handleAddTitleComment();
if (e.key === 'Escape') handleCancelTitleComment();
}}
/>
<div className="docs-approval-title-comment-actions">
<button onClick={handleCancelTitleComment}>Cancel</button>
<button
className="primary"
onClick={handleAddTitleComment}
disabled={!titleCommentText.trim()}
>
Add
</button>
</div>
</div>
)}
{/* Existing title comment */}
{titleComment && !showTitleCommentInput && (
<div className="docs-approval-title-comment">
<span className="docs-approval-title-comment-content">{titleComment}</span>
<button
className="docs-approval-title-comment-edit"
onClick={handleEditTitleComment}
title="Edit comment"
>
Edit
</button>
<button
className="docs-approval-title-comment-remove"
onClick={handleRemoveTitleComment}
title="Remove comment"
>
×
</button>
</div>
)}
</div>
) : (
/* Read-only title for mutate operations */
<div className="docs-approval-doc-info">
<span className="docs-approval-doc-name">{title}</span>
</div>
)}
</div>
<div className="docs-approval-stats">
{!isCreate && currentParagraphs.length > 0 && (
<span className="stat-current">{currentParagraphs.length} current</span>
)}
{isCreate ? (
<span className="stat-additions">{newParagraphs.length} paragraphs</span>
) : isAppend ? (
<span className="stat-additions">+{newParagraphs.length} new</span>
) : (
<span className="stat-additions">{afterParagraphs.length} after</span>
)}
</div>
</div>
{/* Content - single panel for create, side by side for diff */}
<div className={`docs-approval-content ${isCreate ? 'docs-approval-content-single' : ''}`}>
{!isCreate && renderPanel(currentParagraphs, 'left', 'Empty document')}
{renderPanel(isCreate ? newParagraphs : afterParagraphs, 'right', 'No content')}
</div>
{/* Footer */}
<ApprovalFooter
onApprove={handleApprove}
onRequestChanges={handleRequestChanges}
onCancel={onCancel}
isLoading={isLoading}
approveLabel={isCreate ? 'Create Document' : isAppend ? 'Append Content' : 'Replace Content'}
commentCount={comments.length + (titleComment ? 1 : 0)}
/>
</div>
);
}
================================================
FILE: src/components/Approval/GoogleSheetsApproval.css
================================================
/**
* Google Sheets Approval View Styles
*
* Row-based diff layout for spreadsheet approval.
* Shows data in table format with cell columns.
*/
.sheets-approval-view {
display: flex;
flex-direction: column;
height: 100%;
min-height: 500px;
}
/* Header */
.sheets-approval-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-4);
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border-default);
background: var(--color-bg-secondary);
}
.sheets-approval-title {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.sheets-approval-title-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.sheets-approval-title h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
.sheets-approval-doc-info {
display: flex;
align-items: center;
gap: var(--space-2);
font-size: 13px;
}
.sheets-approval-doc-name {
color: var(--color-text-secondary);
font-weight: 500;
}
.sheets-approval-sheet-name {
color: var(--color-text-muted);
}
/* Editable title (for create spreadsheet) */
.sheets-approval-editable-title {
display: flex;
flex-direction: column;
gap: var(--space-2);
width: 450px;
}
.sheets-approval-title-label {
font-size: 12px;
color: var(--color-text-muted);
font-weight: 500;
}
.sheets-approval-title-input-row {
display: flex;
align-items: center;
gap: var(--space-2);
}
.sheets-approval-title-input {
flex: 1;
padding: var(--space-2) var(--space-3);
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
background: var(--color-bg-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
width: 100%;
}
.sheets-approval-title-input:focus {
outline: none;
border-color: var(--color-accent-primary);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
}
.sheets-approval-title-input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.sheets-approval-add-comment {
padding: 4px 8px;
font-size: 11px;
color: var(--color-text-muted);
background: transparent;
border: none;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s;
}
.sheets-approval-title-input-row:hover .sheets-approval-add-comment {
opacity: 1;
}
.sheets-approval-add-comment:hover {
color: var(--color-accent-primary);
}
/* Title comment input */
.sheets-approval-title-comment-input {
display: flex;
flex-direction: column;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-primary);
border: 1px solid var(--color-accent-primary);
border-left: 3px solid var(--color-accent-primary);
border-radius: var(--radius-sm);
}
.sheets-approval-title-comment-input input {
width: 100%;
padding: var(--space-2);
font-size: 13px;
border: none;
background: transparent;
color: var(--color-text-primary);
}
.sheets-approval-title-comment-input input:focus {
outline: none;
}
.sheets-approval-title-comment-input input::placeholder {
color: var(--color-text-muted);
}
.sheets-approval-title-comment-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
}
.sheets-approval-title-comment-actions button {
padding: 4px 10px;
font-size: 12px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.15s ease;
border: none;
}
.sheets-approval-title-comment-actions button:first-child {
background: transparent;
color: var(--color-text-muted);
}
.sheets-approval-title-comment-actions button:first-child:hover {
color: var(--color-text-primary);
}
.sheets-approval-title-comment-actions button.primary {
background: var(--color-accent-primary);
color: white;
font-weight: 500;
}
.sheets-approval-title-comment-actions button.primary:hover:not(:disabled) {
opacity: 0.9;
}
.sheets-approval-title-comment-actions button.primary:disabled {
opacity: 0.4;
cursor: default;
}
/* Existing title comment */
.sheets-approval-title-comment {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-default);
border-left: 3px solid #3b82f6;
border-radius: var(--radius-sm);
font-size: 13px;
}
.sheets-approval-title-comment-content {
flex: 1;
color: var(--color-text-primary);
}
.sheets-approval-title-comment-edit,
.sheets-approval-title-comment-remove {
padding: 2px 6px;
font-size: 11px;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s;
}
.sheets-approval-title-comment:hover .sheets-approval-title-comment-edit,
.sheets-approval-title-comment:hover .sheets-approval-title-comment-remove {
opacity: 1;
}
.sheets-approval-title-comment-edit:hover {
color: var(--color-accent-primary);
}
.sheets-approval-title-comment-remove:hover {
color: #ef4444;
}
.sheets-approval-stats {
display: flex;
gap: var(--space-3);
font-size: 12px;
}
.sheets-approval-stats .stat-current {
color: var(--color-text-muted);
}
.sheets-approval-stats .stat-additions {
color: #22c55e;
}
.sheets-approval-stats .stat-deletions {
color: #ef4444;
}
/* Content - side by side */
.sheets-approval-content {
flex: 1;
display: grid;
grid-template-columns: 1fr 1fr;
overflow: hidden;
min-height: 0;
}
.sheets-approval-content.single-panel {
grid-template-columns: 1fr;
}
/* Panels */
.sheets-approval-panel {
display: flex;
flex-direction: column;
overflow: hidden;
border-right: 1px solid var(--color-border-default);
}
.sheets-approval-panel:last-child {
border-right: none;
}
.sheets-approval-panel-header {
padding: var(--space-2) var(--space-3);
background: var(--color-bg-tertiary);
border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0;
}
.sheets-approval-panel-label {
font-size: 11px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.sheets-approval-panel-current .sheets-approval-panel-header {
background: rgba(239, 68, 68, 0.05);
}
.sheets-approval-panel-after .sheets-approval-panel-header {
background: rgba(34, 197, 94, 0.05);
}
.sheets-approval-panel-after .sheets-approval-panel-label {
color: #22c55e;
}
.sheets-approval-panel-body {
flex: 1;
overflow: auto;
background: var(--color-bg-primary);
}
.sheets-approval-empty-panel {
color: var(--color-text-muted);
font-style: italic;
font-size: 13px;
padding: var(--space-4);
text-align: center;
}
/* Table */
.sheets-table {
display: flex;
flex-direction: column;
}
/* Rows */
.sheets-row-wrapper {
display: flex;
flex-direction: column;
}
.sheets-row {
display: flex;
align-items: stretch;
min-height: 32px;
border-bottom: 1px solid var(--color-border-default);
user-select: none;
cursor: pointer;
}
.sheets-row:hover {
background: rgba(59, 130, 246, 0.06);
}
.sheets-row:hover .sheets-comment-indicator.can-comment {
opacity: 1;
}
.sheets-row.selected {
background: rgba(59, 130, 246, 0.12);
}
/* Row number */
.sheets-row-number {
width: 40px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: var(--color-text-muted);
background: var(--color-bg-tertiary);
border-right: 1px solid var(--color-border-default);
}
/* Cells container */
.sheets-row-cells {
flex: 1;
display: flex;
min-width: 0;
}
/* Individual cell */
.sheets-cell {
flex: 1;
min-width: 80px;
max-width: 200px;
padding: var(--space-2) var(--space-2);
font-family: var(--font-mono);
font-size: 12px;
color: var(--color-text-primary);
border-right: 1px solid var(--color-border-default);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sheets-cell:last-child {
border-right: none;
}
/* Comment indicator */
.sheets-comment-indicator {
width: 28px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: var(--color-text-muted);
opacity: 0;
transition: opacity 0.15s;
}
.sheets-comment-indicator.has-comment {
opacity: 1;
color: var(--color-accent-primary);
}
/* Placeholder rows for padding (to align both sides) */
.sheets-row-placeholder {
background: var(--color-bg-secondary);
opacity: 0.5;
cursor: default;
}
.sheets-row-placeholder:hover {
background: var(--color-bg-secondary);
}
.sheets-row-placeholder .sheets-row-number {
color: var(--color-text-muted);
opacity: 0.5;
}
.sheets-row-placeholder .sheets-cell {
color: transparent;
}
/* Diff status styling */
.sheets-row-added {
background: rgba(34, 197, 94, 0.08);
}
.sheets-row-added:hover {
background: rgba(34, 197, 94, 0.14);
}
.sheets-row-added .sheets-comment-indicator {
color: #22c55e;
}
.sheets-row-added .sheets-row-number {
background: rgba(34, 197, 94, 0.12);
color: #22c55e;
}
.sheets-row-removed {
background: rgba(239, 68, 68, 0.08);
}
.sheets-row-removed:hover {
background: rgba(239, 68, 68, 0.12);
}
.sheets-row-removed .sheets-cell {
text-decoration: line-through;
color: var(--color-text-muted);
}
.sheets-row-removed .sheets-row-number {
background: rgba(239, 68, 68, 0.12);
color: #ef4444;
}
/* Diff marker for removed rows */
.sheets-diff-marker {
width: 28px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: #ef4444;
}
/* Comment input */
.sheets-comment-input {
margin: var(--space-2) var(--space-3);
padding: var(--space-3);
background: var(--color-bg-primary);
border: 1px solid var(--color-accent-primary);
border-left: 3px solid var(--color-accent-primary);
border-radius: var(--radius-sm);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.sheets-comment-input textarea {
width: 100%;
padding: var(--space-2);
font-family: var(--font-sans);
font-size: 13px;
line-height: 1.4;
border: none;
background: transparent;
color: var(--color-text-primary);
resize: none;
min-height: 48px;
}
.sheets-comment-input textarea::placeholder {
color: var(--color-text-muted);
}
.sheets-comment-input textarea:focus {
outline: none;
}
.sheets-comment-actions {
display: flex;
justify-content: flex-end;
gap: var(--space-2);
padding-top: var(--space-2);
border-top: 1px solid var(--color-border-default);
}
.sheets-comment-actions button {
padding: 4px 10px;
font-size: 12px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.15s ease;
border: none;
}
.sheets-comment-actions button:first-child {
background: transparent;
color: var(--color-text-muted);
}
.sheets-comment-actions button:first-child:hover {
color: var(--color-text-primary);
}
.sheets-comment-actions button.primary {
background: var(--color-accent-primary);
color: white;
font-weight: 500;
}
.sheets-comment-actions button.primary:hover:not(:disabled) {
opacity: 0.9;
}
.sheets-comment-actions button.primary:disabled {
opacity: 0.4;
cursor: default;
}
/* Existing comment display */
.sheets-comment {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-2);
margin: var(--space-2) var(--space-3);
padding: var(--space-3);
background: var(--color-bg-secondary);
border: 1px solid var(--color-border-default);
border-left: 3px solid #3b82f6;
border-radius: var(--radius-sm);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.sheets-comment-rows {
flex-shrink: 0;
font-size: 10px;
color: var(--color-text-muted);
padding: 2px 8px;
background: rgba(59, 130, 246, 0.1);
border-radius: 10px;
align-self: flex-start;
}
.sheets-comment-content {
flex: 1;
font-size: 13px;
color: var(--color-text-primary);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.sheets-comment-remove {
position: absolute;
top: var(--space-2);
right: var(--space-2);
flex-shrink: 0;
width: 20px;
height: 20px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
font-size: 14px;
border-radius: var(--radius-sm);
opacity: 0;
transition: all 0.15s ease;
}
.sheets-comment:hover .sheets-comment-remove {
opacity: 1;
}
.sheets-comment-remove:hover {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
/* Footer override */
.sheets-approval-view .approval-footer {
padding: var(--space-3) var(--space-4);
}
================================================
FILE: src/components/Approval/GoogleSheetsApproval.tsx
================================================
/**
* Google Sheets Approval View
*
* Row-based diff for approving Google Sheets changes.
* Shows current rows vs new rows with diff highlighting.
*
* Used for: appendRows, updateCells, replaceSheetContent
* Note: createSpreadsheet shows new content only (no diff)
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { McpIcon } from '../common';
import { ApprovalFooter } from './ApprovalFooter';
import { useTitleEdit } from '../../hooks';
import type { ApprovalViewProps } from './ApprovalViewRegistry';
import './GoogleSheetsApproval.css';
interface GoogleSheetsApprovalData {
spreadsheetId?: string;
title?: string;
sheetName?: string;
currentData?: string[][];
currentRows?: string[][]; // Alternative name for currentData
newData?: string[][];
newRows?: string[][]; // Alternative name for newData
data?: string[][];
rows?: string[][];
range?: string;
action?: 'create' | 'append' | 'update' | 'replace';
url?: string;
}
interface RowComment {
id: string;
rowStart: number;
rowEnd: number;
side: 'left' | 'right';
content: string;
}
interface RowSelection {
startIndex: number;
endIndex: number;
side: 'left' | 'right';
}
export function GoogleSheetsApproval({
tool,
action,
data,
onApprove,
onRequestChanges,
onCancel,
isLoading,
}: ApprovalViewProps) {
const [comments, setComments] = useState<RowComment[]>([]);
const [selection, setSelection] = useState<RowSelection | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [showCommentInput, setShowCommentInput] = useState(false);
const [commentText, setCommentText] = useState('');
const dragStartRef = useRef<{ index: number; side: 'left' | 'right' } | null>(null);
// Editable title (for create spreadsheet) - using shared hook
const {
editedTitle,
setEditedTitle,
titleComment,
showTitleCommentInput,
titleCommentText,
handleStartTitleComment,
handleEditTitleComment,
handleAddTitleComment,
handleCancelTitleComment,
handleRemoveTitleComment,
setTitleCommentText,
} = useTitleEdit();
// Refs for synchronized scrolling
const leftPanelRef = useRef<HTMLDivElement>(null);
const rightPanelRef = useRef<HTMLDivElement>(null);
const isScrollingRef = useRef(false);
// Synchronized scroll handler
const handleScroll = useCallback((source: 'left' | 'right') => {
if (isScrollingRef.current) return;
const sourcePanel = source === 'left' ? leftPanelRef.current : rightPanelRef.current;
const targetPanel = source === 'left' ? rightPanelRef.current : leftPanelRef.current;
if (!sourcePanel || !targetPanel) return;
isScrollingRef.current = true;
targetPanel.scrollTop = sourcePanel.scrollTop;
// Reset flag after scroll completes
requestAnimationFrame(() => {
isScrollingRef.current = false;
});
}, []);
// Attach scroll listeners
useEffect(() => {
const leftPanel = leftPanelRef.current;
const rightPanel = rightPanelRef.current;
const onLeftScroll = () => handleScroll('left');
const onRightScroll = () => handleScroll('right');
leftPanel?.addEventListener('scroll', onLeftScroll);
rightPanel?.addEventListener('scroll', onRightScroll);
return () => {
leftPanel?.removeEventListener('scroll', onLeftScroll);
rightPanel?.removeEventListener('scroll', onRightScroll);
};
}, [handleScroll]);
// Parse data
let sheetData: GoogleSheetsApprovalData = {};
if (typeof data === 'string') {
try {
sheetData = JSON.parse(data);
} catch {
sheetData = {};
}
} else {
sheetData = data as GoogleSheetsApprovalData;
}
const {
title = 'Untitled Spreadsheet',
sheetName = 'Sheet1',
action: sheetAction,
} = sheetData;
// Accept multiple field names for current/new data
const currentData = sheetData.currentData || sheetData.currentRows || [];
const newData = sheetData.newData || sheetData.newRows || sheetData.data || sheetData.rows || [];
// Infer action from tool name if not explicitly provided
const inferredAction = sheetAction || (
tool.includes('createSpreadsheet') ? 'create' :
tool.includes('appendRows') ? 'append' :
tool.includes('updateCells') ? 'update' :
'replace'
);
const isCreate = inferredAction === 'create';
const isAppend = inferredAction === 'append';
const isUpdate = inferredAction === 'update';
const showSinglePanel = isCreate || (isAppend && currentData.length === 0);
const actionLabel = isCreate
? 'Create Spreadsheet'
: isAppend
? 'Append Rows'
: isUpdate
? 'Update Cells'
: 'Replace Sheet Content';
// For append, combine current + new rows
const afterRows = isAppend ? [...currentData, ...newData] : newData;
// Get max columns for consistent table width
const maxCols = Math.max(
...currentData.map(r => r.length),
...afterRows.map(r => r.length),
1
);
// Normalize row to have consistent column count
const normalizeRow = (row: string[]): string[] => {
const normalized = [...row];
while (normalized.length < maxCols) {
normalized.push('');
}
return normalized;
};
// Compare two rows for equality
const rowsEqual = (a: string[], b: string[]): boolean => {
const normA = normalizeRow(a);
const normB = normalizeRow(b);
return normA.every((cell, i) => cell === normB[i]);
};
// Get diff status for a row
const getDiffStatus = (row: string[], rowIndex: number, side: 'left' | 'right'): 'added' | 'removed' | 'unchanged' => {
if (side === 'left') {
if (isAppend || isCreate) return 'unchanged';
// Check if this row exists in new data
const existsInNew = newData.some(newRow => rowsEqual(row, newRow));
return existsInNew ? 'unchanged' : 'removed';
} else {
if (isAppend) {
// In append mode, rows from current are unchanged, new rows are added
if (rowIndex < currentData.length) return 'unchanged';
return 'added';
}
if (isCreate) {
return 'added';
}
// For replace/update, check if row exists in current
const existsInCurrent = currentData.some(currRow => rowsEqual(row, currRow));
return existsInCurrent ? 'unchanged' : 'added';
}
};
// Selection handlers - only for right side
const handleRowMouseDown = useCallback((index: number, side: 'left' | 'right', e: React.MouseEvent) => {
if (isLoading || side === 'left') return;
e.preventDefault();
dragStartRef.current = { index, side };
setIsDragging(true);
setSelection({ startIndex: index, endIndex: index, side });
setShowCommentInput(false);
}, [isLoading]);
const handleRowMouseEnter = useCallback((index: number, side: 'left' | 'right') => {
if (!isDragging || !dragStartRef.current || dragStartRef.current.side !== side) return;
const start = dragStartRef.current.index;
setSelection({
startIndex: Math.min(start, index),
endIndex: Math.max(start, index),
side,
});
}, [isDragging]);
const handleMouseUp = useCallback(() => {
if (isDragging && selection) {
setShowCommentInput(true);
}
setIsDragging(false);
dragStartRef.current = null;
}, [isDragging, selection]);
const handleAddComment = () => {
if (!selection || !commentText.trim()) return;
const newComment: RowComment = {
id: `comment-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
rowStart: selection.startIndex,
rowEnd: selection.endIndex,
side: selection.side,
content: commentText.trim(),
};
setComments(prev => [...prev, newComment]);
setSelection(null);
setShowCommentInput(false);
setCommentText('');
};
const handleRemoveComment = (id: string) => {
setComments(prev => prev.filter(c => c.id !== id));
};
const handleCancelComment = () => {
setSelection(null);
setShowCommentInput(false);
setCommentText('');
};
const isRowSelected = (index: number, side: 'left' | 'right') => {
if (!selection || selection.side !== side) return false;
return index >= selection.startIndex && index <= selection.endIndex;
};
const hasComment = useCallback((index: number, side: 'left' | 'right') => {
return comments.some(c =>
c.side === side && index >= c.rowStart && index <= c.rowEnd
);
}, [comments]);
const getCommentsEndingAt = (index: number, side: 'left' | 'right') => {
return comments.filter(c => c.rowEnd === index && c.side === side);
};
const formatCommentsFeedback = (): string => {
const hasTitleComment = titleComment !== null;
const hasRowComments = comments.length > 0;
if (!hasTitleComment && !hasRowComments) return '';
const lines: string[] = ['SPREADSHEET COMMENTS:'];
// Title comment
if (hasTitleComment) {
lines.push(`\n[Spreadsheet Name]: "${titleComment}"`);
}
// Row comments
for (const c of comments) {
const sideLabel = c.side === 'left' ? 'Current' : 'New';
const rowRef = c.rowEnd !== c.rowStart
? `Rows ${c.rowStart + 1}-${c.rowEnd + 1}`
: `Row ${c.rowStart + 1}`;
lines.push(`\n[${sideLabel} - ${rowRef}]: "${c.content}"`);
}
return lines.join('\n');
};
const handleApprove = () => {
// Pass back edited title if changed (for create spreadsheet)
if (isCreate && editedTitle !== null && editedTitle !== title) {
onApprove({ title: editedTitle });
} else {
onApprove();
}
};
const handleRequestChanges = () => {
const feedback = formatCommentsFeedback();
onRequestChanges(feedback);
};
// Render a single row
const renderRow = (row: string[], rowIndex: number, side: 'left' | 'right') => {
const status = getDiffStatus(row, rowIndex, side);
const isSelected = isRowSelected(rowIndex, side);
const rowHasComment = hasComment(rowIndex, side);
const rowComments = getCommentsEndingAt(rowIndex, side);
const showInputAfterRow = showCommentInput && selection?.endIndex === rowIndex && selection?.side === side;
const normalizedRow = normalizeRow(row);
return (
<div key={rowIndex} className="sheets-row-wrapper">
<div
className={`sheets-row sheets-row-${status} ${isSelected ? 'selected' : ''}`}
onMouseDown={(e) => handleRowMouseDown(rowIndex, side, e)}
onMouseEnter={() => handleRowMouseEnter(rowIndex, side)}
>
<span className="sheets-row-number">{rowIndex + 1}</span>
<div className="sheets-row-cells">
{normalizedRow.map((cell, cellIndex) => (
<span key={cellIndex} className="sheets-cell">
{cell || '\u00A0'}
</span>
))}
</div>
{side === 'right' && (
<span className={`sheets-comment-indicator ${rowHasComment ? 'has-comment' : ''} ${!isLoading ? 'can-comment' : ''}`}>
+
</span>
)}
{side === 'left' && status === 'removed' && (
<span className="sheets-diff-marker">−</span>
)}
</div>
{/* Comment input */}
{showInputAfterRow && (
<div className="sheets-comment-input">
<textarea
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder={selection && selection.startIndex !== selection.endIndex
? `Add feedback on rows ${selection.startIndex + 1}-${selection.endIndex + 1}...`
: 'Add your feedback on this row...'
}
autoFocus
onMouseDown={(e) => e.stopPropagation()}
onKeyDown={(e) => {
if (e.key === 'Enter' && e.metaKey) handleAddComment();
if (e.key === 'Escape') handleCancelComment();
}}
/>
<div className="sheets-comment-actions">
<button onClick={handleCancelComment}>Cancel</button>
<button
className="primary"
onClick={handleAddComment}
disabled={!commentText.trim()}
>
Add Comment
</button>
</div>
</div>
)}
{/* Existing comments */}
{rowComments.map(comment => (
<div key={comment.id} className="sheets-comment">
{comment.rowEnd !== comment.rowStart && (
<span className="sheets-comment-rows">
Rows {comment.rowStart + 1}-{comment.rowEnd + 1}
</span>
)}
<span className="sheets-comment-content">{comment.content}</span>
<button
className="sheets-comment-remove"
onClick={(e) => { e.stopPropagation(); handleRemoveComment(comment.id); }}
title="Remove comment"
>
×
</button>
</div>
))}
</div>
);
};
// Calculate max rows for padding (so both sides have same height in diff view)
const maxRows = Math.max(currentData.length, afterRows.length);
// Render placeholder row for padding
const renderPlaceholderRow = (rowIndex: number) => (
<div key={`placeholder-${rowIndex}`} className="sheets-row-wrapper">
<div className="sheets-row sheets-row-placeholder">
<span className="sheets-row-number">{rowIndex + 1}</span>
<div className="sheets-row-cells">
{Array.from({ length: maxCols }, (_, i) => (
<span key={i} className="sheets-cell">{'\u00A0'}</span>
))}
</div>
</div>
</div>
);
// Render a panel
const renderPanel = (
rows: string[][],
side: 'left' | 'right',
emptyMessage: string,
panelRef?: React.RefObject<HTMLDivElement | null>
) => {
// Calculate how many placeholder rows to add
const placeholderCount = !showSinglePanel ? maxRows - rows.length : 0;
return (
<div
className={`sheets-approval-panel sheets-approval-panel-${side === 'left' ? 'current' : 'after'}`}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
>
<div className="sheets-approval-panel-header">
<span className="sheets-approval-panel-label">
{side === 'left' ? 'Current' : showSinglePanel ? 'New Content' : 'After Changes'}
</span>
</div>
<div className="sheets-approval-panel-body" ref={panelRef}>
{rows.length === 0 && placeholderCount === 0 ? (
<div className="sheets-approval-empty-panel">{emptyMessage}</div>
) : (
<div className="sheets-table">
{rows.map((row, idx) => renderRow(row, idx, side))}
{/* Add placeholder rows to match the other side's height */}
{placeholderCount > 0 && Array.from({ length: placeholderCount }, (_, i) =>
renderPlaceholderRow(rows.length + i)
)}
</div>
)}
</div>
</div>
);
};
// Count stats
const addedRows = afterRows.filter((row, idx) => getDiffStatus(row, idx, 'right') === 'added').length;
const removedRows = currentData.filter((row, idx) => getDiffStatus(row, idx, 'left') === 'removed').length;
return (
<div className="sheets-approval-view">
{/* Header */}
<div className="sheets-approval-header">
<div className="sheets-approval-title">
<div className="sheets-approval-title-row">
<McpIcon type="google-sheets" size={20} />
<h3>{action || actionLabel}</h3>
</div>
{isCreate ? (
/* Editable title for create spreadsheet */
<div className="sheets-approval-editable-title">
<label className="sheets-approval-title-label">Spreadsheet Name:</label>
<div className="sheets-approval-title-input-row">
<input
type="text"
className="sheets-approval-title-input"
value={editedTitle ?? title}
onChange={(e) => setEditedTitle(e.target.value)}
disabled={isLoading}
placeholder="Enter spreadsheet name..."
/>
{!showTitleCommentInput && !titleComment && !isLoading && (
<button
className="sheets-approval-add-comment"
onClick={handleStartTitleComment}
title="Add comment"
>
+ comment
</button>
)}
</div>
{/* Title comment input */}
{showTitleCommentInput && (
<div className="sheets-approval-title-comment-input">
<input
type="text"
value={titleCommentText}
onChange={(e) => setTitleCommentText(e.target.value)}
placeholder="Add feedback on the spreadsheet name..."
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter' && titleCommentText.trim()) handleAddTitleComment();
if (e.key === 'Escape') handleCancelTitleComment();
}}
/>
<div className="sheets-approval-title-comment-actions">
<button onClick={handleCancelTitleComment}>Cancel</button>
<button
className="primary"
onClick={handleAddTitleComment}
disabled={!titleCommentText.trim()}
>
Add
</button>
</div>
</div>
)}
{/* Existing title comment */}
{titleComment && !showTitleCommentInput && (
<div className="sheets-approval-title-comment">
<span className="sheets-approval-title-comment-content">{titleComment}</span>
<button
className="sheets-approval-title-comment-edit"
onClick={handleEditTitleComment}
title="Edit comment"
>
Edit
</button>
<button
className="sheets-approval-title-comment-remove"
onClick={handleRemoveTitleComment}
title="Remove comment"
>
×
</button>
</div>
)}
</div>
) : (
/* Read-only title for mutate operations */
<div className="sheets-approval-doc-info">
<span className="sheets-approval-doc-name">{title}</span>
{sheetName && sheetName !== 'Sheet1' && (
<span className="sheets-approval-sheet-name">/ {sheetName}</span>
)}
</div>
)}
</div>
<div className="sheets-approval-stats">
{!showSinglePanel && currentData.length > 0 && (
<span className="stat-current">{currentData.length} rows</span>
)}
{addedRows > 0 && (
<span className="stat-additions">+{addedRows} rows</span>
)}
{removedRows > 0 && (
<span className="stat-deletions">-{removedRows} rows</span>
)}
</div>
</div>
{/* Content */}
<div className={`sheets-approval-content ${showSinglePanel ? 'single-panel' : ''}`}>
{!showSinglePanel && renderPanel(currentData, 'left', 'Empty sheet', leftPanelRef)}
{renderPanel(afterRows, 'right', 'No data', rightPanelRef)}
</div>
{/* Footer */}
<ApprovalFooter
onApprove={handleApprove}
onRequestChanges={handleRequestChanges}
onCancel={onCancel}
isLoading={isLoading}
approveLabel={isCreate ? 'Create Spreadsheet' : isAppend ? 'Append Rows' : 'Update Sheet'}
commentCount={comments.length + (titleComment ? 1 : 0)}
/>
</div>
);
}
================================================
FILE: src/components/Approval/index.ts
================================================
/**
* Approval Views Module
*
* Composable approval views for different MCP tools.
*/
export { getApprovalView } from './ApprovalViewRegistry';
export type { ApprovalViewProps } from './ApprovalViewRegistry';
export { DefaultApproval } from './DefaultApproval';
export { GitHubPRApproval } from './GitHubPRApproval';
export { GitHubPRReviewApproval } from './GitHubPRReviewApproval';
================================================
FILE: src/components/Board/Board.css
================================================
.board {
flex: 1;
display: flex;
overflow-x: auto;
padding: var(--space-4);
background: var(--color-bg-secondary);
}
.board-columns {
display: flex;
gap: var(--column-gap);
align-items: flex-start;
}
.column-wrapper {
display: flex;
}
/* Add Column Button */
.add-column-btn {
display: flex;
align-items: flex-start;
justify-content: center;
width: 28px;
min-width: 28px;
padding-top: 14px;
align-self: stretch;
min-height: 80px;
background: transparent;
border: none;
border-radius: var(--border-radius);
font-family: var(--font-mono);
color: var(--color-text-muted);
cursor: pointer;
transition: all var(--transition-fast);
opacity: 0.3;
}
.add-column-btn:hover {
opacity: 1;
color: var(--color-accent-primary);
background: var(--color-accent-muted);
}
.add-column-icon {
font-size: 16px;
line-height: 1;
}
/* Loading State */
.board-loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-bg-secondary);
}
.loading-text {
font-size: 14px;
color: var(--color-text-muted);
}
.loading-text::after {
content: '';
animation: dots 1.5s infinite;
}
@keyframes dots {
0%, 20% { content: ''; }
40% { content: '.'; }
60% { content: '..'; }
80%, 100% { content: '...'; }
}
/* Empty State */
.board-empty {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-bg-secondary);
}
.empty-content {
text-align: center;
max-width: 320px;
}
.empty-icon {
display: block;
font-size: 32px;
color: var(--color-text-muted);
margin-bottom: var(--space-4);
}
.empty-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: var(--space-2);
}
.empty-text {
font-size: 14px;
color: var(--color-text-muted);
line-height: 1.5;
}
/* ============================================
Mobile Styles - Horizontal Snap Scrolling
============================================ */
@media (max-width: 767px) {
.board {
padding: var(--space-2) 0;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
/* Hide scrollbar but keep functionality */
scrollbar-width: none;
-ms-overflow-style: none;
}
.board::-webkit-scrollbar {
display: none;
}
.board-columns {
gap: 0;
padding: 0 var(--space-mobile-gutter);
}
.column-wrapper {
flex: 0 0 auto;
width: var(--mobile-column-width);
scroll-snap-align: center;
scroll-snap-stop: always;
padding: 0 var(--space-1);
}
/* Add column button - full card style */
.add-column-btn {
flex: 0 0 auto;
width: var(--mobile-column-width);
min-width: var(--mobile-column-width);
min-height: 120px;
margin-right: var(--space-mobile-gutter);
scroll-snap-align: center;
opacity: 0.6;
border: 2px dashed var(--color-border-default);
background: var(--color-bg-tertiary);
padding-top: var(--space-6);
align-items: flex-start;
}
.add-column-btn:active {
opacity: 1;
background: var(--color-accent-muted);
border-color: var(--color-accent-primary);
}
.add-column-icon {
font-size: 24px;
}
/* Empty/loading states */
.board-empty,
.board-loading {
padding: var(--space-4);
}
.empty-content {
max-width: 100%;
padding: 0 var(--space-4);
}
}
================================================
FILE: src/components/Board/Board.tsx
================================================
import { useEffect, useState, useCallback, type DragEvent } from 'react';
import { useParams } from 'react-router-dom';
import { useBoard } from '../../context/BoardContext';
import { Column } from '../Column/Column';
import './Board.css';
export function Board() {
const { boardId } = useParams<{ boardId: string }>();
const { activeBoard, loading, loadBoard, createColumn, columnDragState, setColumnDragState, moveColumn } = useBoard();
const [newColumnId, setNewColumnId] = useState<string | null>(null);
const [dropTargetIndex, setDropTargetIndex] = useState<number | null>(null);
// Load board from URL param on mount or when boardId changes
useEffect(() => {
if (boardId && boardId !== activeBoard?.id) {
loadBoard(b
gitextract_09zdyiuf/ ├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── eslint.config.js ├── index.html ├── package.json ├── src/ │ ├── App.css │ ├── App.tsx │ ├── api/ │ │ └── client.ts │ ├── components/ │ │ ├── Approval/ │ │ │ ├── Approval.css │ │ │ ├── ApprovalFooter.tsx │ │ │ ├── ApprovalViewRegistry.tsx │ │ │ ├── DefaultApproval.tsx │ │ │ ├── EmailApproval.css │ │ │ ├── EmailApproval.tsx │ │ │ ├── GitHubPRApproval.css │ │ │ ├── GitHubPRApproval.tsx │ │ │ ├── GitHubPRReviewApproval.css │ │ │ ├── GitHubPRReviewApproval.tsx │ │ │ ├── GoogleDocsApproval.css │ │ │ ├── GoogleDocsApproval.tsx │ │ │ ├── GoogleSheetsApproval.css │ │ │ ├── GoogleSheetsApproval.tsx │ │ │ └── index.ts │ │ ├── Board/ │ │ │ ├── Board.css │ │ │ └── Board.tsx │ │ ├── Column/ │ │ │ ├── Column.css │ │ │ └── Column.tsx │ │ ├── CommandPalette/ │ │ │ ├── CommandPalette.css │ │ │ ├── CommandPalette.tsx │ │ │ └── index.ts │ │ ├── CommentableText/ │ │ │ ├── CommentableText.css │ │ │ └── CommentableText.tsx │ │ ├── DiffViewer/ │ │ │ ├── DiffViewer.css │ │ │ ├── DiffViewer.tsx │ │ │ └── index.ts │ │ ├── GitHubCallback.tsx │ │ ├── GoogleCallback.tsx │ │ ├── Header/ │ │ │ ├── Header.css │ │ │ ├── Header.tsx │ │ │ └── WeftLogo.tsx │ │ ├── Home/ │ │ │ ├── Home.css │ │ │ └── Home.tsx │ │ ├── MCP/ │ │ │ ├── MCP.css │ │ │ ├── MCPOAuthCallback.css │ │ │ ├── MCPOAuthCallback.tsx │ │ │ ├── MCPServerConnect.css │ │ │ ├── MCPServerConnect.tsx │ │ │ └── index.ts │ │ ├── Settings/ │ │ │ ├── AccountsSection.css │ │ │ ├── AccountsSection.tsx │ │ │ ├── BoardSettings.css │ │ │ ├── BoardSettings.tsx │ │ │ ├── MCPSection.css │ │ │ ├── MCPSection.tsx │ │ │ ├── index.ts │ │ │ └── sections/ │ │ │ ├── CredentialsSection.tsx │ │ │ ├── DangerSection.tsx │ │ │ ├── GeneralSection.tsx │ │ │ ├── IntegrationsSection.tsx │ │ │ └── index.ts │ │ ├── Task/ │ │ │ ├── AgentSection.css │ │ │ ├── AgentSection.tsx │ │ │ ├── RunHistory.css │ │ │ ├── RunHistory.tsx │ │ │ ├── TaskCard.css │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskModal.css │ │ │ └── TaskModal.tsx │ │ ├── Toast/ │ │ │ ├── Toast.css │ │ │ ├── Toast.tsx │ │ │ ├── ToastContainer.tsx │ │ │ ├── WorkflowToastListener.tsx │ │ │ └── index.ts │ │ ├── Workflow/ │ │ │ ├── EmailViewerModal.css │ │ │ ├── EmailViewerModal.tsx │ │ │ ├── PlanReviewView.css │ │ │ ├── PlanReviewView.tsx │ │ │ ├── Workflow.css │ │ │ ├── WorkflowProgress.tsx │ │ │ └── index.ts │ │ └── common/ │ │ ├── Button.css │ │ ├── Button.tsx │ │ ├── ErrorBoundary.css │ │ ├── ErrorBoundary.tsx │ │ ├── Input.css │ │ ├── Input.tsx │ │ ├── McpIcon.tsx │ │ ├── Modal.css │ │ ├── Modal.tsx │ │ ├── RichTextEditor.css │ │ ├── RichTextEditor.tsx │ │ └── index.ts │ ├── constants.ts │ ├── context/ │ │ ├── AuthContext.tsx │ │ ├── BoardContext.tsx │ │ ├── ToastContext.tsx │ │ └── boardReducer.ts │ ├── hooks/ │ │ ├── index.ts │ │ ├── useApprovalComments.ts │ │ ├── useIsMobile.ts │ │ └── useUrlDetection.ts │ ├── index.css │ ├── main.tsx │ ├── types/ │ │ └── index.ts │ ├── utils/ │ │ └── diffParser.ts │ └── vite-env.d.ts ├── tests/ │ ├── mocks/ │ │ └── cloudflare-sandbox.ts │ └── worker/ │ ├── auth.test.ts │ ├── board-access.integration.test.ts │ └── user-isolation.test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.worker.json ├── vite.config.ts ├── vitest.config.ts ├── vitest.config.workers.ts ├── worker/ │ ├── BoardDO.ts │ ├── UserDO.ts │ ├── auth.ts │ ├── constants.ts │ ├── db/ │ │ ├── index.ts │ │ └── schema.ts │ ├── github/ │ │ ├── GitHubMCP.ts │ │ ├── githubTools.ts │ │ ├── index.ts │ │ └── oauth.ts │ ├── google/ │ │ ├── DocsMCP.ts │ │ ├── GmailMCP.ts │ │ ├── SheetsMCP.ts │ │ ├── docsTools.ts │ │ ├── gmailTools.ts │ │ ├── index.ts │ │ ├── markdownToDocs.ts │ │ ├── oauth.ts │ │ └── sheetsTools.ts │ ├── handlers/ │ │ ├── boards.ts │ │ ├── oauth.ts │ │ └── workflows.ts │ ├── index.ts │ ├── mcp/ │ │ ├── AccountMCPRegistry.ts │ │ ├── MCPBridge.ts │ │ ├── MCPClient.ts │ │ ├── SchemaConverter.ts │ │ ├── index.ts │ │ └── oauth/ │ │ ├── discovery.ts │ │ ├── flow.ts │ │ ├── index.ts │ │ ├── pkce.ts │ │ └── types.ts │ ├── sandbox/ │ │ ├── SandboxMCP.ts │ │ └── sandboxTools.ts │ ├── services/ │ │ ├── BoardService.ts │ │ ├── CredentialService.ts │ │ ├── MCPOAuthService.ts │ │ ├── MCPService.ts │ │ ├── ScheduleService.ts │ │ ├── WorkflowService.ts │ │ └── index.ts │ ├── utils/ │ │ ├── crypto.ts │ │ ├── logger.ts │ │ ├── oauth-state.ts │ │ ├── response.ts │ │ ├── transformations.ts │ │ └── zodTools.ts │ └── workflows/ │ └── AgentWorkflow.ts ├── worker-configuration.d.ts └── wrangler.jsonc
SYMBOL INDEX (1515 symbols across 100 files)
FILE: src/App.tsx
constant CALLBACK_ROUTES (line 18) | const CALLBACK_ROUTES = ['/github/callback', '/google/callback', '/mcp/o...
function AuthGate (line 20) | function AuthGate({ children }: { children: React.ReactNode }) {
function AppContent (line 44) | function AppContent() {
function App (line 137) | function App() {
FILE: src/api/client.ts
constant API_BASE (line 17) | const API_BASE = '/api';
function getMe (line 23) | async function getMe(): Promise<ApiResponse<User>> {
function request (line 28) | async function request<T>(
type BoardWithDetails (line 61) | interface BoardWithDetails extends Board {
function getBoards (line 66) | async function getBoards(): Promise<ApiResponse<Board[]>> {
function getBoard (line 70) | async function getBoard(id: string): Promise<ApiResponse<BoardWithDetail...
function createBoard (line 74) | async function createBoard(name: string): Promise<ApiResponse<BoardWithD...
function updateBoard (line 81) | async function updateBoard(
function deleteBoard (line 91) | async function deleteBoard(id: string): Promise<ApiResponse<void>> {
function createColumn (line 101) | async function createColumn(
function updateColumn (line 111) | async function updateColumn(
function deleteColumn (line 122) | async function deleteColumn(boardId: string, id: string): Promise<ApiRes...
function createTask (line 132) | async function createTask(
function getTask (line 147) | async function getTask(boardId: string, id: string): Promise<ApiResponse...
function updateTask (line 151) | async function updateTask(
function deleteTask (line 167) | async function deleteTask(boardId: string, id: string): Promise<ApiRespo...
function moveTask (line 173) | async function moveTask(
function getCredentials (line 189) | async function getCredentials(
function createCredential (line 195) | async function createCredential(
function deleteCredential (line 210) | async function deleteCredential(
type GitHubRepo (line 223) | interface GitHubRepo {
function getGitHubOAuthUrl (line 233) | async function getGitHubOAuthUrl(
function getGitHubRepos (line 239) | async function getGitHubRepos(
function getGoogleOAuthUrl (line 249) | async function getGoogleOAuthUrl(
function getMCPServers (line 259) | async function getMCPServers(
function getMCPServer (line 265) | async function getMCPServer(
function createMCPServer (line 272) | async function createMCPServer(
function updateMCPServer (line 289) | async function updateMCPServer(
function deleteMCPServer (line 308) | async function deleteMCPServer(
function getMCPServerTools (line 317) | async function getMCPServerTools(
function connectMCPServer (line 324) | async function connectMCPServer(
function cacheMCPServerTools (line 334) | async function cacheMCPServerTools(
function createAccountMCP (line 353) | async function createAccountMCP(
function discoverMCPOAuth (line 371) | async function discoverMCPOAuth(
function getMCPOAuthUrl (line 389) | async function getMCPOAuthUrl(
function exchangeMCPOAuthCode (line 401) | async function exchangeMCPOAuthCode(
function getBoardWorkflowPlans (line 418) | async function getBoardWorkflowPlans(
function getTaskWorkflowPlan (line 424) | async function getTaskWorkflowPlan(
function createWorkflowPlan (line 431) | async function createWorkflowPlan(
function getWorkflowPlan (line 446) | async function getWorkflowPlan(
function updateWorkflowPlan (line 453) | async function updateWorkflowPlan(
function deleteWorkflowPlan (line 471) | async function deleteWorkflowPlan(
function approveWorkflowPlan (line 480) | async function approveWorkflowPlan(
function cancelWorkflow (line 489) | async function cancelWorkflow(
function resolveWorkflowCheckpoint (line 498) | async function resolveWorkflowCheckpoint(
function getWorkflowLogs (line 513) | async function getWorkflowLogs(
function generateWorkflowPlan (line 525) | async function generateWorkflowPlan(
type LinkMetadata (line 538) | interface LinkMetadata {
function getLinkMetadata (line 544) | async function getLinkMetadata(
function getScheduledTasks (line 561) | async function getScheduledTasks(
function getScheduledRuns (line 570) | async function getScheduledRuns(
function getRunTasks (line 582) | async function getRunTasks(
function deleteScheduledRun (line 592) | async function deleteScheduledRun(
function getChildTasks (line 604) | async function getChildTasks(
function triggerScheduledRun (line 614) | async function triggerScheduledRun(
FILE: src/components/Approval/ApprovalFooter.tsx
type ApprovalFooterProps (line 14) | interface ApprovalFooterProps {
function ApprovalFooter (line 27) | function ApprovalFooter({
FILE: src/components/Approval/ApprovalViewRegistry.tsx
type ApprovalViewProps (line 18) | interface ApprovalViewProps {
type ApprovalViewComponent (line 29) | type ApprovalViewComponent = React.FC<ApprovalViewProps>;
constant APPROVAL_VIEW_REGISTRY (line 34) | const APPROVAL_VIEW_REGISTRY: Record<string, ApprovalViewComponent> = {
function getApprovalView (line 55) | function getApprovalView(toolName: string): ApprovalViewComponent {
FILE: src/components/Approval/DefaultApproval.tsx
constant TOOL_CONFIG (line 20) | const TOOL_CONFIG: Record<string, {
function getToolConfig (line 55) | function getToolConfig(toolName: string) {
function getFieldLabel (line 64) | function getFieldLabel(toolName: string, fieldKey: string): string {
type TextCommentsMap (line 78) | interface TextCommentsMap {
function DefaultApproval (line 82) | function DefaultApproval({
FILE: src/components/Approval/EmailApproval.tsx
type EmailApprovalData (line 18) | interface EmailApprovalData {
function EmailApproval (line 26) | function EmailApproval({
type RecipientFieldProps (line 251) | interface RecipientFieldProps {
function RecipientField (line 268) | function RecipientField({
FILE: src/components/Approval/GitHubPRApproval.tsx
type PRApprovalData (line 17) | interface PRApprovalData {
function GitHubPRApproval (line 34) | function GitHubPRApproval({
FILE: src/components/Approval/GitHubPRReviewApproval.tsx
type ReviewDecision (line 20) | type ReviewDecision = 'APPROVE' | 'REQUEST_CHANGES' | 'COMMENT';
type ReviewCommentSide (line 21) | type ReviewCommentSide = 'LEFT' | 'RIGHT';
type ReviewCommentDraft (line 23) | interface ReviewCommentDraft {
type PRReviewApprovalData (line 33) | interface PRReviewApprovalData {
function serializeComments (line 56) | function serializeComments(comments: DiffComment[]): string {
function stripCodeFences (line 70) | function stripCodeFences(text: string): string {
function toDiffComment (line 74) | function toDiffComment(comment: ReviewCommentDraft): DiffComment {
function toReviewComment (line 95) | function toReviewComment(comment: DiffComment): ReviewCommentDraft | null {
function formatLineRef (line 131) | function formatLineRef(comment: DiffComment): string {
function GitHubPRReviewApproval (line 138) | function GitHubPRReviewApproval({
FILE: src/components/Approval/GoogleDocsApproval.tsx
type GoogleDocsApprovalData (line 17) | interface GoogleDocsApprovalData {
type TextComment (line 27) | interface TextComment {
type ParagraphSelection (line 35) | interface ParagraphSelection {
function GoogleDocsApproval (line 41) | function GoogleDocsApproval({
FILE: src/components/Approval/GoogleSheetsApproval.tsx
type GoogleSheetsApprovalData (line 18) | interface GoogleSheetsApprovalData {
type RowComment (line 33) | interface RowComment {
type RowSelection (line 41) | interface RowSelection {
function GoogleSheetsApproval (line 47) | function GoogleSheetsApproval({
FILE: src/components/Board/Board.tsx
function Board (line 7) | function Board() {
FILE: src/components/Column/Column.tsx
type ColumnProps (line 9) | interface ColumnProps {
function Column (line 17) | function Column({ column, onStartEdit, onColumnDragStart, onColumnDragEn...
FILE: src/components/CommandPalette/CommandPalette.tsx
type CommandPaletteProps (line 6) | interface CommandPaletteProps {
type ResultItem (line 12) | type ResultItem = {
function CommandPalette (line 20) | function CommandPalette({ isOpen, onClose, onNewTask }: CommandPalettePr...
FILE: src/components/CommentableText/CommentableText.tsx
type LineSelection (line 15) | interface LineSelection {
type CommentableTextProps (line 20) | interface CommentableTextProps {
function CommentableText (line 35) | function CommentableText({
function formatTextComments (line 219) | function formatTextComments(comments: TextComment[]): string {
FILE: src/components/DiffViewer/DiffViewer.tsx
function SimpleMarkdown (line 10) | function SimpleMarkdown({ text }: { text: string }) {
function renderInline (line 38) | function renderInline(text: string, keyStart: number): React.ReactNode[] {
type LineSelection (line 65) | interface LineSelection {
type DiffViewerProps (line 73) | interface DiffViewerProps {
function DiffViewer (line 84) | function DiffViewer({
type DiffHunkViewProps (line 312) | interface DiffHunkViewProps {
function DiffHunkView (line 335) | function DiffHunkView({
type DiffLineViewProps (line 413) | interface DiffLineViewProps {
function DiffLineView (line 436) | function DiffLineView({
type FileTreeProps (line 615) | interface FileTreeProps {
function FileTree (line 622) | function FileTree({ files, selectedFile, onSelect, noteCounts }: FileTre...
FILE: src/components/GitHubCallback.tsx
function GitHubCallback (line 9) | function GitHubCallback() {
FILE: src/components/GoogleCallback.tsx
function GoogleCallback (line 9) | function GoogleCallback() {
FILE: src/components/Header/Header.tsx
function Header (line 11) | function Header() {
FILE: src/components/Header/WeftLogo.tsx
type WeftLogoProps (line 9) | interface WeftLogoProps {
function WeftLogo (line 13) | function WeftLogo({ onClick }: WeftLogoProps) {
FILE: src/components/Home/Home.tsx
function Home (line 7) | function Home() {
FILE: src/components/MCP/MCPOAuthCallback.tsx
function MCPOAuthCallback (line 14) | function MCPOAuthCallback() {
FILE: src/components/MCP/MCPServerConnect.tsx
type MCPServerConnectProps (line 7) | interface MCPServerConnectProps {
type TransportType (line 14) | type TransportType = 'streamable-http' | 'sse';
function MCPServerConnect (line 16) | function MCPServerConnect({ boardId, onClose, onServerAdded, inline }: M...
FILE: src/components/Settings/AccountsSection.tsx
constant ACCOUNTS (line 5) | const ACCOUNTS = [
type AccountsSectionProps (line 33) | interface AccountsSectionProps {
function AccountsSection (line 40) | function AccountsSection({
FILE: src/components/Settings/BoardSettings.tsx
type SettingsTab (line 13) | type SettingsTab = 'general' | 'credentials' | 'integrations' | 'danger';
constant GEAR_ICON (line 15) | const GEAR_ICON = (
constant LOCK_ICON (line 22) | const LOCK_ICON = (
constant INTEGRATIONS_ICON (line 29) | const INTEGRATIONS_ICON = (
constant WARNING_ICON (line 38) | const WARNING_ICON = (
constant TABS (line 46) | const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
constant TAB_IDS (line 53) | const TAB_IDS = new Set<string>(TABS.map((t) => t.id));
function isSettingsTab (line 55) | function isSettingsTab(value: string): value is SettingsTab {
type BoardSettingsProps (line 62) | interface BoardSettingsProps {
function BoardSettings (line 68) | function BoardSettings({ isOpen, onClose, initialTab }: BoardSettingsPro...
FILE: src/components/Settings/MCPSection.tsx
function ToolsMore (line 9) | function ToolsMore({ count, tools }: { count: number; tools: string[] }) {
constant ACCOUNT_MCPS (line 47) | const ACCOUNT_MCPS = [
type MCPSectionProps (line 59) | interface MCPSectionProps {
constant GITHUB_ICON (line 67) | const GITHUB_ICON = (
constant MCP_ICON (line 73) | const MCP_ICON = (
constant GOOGLE_ICON (line 82) | const GOOGLE_ICON = (
function MCPSection (line 91) | function MCPSection({
FILE: src/components/Settings/sections/CredentialsSection.tsx
type CredentialsSectionProps (line 7) | interface CredentialsSectionProps {
function CredentialsSection (line 17) | function CredentialsSection({
FILE: src/components/Settings/sections/DangerSection.tsx
type DangerSectionProps (line 5) | interface DangerSectionProps {
function DangerSection (line 10) | function DangerSection({ board, onDelete }: DangerSectionProps) {
FILE: src/components/Settings/sections/GeneralSection.tsx
type GeneralSectionProps (line 5) | interface GeneralSectionProps {
function GeneralSection (line 10) | function GeneralSection({ board, onRename }: GeneralSectionProps) {
FILE: src/components/Settings/sections/IntegrationsSection.tsx
function ToolsMore (line 6) | function ToolsMore({ count, tools }: { count: number; tools: string[] }) {
constant ACCOUNT_MCPS (line 40) | const ACCOUNT_MCPS = [
constant GITHUB_ICON (line 59) | const GITHUB_ICON = (
constant MCP_ICON (line 65) | const MCP_ICON = (
constant GOOGLE_ICON (line 74) | const GOOGLE_ICON = (
constant MAX_VISIBLE_TOOLS (line 83) | const MAX_VISIBLE_TOOLS = 3;
constant GOOGLE_SERVICES (line 85) | const GOOGLE_SERVICES = new Set(['Gmail', 'Google Docs', 'Google Sheets']);
constant ACCOUNT_DESCRIPTIONS (line 87) | const ACCOUNT_DESCRIPTIONS: Record<string, { name: string; description: ...
function AddIntegrationView (line 92) | function AddIntegrationView({
type IntegrationsSectionProps (line 238) | interface IntegrationsSectionProps {
function IntegrationsSection (line 248) | function IntegrationsSection({
FILE: src/components/Task/AgentSection.tsx
type AgentSectionProps (line 7) | interface AgentSectionProps {
constant PLAYFUL_SENTENCES (line 18) | const PLAYFUL_SENTENCES = [
function getIconType (line 27) | function getIconType(name: string): 'gmail' | 'google-docs' | 'google-sh...
constant BUILTIN_TOOLS (line 38) | const BUILTIN_TOOLS = [
constant COMMON_TIMEZONES (line 43) | const COMMON_TIMEZONES = [
constant DAYS_OF_WEEK (line 56) | const DAYS_OF_WEEK = [
function formatScheduleSummary (line 66) | function formatScheduleSummary(config: ScheduleConfig): string {
function parse24HourTime (line 74) | function parse24HourTime(time: string): { hour: number; minute: number; ...
function to24HourTime (line 81) | function to24HourTime(hour: number, minute: number, period: 'AM' | 'PM')...
function AgentSection (line 88) | function AgentSection({
FILE: src/components/Task/RunHistory.tsx
type RunHistoryProps (line 7) | interface RunHistoryProps {
function formatDate (line 14) | function formatDate(dateStr: string): string {
function RunHistory (line 31) | function RunHistory({ boardId, taskId, isOpen, onClose }: RunHistoryProp...
FILE: src/components/Task/TaskCard.tsx
function stripMarkdownToText (line 9) | function stripMarkdownToText(text: string): string {
function formatScheduleFrequency (line 18) | function formatScheduleFrequency(config: ScheduleConfig): string {
type TaskCardProps (line 43) | interface TaskCardProps {
type ContextMenuState (line 48) | interface ContextMenuState {
function TaskCard (line 54) | function TaskCard({ task }: TaskCardProps) {
FILE: src/components/Task/TaskModal.tsx
type TaskModalView (line 13) | type TaskModalView = 'main' | 'plan-review' | 'checkpoint-review' | 'ema...
type TaskModalProps (line 15) | interface TaskModalProps {
function TaskModal (line 21) | function TaskModal({ task, isOpen, onClose }: TaskModalProps) {
FILE: src/components/Toast/Toast.tsx
type ToastProps (line 5) | interface ToastProps {
constant ICONS (line 10) | const ICONS: Record<ToastVariant, string> = {
function Toast (line 17) | function Toast({ toast, onClose }: ToastProps) {
FILE: src/components/Toast/ToastContainer.tsx
function ToastContainer (line 5) | function ToastContainer() {
FILE: src/components/Toast/WorkflowToastListener.tsx
function WorkflowToastListener (line 10) | function WorkflowToastListener() {
function getTaskTitle (line 68) | function getTaskTitle(plan: WorkflowPlan, tasks: Task[] | null): string {
FILE: src/components/Workflow/EmailViewerModal.tsx
type EmailViewerProps (line 12) | interface EmailViewerProps {
function formatDate (line 16) | function formatDate(isoString?: string) {
function EmailViewer (line 32) | function EmailViewer({ content }: EmailViewerProps) {
FILE: src/components/Workflow/PlanReviewView.tsx
type PlanReviewViewProps (line 6) | interface PlanReviewViewProps {
function PlanReviewView (line 18) | function PlanReviewView({
FILE: src/components/Workflow/WorkflowProgress.tsx
type WorkflowProgressProps (line 7) | interface WorkflowProgressProps {
function WorkflowProgress (line 20) | function WorkflowProgress({
function getPRNumber (line 376) | function getPRNumber(url: string): string | null {
function truncateTitle (line 381) | function truncateTitle(title: string, maxLen = 30): string {
function getStepIcon (line 385) | function getStepIcon(status: WorkflowStepType['status']): string {
function ArtifactButton (line 439) | function ArtifactButton({
function WorkflowBadge (line 571) | function WorkflowBadge({
FILE: src/components/common/Button.tsx
type ButtonProps (line 4) | interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
FILE: src/components/common/ErrorBoundary.tsx
type ErrorBoundaryProps (line 4) | interface ErrorBoundaryProps {
type ErrorBoundaryState (line 9) | interface ErrorBoundaryState {
class ErrorBoundary (line 18) | class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryS...
method constructor (line 19) | constructor(props: ErrorBoundaryProps) {
method getDerivedStateFromError (line 24) | static getDerivedStateFromError(error: Error): ErrorBoundaryState {
method componentDidCatch (line 28) | componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
method render (line 37) | render(): ReactNode {
FILE: src/components/common/Input.tsx
type InputProps (line 4) | interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
type TextareaProps (line 34) | interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaEleme...
FILE: src/components/common/McpIcon.tsx
type McpIconProps (line 5) | interface McpIconProps {
function McpIcon (line 11) | function McpIcon({ type, size = 20, className = '' }: McpIconProps) {
type AgentIconProps (line 156) | interface AgentIconProps {
function AgentIcon (line 161) | function AgentIcon({ size = 20, className = '' }: AgentIconProps) {
function getIconTypeFromTool (line 195) | function getIconTypeFromTool(toolName: string): McpIconProps['type'] {
FILE: src/components/common/Modal.tsx
type ModalProps (line 5) | interface ModalProps {
function Modal (line 20) | function Modal({ isOpen, onClose, title, titleBadge, children, width = '...
FILE: src/components/common/RichTextEditor.tsx
constant PILL_REGEX (line 23) | const PILL_REGEX = /\[pill:([^:]+):([^\]]+)\]\(([^)]+)\)/g;
constant MARKDOWN_LINK_REGEX (line 26) | const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g;
constant URL_REGEX (line 30) | const URL_REGEX = /https?:\/\/[^\s<>")\]]+(?<![.,;:!?)\]])/g;
constant ENRICHABLE_URL_PATTERNS (line 33) | const ENRICHABLE_URL_PATTERNS = [
type PendingUrl (line 39) | interface PendingUrl {
type RichTextEditorProps (line 47) | interface RichTextEditorProps {
type PillData (line 63) | interface PillData {
type LinkData (line 69) | interface LinkData {
type Segment (line 74) | type Segment = {
function createLinkElement (line 84) | function createLinkElement(url: string, displayText?: string): HTMLAncho...
function parseLinksInText (line 116) | function parseLinksInText(text: string): Segment[] {
function parseMarkdown (line 152) | function parseMarkdown(markdown: string): Segment[] {
function htmlToMarkdown (line 195) | function htmlToMarkdown(element: HTMLElement): string {
function getIconType (line 272) | function getIconType(type: LinkPillType): 'google-docs' | 'google-sheets...
function RichTextEditor (line 285) | function RichTextEditor({
FILE: src/constants.ts
constant CREDENTIAL_TYPES (line 6) | const CREDENTIAL_TYPES = {
type CredentialType (line 12) | type CredentialType = typeof CREDENTIAL_TYPES[keyof typeof CREDENTIAL_TY...
FILE: src/context/AuthContext.tsx
type AuthState (line 11) | interface AuthState {
type AuthContextValue (line 17) | interface AuthContextValue extends AuthState {
function AuthProvider (line 24) | function AuthProvider({ children }: { children: React.ReactNode }) {
function useAuth (line 77) | function useAuth(): AuthContextValue {
FILE: src/context/BoardContext.tsx
type BoardContextValue (line 19) | interface BoardContextValue extends Omit<BoardState, 'workflowLogs'> {
function BoardProvider (line 57) | function BoardProvider({ children }: { children: ReactNode }) {
function useBoard (line 484) | function useBoard(): BoardContextValue {
FILE: src/context/ToastContext.tsx
type ToastType (line 13) | type ToastType = 'success' | 'error' | 'warning' | 'info';
type Toast (line 15) | interface Toast {
type ToastContextValue (line 23) | interface ToastContextValue {
constant DEFAULT_DURATION (line 39) | const DEFAULT_DURATION = 4000;
function ToastProvider (line 41) | function ToastProvider({ children }: { children: ReactNode }) {
function useToast (line 67) | function useToast() {
FILE: src/context/boardReducer.ts
type BoardState (line 14) | interface BoardState {
type BoardAction (line 48) | type BoardAction =
function boardReducer (line 77) | function boardReducer(state: BoardState, action: BoardAction): BoardState {
FILE: src/hooks/useApprovalComments.ts
type TitleEditState (line 17) | interface TitleEditState {
type DragState (line 24) | interface DragState<T> {
type CommentInputState (line 30) | interface CommentInputState {
type UseTitleEditOptions (line 39) | interface UseTitleEditOptions {
type UseTitleEditResult (line 43) | interface UseTitleEditResult {
function useTitleEdit (line 58) | function useTitleEdit(options: UseTitleEditOptions = {}): UseTitleEditRe...
type SelectionRange (line 113) | interface SelectionRange {
type UseRowSelectionResult (line 119) | interface UseRowSelectionResult {
function useRowSelection (line 129) | function useRowSelection(): UseRowSelectionResult {
type UseCommentInputResult (line 173) | interface UseCommentInputResult {
function useCommentInput (line 182) | function useCommentInput(): UseCommentInputResult {
type CommentBase (line 218) | interface CommentBase {
type UseCommentsResult (line 222) | interface UseCommentsResult<T extends CommentBase> {
function useComments (line 230) | function useComments<T extends CommentBase>(): UseCommentsResult<T> {
type FieldComment (line 262) | interface FieldComment {
type UseFieldCommentsResult (line 269) | interface UseFieldCommentsResult {
function useFieldComments (line 284) | function useFieldComments(): UseFieldCommentsResult {
function generateCommentId (line 366) | function generateCommentId(): string {
FILE: src/hooks/useIsMobile.ts
constant MOBILE_BREAKPOINT (line 3) | const MOBILE_BREAKPOINT = 768;
function useIsMobile (line 9) | function useIsMobile(breakpoint = MOBILE_BREAKPOINT): boolean {
FILE: src/hooks/useUrlDetection.ts
type PendingUrl (line 10) | interface PendingUrl {
type UseUrlDetectionResult (line 15) | interface UseUrlDetectionResult {
constant URL_REGEX (line 28) | const URL_REGEX = /https?:\/\/[^\s<>"\]]+/g;
function extractUrl (line 33) | function extractUrl(text: string): string | null {
function toPillSyntax (line 41) | function toPillSyntax(pending: PendingUrl): string {
function useUrlDetection (line 48) | function useUrlDetection(boardId: string): UseUrlDetectionResult {
FILE: src/types/index.ts
type User (line 7) | interface User {
type Board (line 13) | interface Board {
type Column (line 23) | interface Column {
type Task (line 30) | interface Task {
type TaskPriority (line 46) | type TaskPriority = 'low' | 'medium' | 'high' | 'critical';
type ScheduleConfig (line 52) | interface ScheduleConfig {
type ScheduledRunStatus (line 62) | type ScheduledRunStatus = 'pending' | 'running' | 'completed' | 'failed';
type ScheduledRun (line 64) | interface ScheduledRun {
type BoardToolConfig (line 82) | interface BoardToolConfig {
type ToolDefinition (line 88) | interface ToolDefinition {
type CredentialRef (line 100) | interface CredentialRef {
type BoardCredential (line 107) | interface BoardCredential {
type SandboxSettings (line 118) | interface SandboxSettings {
type TaskContext (line 124) | interface TaskContext {
type ApiResponse (line 136) | interface ApiResponse<T> {
type ApiError (line 142) | interface ApiError {
type DiffComment (line 151) | interface DiffComment {
type TextComment (line 166) | interface TextComment {
type DragState (line 177) | interface DragState {
type ColumnDragState (line 183) | interface ColumnDragState {
type BoardViewState (line 188) | interface BoardViewState {
type MCPServer (line 199) | interface MCPServer {
type LinkPillType (line 218) | type LinkPillType = 'google_doc' | 'google_sheet' | 'github_pr' | 'githu...
type MCPUrlPattern (line 220) | interface MCPUrlPattern {
type MCPTool (line 229) | interface MCPTool {
type JSONSchema (line 238) | interface JSONSchema {
type WorkflowPlanStatus (line 253) | type WorkflowPlanStatus =
type WorkflowArtifact (line 262) | interface WorkflowArtifact {
type WorkflowResult (line 278) | interface WorkflowResult {
type WorkflowPlan (line 285) | interface WorkflowPlan {
type WorkflowStepType (line 300) | type WorkflowStepType = 'tool_call' | 'checkpoint' | 'internal' | 'agent...
type WorkflowStepStatus (line 301) | type WorkflowStepStatus =
type WorkflowStep (line 308) | interface WorkflowStep {
type WorkflowLogLevel (line 326) | type WorkflowLogLevel = 'info' | 'warn' | 'error';
type WorkflowLogMetadata (line 328) | interface WorkflowLogMetadata {
type WorkflowLog (line 342) | interface WorkflowLog {
FILE: src/utils/diffParser.ts
type DiffFile (line 6) | interface DiffFile {
type DiffHunk (line 15) | interface DiffHunk {
type DiffLine (line 24) | interface DiffLine {
function parseDiff (line 34) | function parseDiff(diffText: string): DiffFile[] {
function parseFileDiff (line 48) | function parseFileDiff(diffText: string): DiffFile | null {
function getFileExtension (line 148) | function getFileExtension(path: string): string {
function getLanguage (line 156) | function getLanguage(path: string): string {
FILE: tests/mocks/cloudflare-sandbox.ts
class Sandbox (line 2) | class Sandbox {
method fromClass (line 3) | static fromClass() {
FILE: worker-configuration.d.ts
type GlobalProps (line 5) | interface GlobalProps {
type Env (line 9) | interface Env {
type Env (line 26) | interface Env extends Cloudflare.Env {}
class DOMException (line 51) | class DOMException extends Error {
type WorkerGlobalScopeEventMap (line 100) | type WorkerGlobalScopeEventMap = {
type Console (line 115) | interface Console {
type BufferSource (line 228) | type BufferSource = ArrayBufferView | ArrayBuffer;
type TypedArray (line 229) | type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Arra...
class CompileError (line 231) | class CompileError extends Error {
class RuntimeError (line 234) | class RuntimeError extends Error {
type ValueType (line 237) | type ValueType = "anyfunc" | "externref" | "f32" | "f64" | "i32" | "i64"...
type GlobalDescriptor (line 238) | interface GlobalDescriptor {
class Global (line 242) | class Global {
type ImportValue (line 247) | type ImportValue = ExportValue | number;
type ModuleImports (line 248) | type ModuleImports = Record<string, ImportValue>;
type Imports (line 249) | type Imports = Record<string, ModuleImports>;
type ExportValue (line 250) | type ExportValue = Function | Global | Memory | Table;
type Exports (line 251) | type Exports = Record<string, ExportValue>;
class Instance (line 252) | class Instance {
type MemoryDescriptor (line 256) | interface MemoryDescriptor {
class Memory (line 261) | class Memory {
type ImportExportKind (line 266) | type ImportExportKind = "function" | "global" | "memory" | "table";
type ModuleExportDescriptor (line 267) | interface ModuleExportDescriptor {
type ModuleImportDescriptor (line 271) | interface ModuleImportDescriptor {
type TableKind (line 281) | type TableKind = "anyfunc" | "externref";
type TableDescriptor (line 282) | interface TableDescriptor {
class Table (line 287) | class Table {
type ServiceWorkerGlobalScope (line 303) | interface ServiceWorkerGlobalScope extends WorkerGlobalScope {
type TestController (line 443) | interface TestController {
type ExecutionContext (line 445) | interface ExecutionContext<Props = unknown> {
type ExportedHandlerFetchHandler (line 451) | type ExportedHandlerFetchHandler<Env = unknown, CfHostMetadata = unknown...
type ExportedHandlerTailHandler (line 452) | type ExportedHandlerTailHandler<Env = unknown> = (events: TraceItem[], e...
type ExportedHandlerTraceHandler (line 453) | type ExportedHandlerTraceHandler<Env = unknown> = (traces: TraceItem[], ...
type ExportedHandlerTailStreamHandler (line 454) | type ExportedHandlerTailStreamHandler<Env = unknown> = (event: TailStrea...
type ExportedHandlerScheduledHandler (line 455) | type ExportedHandlerScheduledHandler<Env = unknown> = (controller: Sched...
type ExportedHandlerQueueHandler (line 456) | type ExportedHandlerQueueHandler<Env = unknown, Message = unknown> = (ba...
type ExportedHandlerTestHandler (line 457) | type ExportedHandlerTestHandler<Env = unknown> = (controller: TestContro...
type ExportedHandler (line 458) | interface ExportedHandler<Env = unknown, QueueHandlerMessage = unknown, ...
type StructuredSerializeOptions (line 468) | interface StructuredSerializeOptions {
type AlarmInvocationInfo (line 478) | interface AlarmInvocationInfo {
type Cloudflare (line 482) | interface Cloudflare {
type DurableObject (line 485) | interface DurableObject {
type DurableObjectStub (line 492) | type DurableObjectStub<T extends Rpc.DurableObjectBranded | undefined = ...
type DurableObjectId (line 496) | interface DurableObjectId {
type DurableObjectJurisdiction (line 509) | type DurableObjectJurisdiction = "eu" | "fedramp" | "fedramp-high";
type DurableObjectNamespaceNewUniqueIdOptions (line 510) | interface DurableObjectNamespaceNewUniqueIdOptions {
type DurableObjectLocationHint (line 513) | type DurableObjectLocationHint = "wnam" | "enam" | "sam" | "weur" | "eeu...
type DurableObjectNamespaceGetDurableObjectOptions (line 514) | interface DurableObjectNamespaceGetDurableObjectOptions {
type DurableObjectClass (line 517) | interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undef...
type DurableObjectState (line 519) | interface DurableObjectState<Props = unknown> {
type DurableObjectTransaction (line 537) | interface DurableObjectTransaction {
type DurableObjectStorage (line 550) | interface DurableObjectStorage {
type DurableObjectListOptions (line 571) | interface DurableObjectListOptions {
type DurableObjectGetOptions (line 581) | interface DurableObjectGetOptions {
type DurableObjectGetAlarmOptions (line 585) | interface DurableObjectGetAlarmOptions {
type DurableObjectPutOptions (line 588) | interface DurableObjectPutOptions {
type DurableObjectSetAlarmOptions (line 593) | interface DurableObjectSetAlarmOptions {
class WebSocketRequestResponsePair (line 597) | class WebSocketRequestResponsePair {
type AnalyticsEngineDataset (line 602) | interface AnalyticsEngineDataset {
type AnalyticsEngineDataPoint (line 605) | interface AnalyticsEngineDataPoint {
class Event (line 615) | class Event {
type EventInit (line 734) | interface EventInit {
type EventListener (line 739) | type EventListener<EventType extends Event = Event> = (event: EventType)...
type EventListenerObject (line 740) | interface EventListenerObject<EventType extends Event = Event> {
type EventListenerOrEventListenerObject (line 743) | type EventListenerOrEventListenerObject<EventType extends Event = Event>...
class EventTarget (line 749) | class EventTarget<EventMap extends Record<string, Event> = Record<string...
type EventTargetEventListenerOptions (line 770) | interface EventTargetEventListenerOptions {
type EventTargetAddEventListenerOptions (line 773) | interface EventTargetAddEventListenerOptions {
type EventTargetHandlerObject (line 779) | interface EventTargetHandlerObject {
class AbortController (line 787) | class AbortController {
type Scheduler (line 849) | interface Scheduler {
type SchedulerWaitOptions (line 852) | interface SchedulerWaitOptions {
class CustomEvent (line 873) | class CustomEvent<T = any> extends Event {
type CustomEventCustomEventInit (line 882) | interface CustomEventCustomEventInit {
class Blob (line 893) | class Blob {
type BlobOptions (line 938) | interface BlobOptions {
class File (line 946) | class File extends Blob {
type FileOptions (line 961) | interface FileOptions {
type CacheQueryOptions (line 992) | interface CacheQueryOptions {
type CryptoKeyPair (line 1139) | interface CryptoKeyPair {
type JsonWebKey (line 1143) | interface JsonWebKey {
type RsaOtherPrimesInfo (line 1163) | interface RsaOtherPrimesInfo {
type SubtleCryptoDeriveKeyAlgorithm (line 1168) | interface SubtleCryptoDeriveKeyAlgorithm {
type SubtleCryptoEncryptAlgorithm (line 1176) | interface SubtleCryptoEncryptAlgorithm {
type SubtleCryptoGenerateKeyAlgorithm (line 1185) | interface SubtleCryptoGenerateKeyAlgorithm {
type SubtleCryptoHashAlgorithm (line 1193) | interface SubtleCryptoHashAlgorithm {
type SubtleCryptoImportKeyAlgorithm (line 1196) | interface SubtleCryptoImportKeyAlgorithm {
type SubtleCryptoSignAlgorithm (line 1203) | interface SubtleCryptoSignAlgorithm {
type CryptoKeyKeyAlgorithm (line 1209) | interface CryptoKeyKeyAlgorithm {
type CryptoKeyAesKeyAlgorithm (line 1212) | interface CryptoKeyAesKeyAlgorithm {
type CryptoKeyHmacKeyAlgorithm (line 1216) | interface CryptoKeyHmacKeyAlgorithm {
type CryptoKeyRsaKeyAlgorithm (line 1221) | interface CryptoKeyRsaKeyAlgorithm {
type CryptoKeyEllipticKeyAlgorithm (line 1227) | interface CryptoKeyEllipticKeyAlgorithm {
type CryptoKeyArbitraryKeyAlgorithm (line 1231) | interface CryptoKeyArbitraryKeyAlgorithm {
class DigestStream (line 1237) | class DigestStream extends WritableStream<ArrayBuffer | ArrayBufferView> {
class TextDecoder (line 1247) | class TextDecoder {
class TextEncoder (line 1264) | class TextEncoder {
type TextDecoderConstructorOptions (line 1280) | interface TextDecoderConstructorOptions {
type TextDecoderDecodeOptions (line 1284) | interface TextDecoderDecodeOptions {
type TextEncoderEncodeIntoResult (line 1287) | interface TextEncoderEncodeIntoResult {
class ErrorEvent (line 1296) | class ErrorEvent extends Event {
type ErrorEventErrorEventInit (line 1329) | interface ErrorEventErrorEventInit {
class MessageEvent (line 1341) | class MessageEvent extends Event {
type MessageEventInit (line 1374) | interface MessageEventInit {
class FormData (line 1401) | class FormData {
type ContentOptions (line 1466) | interface ContentOptions {
class HTMLRewriter (line 1469) | class HTMLRewriter {
type HTMLRewriterElementContentHandlers (line 1475) | interface HTMLRewriterElementContentHandlers {
type HTMLRewriterDocumentContentHandlers (line 1480) | interface HTMLRewriterDocumentContentHandlers {
type Doctype (line 1486) | interface Doctype {
type Element (line 1491) | interface Element {
type EndTag (line 1510) | interface EndTag {
type Comment (line 1516) | interface Comment {
type Text (line 1524) | interface Text {
type DocumentEnd (line 1533) | interface DocumentEnd {
type HeadersInit (line 1556) | type HeadersInit = Headers | Iterable<Iterable<string>> | Record<string,...
class Headers (line 1562) | class Headers {
type BodyInit (line 1616) | type BodyInit = ReadableStream<Uint8Array> | string | ArrayBuffer | Arra...
type Response (line 1652) | interface Response extends Body {
type ResponseInit (line 1704) | interface ResponseInit {
type RequestInfo (line 1712) | type RequestInfo<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetad...
type Request (line 1727) | interface Request<CfHostMetadata = unknown, Cf = CfProperties<CfHostMeta...
type RequestInit (line 1785) | interface RequestInit<Cf = CfProperties> {
type Service (line 1804) | type Service<T extends (new (...args: any[]) => Rpc.WorkerEntrypointBran...
type Fetcher (line 1805) | type Fetcher<T extends Rpc.EntrypointBranded | undefined = undefined, Re...
type KVNamespaceListKey (line 1809) | interface KVNamespaceListKey<Metadata, Key extends string = string> {
type KVNamespaceListResult (line 1814) | type KVNamespaceListResult<Metadata, Key extends string = string> = {
type KVNamespace (line 1824) | interface KVNamespace<Key extends string = string> {
type KVNamespaceListOptions (line 1857) | interface KVNamespaceListOptions {
type KVNamespaceGetOptions (line 1862) | interface KVNamespaceGetOptions<Type> {
type KVNamespacePutOptions (line 1866) | interface KVNamespacePutOptions {
type KVNamespaceGetWithMetadataResult (line 1871) | interface KVNamespaceGetWithMetadataResult<Value, Metadata> {
type QueueContentType (line 1876) | type QueueContentType = "text" | "bytes" | "json" | "v8";
type Queue (line 1877) | interface Queue<Body = unknown> {
type QueueSendOptions (line 1881) | interface QueueSendOptions {
type QueueSendBatchOptions (line 1885) | interface QueueSendBatchOptions {
type MessageSendRequest (line 1888) | interface MessageSendRequest<Body = unknown> {
type QueueRetryOptions (line 1893) | interface QueueRetryOptions {
type Message (line 1896) | interface Message<Body = unknown> {
type QueueEvent (line 1904) | interface QueueEvent<Body = unknown> extends ExtendableEvent {
type MessageBatch (line 1910) | interface MessageBatch<Body = unknown> {
type R2Error (line 1916) | interface R2Error extends Error {
type R2ListOptions (line 1923) | interface R2ListOptions {
type R2MultipartUpload (line 1946) | interface R2MultipartUpload {
type R2UploadedPart (line 1953) | interface R2UploadedPart {
type R2ObjectBody (line 1972) | interface R2ObjectBody extends R2Object {
type R2Range (line 1981) | type R2Range = {
type R2Conditional (line 1990) | interface R2Conditional {
type R2GetOptions (line 1997) | interface R2GetOptions {
type R2PutOptions (line 2002) | interface R2PutOptions {
type R2MultipartOptions (line 2014) | interface R2MultipartOptions {
type R2Checksums (line 2020) | interface R2Checksums {
type R2StringChecksums (line 2028) | interface R2StringChecksums {
type R2HTTPMetadata (line 2035) | interface R2HTTPMetadata {
type R2Objects (line 2043) | type R2Objects = {
type R2UploadPartOptions (line 2052) | interface R2UploadPartOptions {
type ScheduledController (line 2060) | interface ScheduledController {
type QueuingStrategy (line 2065) | interface QueuingStrategy<T = any> {
type UnderlyingSink (line 2069) | interface UnderlyingSink<W = any> {
type UnderlyingByteSource (line 2076) | interface UnderlyingByteSource {
type UnderlyingSource (line 2083) | interface UnderlyingSource<R = any> {
type Transformer (line 2090) | interface Transformer<I = any, O = any> {
type StreamPipeOptions (line 2099) | interface StreamPipeOptions {
type ReadableStreamReadResult (line 2122) | type ReadableStreamReadResult<R = any> = {
type ReadableStream (line 2134) | interface ReadableStream<R = any> {
class ReadableStreamDefaultReader (line 2198) | class ReadableStreamDefaultReader<R = any> {
class ReadableStreamBYOBReader (line 2220) | class ReadableStreamBYOBReader {
type ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions (line 2238) | interface ReadableStreamBYOBReaderReadableStreamBYOBReaderReadOptions {
type ReadableStreamGetReaderOptions (line 2241) | interface ReadableStreamGetReaderOptions {
type ReadableWritablePair (line 2393) | interface ReadableWritablePair<R = any, W = any> {
class WritableStream (line 2407) | class WritableStream<W = any> {
class WritableStreamDefaultWriter (line 2439) | class WritableStreamDefaultWriter<W = any> {
class TransformStream (line 2489) | class TransformStream<I = any, O = any> {
class FixedLengthStream (line 2504) | class FixedLengthStream extends IdentityTransformStream {
class IdentityTransformStream (line 2507) | class IdentityTransformStream extends TransformStream<ArrayBuffer | Arra...
type IdentityTransformStreamQueuingStrategy (line 2510) | interface IdentityTransformStreamQueuingStrategy {
type ReadableStreamValuesOptions (line 2513) | interface ReadableStreamValuesOptions {
class CompressionStream (line 2521) | class CompressionStream extends TransformStream<ArrayBuffer | ArrayBuffe...
class DecompressionStream (line 2529) | class DecompressionStream extends TransformStream<ArrayBuffer | ArrayBuf...
class TextEncoderStream (line 2537) | class TextEncoderStream extends TransformStream<string, Uint8Array> {
class TextDecoderStream (line 2546) | class TextDecoderStream extends TransformStream<ArrayBuffer | ArrayBuffe...
type TextDecoderStreamTextDecoderStreamInit (line 2552) | interface TextDecoderStreamTextDecoderStreamInit {
class ByteLengthQueuingStrategy (line 2561) | class ByteLengthQueuingStrategy implements QueuingStrategy<ArrayBufferVi...
class CountQueuingStrategy (line 2577) | class CountQueuingStrategy implements QueuingStrategy {
type QueuingStrategyInit (line 2588) | interface QueuingStrategyInit {
type ScriptVersion (line 2596) | interface ScriptVersion {
type TraceItem (line 2605) | interface TraceItem {
type TraceItemAlarmEventInfo (line 2623) | interface TraceItemAlarmEventInfo {
type TraceItemCustomEventInfo (line 2626) | interface TraceItemCustomEventInfo {
type TraceItemScheduledEventInfo (line 2628) | interface TraceItemScheduledEventInfo {
type TraceItemQueueEventInfo (line 2632) | interface TraceItemQueueEventInfo {
type TraceItemEmailEventInfo (line 2636) | interface TraceItemEmailEventInfo {
type TraceItemTailEventInfo (line 2641) | interface TraceItemTailEventInfo {
type TraceItemTailEventInfoTailItem (line 2644) | interface TraceItemTailEventInfoTailItem {
type TraceItemFetchEventInfo (line 2647) | interface TraceItemFetchEventInfo {
type TraceItemFetchEventInfoRequest (line 2651) | interface TraceItemFetchEventInfoRequest {
type TraceItemFetchEventInfoResponse (line 2658) | interface TraceItemFetchEventInfoResponse {
type TraceItemJsRpcEventInfo (line 2661) | interface TraceItemJsRpcEventInfo {
type TraceItemHibernatableWebSocketEventInfo (line 2664) | interface TraceItemHibernatableWebSocketEventInfo {
type TraceItemHibernatableWebSocketEventInfoMessage (line 2667) | interface TraceItemHibernatableWebSocketEventInfoMessage {
type TraceItemHibernatableWebSocketEventInfoClose (line 2670) | interface TraceItemHibernatableWebSocketEventInfoClose {
type TraceItemHibernatableWebSocketEventInfoError (line 2675) | interface TraceItemHibernatableWebSocketEventInfoError {
type TraceLog (line 2678) | interface TraceLog {
type TraceException (line 2683) | interface TraceException {
type TraceDiagnosticChannelEvent (line 2689) | interface TraceDiagnosticChannelEvent {
type TraceMetrics (line 2694) | interface TraceMetrics {
type UnsafeTraceMetrics (line 2698) | interface UnsafeTraceMetrics {
class URL (line 2706) | class URL {
class URLSearchParams (line 2878) | class URLSearchParams {
class URLPattern (line 2945) | class URLPattern {
type URLPatternInit (line 2959) | interface URLPatternInit {
type URLPatternComponentResult (line 2970) | interface URLPatternComponentResult {
type URLPatternResult (line 2974) | interface URLPatternResult {
type URLPatternOptions (line 2985) | interface URLPatternOptions {
class CloseEvent (line 2993) | class CloseEvent extends Event {
type CloseEventInit (line 3014) | interface CloseEventInit {
type WebSocketEventMap (line 3019) | type WebSocketEventMap = {
type WebSocket (line 3047) | interface WebSocket extends EventTarget<WebSocketEventMap> {
type SqlStorage (line 3094) | interface SqlStorage {
type SqlStorageValue (line 3102) | type SqlStorageValue = ArrayBuffer | string | number | null;
type Socket (line 3119) | interface Socket {
type SocketOptions (line 3129) | interface SocketOptions {
type SocketAddress (line 3134) | interface SocketAddress {
type TlsOptions (line 3138) | interface TlsOptions {
type SocketInfo (line 3141) | interface SocketInfo {
class EventSource (line 3150) | class EventSource extends EventTarget {
type EventSourceEventSourceInit (line 3193) | interface EventSourceEventSourceInit {
type Container (line 3197) | interface Container {
type ContainerStartupOptions (line 3206) | interface ContainerStartupOptions {
class MessageChannel (line 3244) | class MessageChannel {
type MessagePortPostMessageOptions (line 3259) | interface MessagePortPostMessageOptions {
type LoopbackForExport (line 3262) | type LoopbackForExport<T extends (new (...args: any[]) => Rpc.Entrypoint...
type LoopbackServiceStub (line 3263) | type LoopbackServiceStub<T extends Rpc.WorkerEntrypointBranded | undefin...
type LoopbackDurableObjectClass (line 3268) | type LoopbackDurableObjectClass<T extends Rpc.DurableObjectBranded | und...
type SyncKvStorage (line 3273) | interface SyncKvStorage {
type SyncKvListOptions (line 3282) | interface SyncKvListOptions {
type WorkerStub (line 3290) | interface WorkerStub {
type WorkerStubEntrypointOptions (line 3293) | interface WorkerStubEntrypointOptions {
type WorkerLoader (line 3296) | interface WorkerLoader {
type WorkerLoaderModule (line 3299) | interface WorkerLoaderModule {
type WorkerLoaderWorkerCode (line 3308) | interface WorkerLoaderWorkerCode {
type AiImageClassificationInput (line 3331) | type AiImageClassificationInput = {
type AiImageClassificationOutput (line 3334) | type AiImageClassificationOutput = {
type AiImageToTextInput (line 3342) | type AiImageToTextInput = {
type AiImageToTextOutput (line 3356) | type AiImageToTextOutput = {
type AiImageTextToTextInput (line 3363) | type AiImageTextToTextInput = {
type AiImageTextToTextOutput (line 3378) | type AiImageTextToTextOutput = {
type AiMultimodalEmbeddingsInput (line 3385) | type AiMultimodalEmbeddingsInput = {
type AiIMultimodalEmbeddingsOutput (line 3389) | type AiIMultimodalEmbeddingsOutput = {
type AiObjectDetectionInput (line 3397) | type AiObjectDetectionInput = {
type AiObjectDetectionOutput (line 3400) | type AiObjectDetectionOutput = {
type AiSentenceSimilarityInput (line 3408) | type AiSentenceSimilarityInput = {
type AiSentenceSimilarityOutput (line 3412) | type AiSentenceSimilarityOutput = number[];
type AiAutomaticSpeechRecognitionInput (line 3417) | type AiAutomaticSpeechRecognitionInput = {
type AiAutomaticSpeechRecognitionOutput (line 3420) | type AiAutomaticSpeechRecognitionOutput = {
type AiSummarizationInput (line 3433) | type AiSummarizationInput = {
type AiSummarizationOutput (line 3437) | type AiSummarizationOutput = {
type AiTextClassificationInput (line 3444) | type AiTextClassificationInput = {
type AiTextClassificationOutput (line 3447) | type AiTextClassificationOutput = {
type AiTextEmbeddingsInput (line 3455) | type AiTextEmbeddingsInput = {
type AiTextEmbeddingsOutput (line 3458) | type AiTextEmbeddingsOutput = {
type RoleScopedChatInput (line 3466) | type RoleScopedChatInput = {
type AiTextGenerationToolLegacyInput (line 3471) | type AiTextGenerationToolLegacyInput = {
type AiTextGenerationToolInput (line 3485) | type AiTextGenerationToolInput = {
type AiTextGenerationFunctionsInput (line 3502) | type AiTextGenerationFunctionsInput = {
type AiTextGenerationResponseFormat (line 3506) | type AiTextGenerationResponseFormat = {
type AiTextGenerationInput (line 3510) | type AiTextGenerationInput = {
type AiTextGenerationToolLegacyOutput (line 3527) | type AiTextGenerationToolLegacyOutput = {
type AiTextGenerationToolOutput (line 3531) | type AiTextGenerationToolOutput = {
type UsageTags (line 3539) | type UsageTags = {
type AiTextGenerationOutput (line 3544) | type AiTextGenerationOutput = {
type AiTextToSpeechInput (line 3553) | type AiTextToSpeechInput = {
type AiTextToSpeechOutput (line 3557) | type AiTextToSpeechOutput = Uint8Array | {
type AiTextToImageInput (line 3564) | type AiTextToImageInput = {
type AiTextToImageOutput (line 3577) | type AiTextToImageOutput = ReadableStream<Uint8Array>;
type AiTranslationInput (line 3582) | type AiTranslationInput = {
type AiTranslationOutput (line 3587) | type AiTranslationOutput = {
type ResponsesInput (line 3604) | type ResponsesInput = {
type ResponsesOutput (line 3626) | type ResponsesOutput = {
type EasyInputMessage (line 3651) | type EasyInputMessage = {
type ResponsesFunctionTool (line 3656) | type ResponsesFunctionTool = {
type ResponseIncompleteDetails (line 3665) | type ResponseIncompleteDetails = {
type ResponsePrompt (line 3668) | type ResponsePrompt = {
type Reasoning (line 3675) | type Reasoning = {
type ResponseContent (line 3680) | type ResponseContent = ResponseInputText | ResponseInputImage | Response...
type ResponseContentReasoningText (line 3681) | type ResponseContentReasoningText = {
type ResponseConversationParam (line 3685) | type ResponseConversationParam = {
type ResponseCreatedEvent (line 3688) | type ResponseCreatedEvent = {
type ResponseCustomToolCallOutput (line 3693) | type ResponseCustomToolCallOutput = {
type ResponseError (line 3699) | type ResponseError = {
type ResponseErrorEvent (line 3703) | type ResponseErrorEvent = {
type ResponseFailedEvent (line 3710) | type ResponseFailedEvent = {
type ResponseFormatText (line 3715) | type ResponseFormatText = {
type ResponseFormatJSONObject (line 3718) | type ResponseFormatJSONObject = {
type ResponseFormatTextConfig (line 3721) | type ResponseFormatTextConfig = ResponseFormatText | ResponseFormatTextJ...
type ResponseFormatTextJSONSchemaConfig (line 3722) | type ResponseFormatTextJSONSchemaConfig = {
type ResponseFunctionCallArgumentsDeltaEvent (line 3731) | type ResponseFunctionCallArgumentsDeltaEvent = {
type ResponseFunctionCallArgumentsDoneEvent (line 3738) | type ResponseFunctionCallArgumentsDoneEvent = {
type ResponseFunctionCallOutputItem (line 3746) | type ResponseFunctionCallOutputItem = ResponseInputTextContent | Respons...
type ResponseFunctionCallOutputItemList (line 3747) | type ResponseFunctionCallOutputItemList = Array<ResponseFunctionCallOutp...
type ResponseFunctionToolCall (line 3748) | type ResponseFunctionToolCall = {
type ResponseFunctionToolCallItem (line 3756) | interface ResponseFunctionToolCallItem extends ResponseFunctionToolCall {
type ResponseFunctionToolCallOutputItem (line 3759) | type ResponseFunctionToolCallOutputItem = {
type ResponseIncludable (line 3766) | type ResponseIncludable = "message.input_image.image_url" | "message.out...
type ResponseIncompleteEvent (line 3767) | type ResponseIncompleteEvent = {
type ResponseInput (line 3772) | type ResponseInput = Array<ResponseInputItem>;
type ResponseInputContent (line 3773) | type ResponseInputContent = ResponseInputText | ResponseInputImage;
type ResponseInputImage (line 3774) | type ResponseInputImage = {
type ResponseInputImageContent (line 3782) | type ResponseInputImageContent = {
type ResponseInputItem (line 3790) | type ResponseInputItem = EasyInputMessage | ResponseInputItemMessage | R...
type ResponseInputItemFunctionCallOutput (line 3791) | type ResponseInputItemFunctionCallOutput = {
type ResponseInputItemMessage (line 3798) | type ResponseInputItemMessage = {
type ResponseInputMessageContentList (line 3804) | type ResponseInputMessageContentList = Array<ResponseInputContent>;
type ResponseInputMessageItem (line 3805) | type ResponseInputMessageItem = {
type ResponseInputText (line 3812) | type ResponseInputText = {
type ResponseInputTextContent (line 3816) | type ResponseInputTextContent = {
type ResponseItem (line 3820) | type ResponseItem = ResponseInputMessageItem | ResponseOutputMessage | R...
type ResponseOutputItem (line 3821) | type ResponseOutputItem = ResponseOutputMessage | ResponseFunctionToolCa...
type ResponseOutputItemAddedEvent (line 3822) | type ResponseOutputItemAddedEvent = {
type ResponseOutputItemDoneEvent (line 3828) | type ResponseOutputItemDoneEvent = {
type ResponseOutputMessage (line 3834) | type ResponseOutputMessage = {
type ResponseOutputRefusal (line 3841) | type ResponseOutputRefusal = {
type ResponseOutputText (line 3845) | type ResponseOutputText = {
type ResponseReasoningItem (line 3850) | type ResponseReasoningItem = {
type ResponseReasoningSummaryItem (line 3858) | type ResponseReasoningSummaryItem = {
type ResponseReasoningContentItem (line 3862) | type ResponseReasoningContentItem = {
type ResponseReasoningTextDeltaEvent (line 3866) | type ResponseReasoningTextDeltaEvent = {
type ResponseReasoningTextDoneEvent (line 3874) | type ResponseReasoningTextDoneEvent = {
type ResponseRefusalDeltaEvent (line 3882) | type ResponseRefusalDeltaEvent = {
type ResponseRefusalDoneEvent (line 3890) | type ResponseRefusalDoneEvent = {
type ResponseStatus (line 3898) | type ResponseStatus = "completed" | "failed" | "in_progress" | "cancelle...
type ResponseStreamEvent (line 3899) | type ResponseStreamEvent = ResponseCompletedEvent | ResponseCreatedEvent...
type ResponseCompletedEvent (line 3900) | type ResponseCompletedEvent = {
type ResponseTextConfig (line 3905) | type ResponseTextConfig = {
type ResponseTextDeltaEvent (line 3909) | type ResponseTextDeltaEvent = {
type ResponseTextDoneEvent (line 3918) | type ResponseTextDoneEvent = {
type Logprob (line 3927) | type Logprob = {
type TopLogprob (line 3932) | type TopLogprob = {
type ResponseUsage (line 3936) | type ResponseUsage = {
type Tool (line 3941) | type Tool = ResponsesFunctionTool;
type ToolChoiceFunction (line 3942) | type ToolChoiceFunction = {
type ToolChoiceOptions (line 3946) | type ToolChoiceOptions = "none";
type ReasoningEffort (line 3947) | type ReasoningEffort = "minimal" | "low" | "medium" | "high" | null;
type StreamOptions (line 3948) | type StreamOptions = {
type Ai_Cf_Baai_Bge_Base_En_V1_5_Input (line 3951) | type Ai_Cf_Baai_Bge_Base_En_V1_5_Input = {
type Ai_Cf_Baai_Bge_Base_En_V1_5_Output (line 3969) | type Ai_Cf_Baai_Bge_Base_En_V1_5_Output = {
type Ai_Cf_Baai_Bge_Base_En_V1_5_AsyncResponse (line 3980) | interface Ai_Cf_Baai_Bge_Base_En_V1_5_AsyncResponse {
type Ai_Cf_Openai_Whisper_Input (line 3990) | type Ai_Cf_Openai_Whisper_Input = string | {
type Ai_Cf_Openai_Whisper_Output (line 3996) | interface Ai_Cf_Openai_Whisper_Output {
type Ai_Cf_Meta_M2M100_1_2B_Input (line 4019) | type Ai_Cf_Meta_M2M100_1_2B_Input = {
type Ai_Cf_Meta_M2M100_1_2B_Output (line 4051) | type Ai_Cf_Meta_M2M100_1_2B_Output = {
type Ai_Cf_Meta_M2M100_1_2B_AsyncResponse (line 4057) | interface Ai_Cf_Meta_M2M100_1_2B_AsyncResponse {
type Ai_Cf_Baai_Bge_Small_En_V1_5_Input (line 4067) | type Ai_Cf_Baai_Bge_Small_En_V1_5_Input = {
type Ai_Cf_Baai_Bge_Small_En_V1_5_Output (line 4085) | type Ai_Cf_Baai_Bge_Small_En_V1_5_Output = {
type Ai_Cf_Baai_Bge_Small_En_V1_5_AsyncResponse (line 4096) | interface Ai_Cf_Baai_Bge_Small_En_V1_5_AsyncResponse {
type Ai_Cf_Baai_Bge_Large_En_V1_5_Input (line 4106) | type Ai_Cf_Baai_Bge_Large_En_V1_5_Input = {
type Ai_Cf_Baai_Bge_Large_En_V1_5_Output (line 4124) | type Ai_Cf_Baai_Bge_Large_En_V1_5_Output = {
type Ai_Cf_Baai_Bge_Large_En_V1_5_AsyncResponse (line 4135) | interface Ai_Cf_Baai_Bge_Large_En_V1_5_AsyncResponse {
type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input (line 4145) | type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Input = string | {
type Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output (line 4184) | interface Ai_Cf_Unum_Uform_Gen2_Qwen_500M_Output {
type Ai_Cf_Openai_Whisper_Tiny_En_Input (line 4191) | type Ai_Cf_Openai_Whisper_Tiny_En_Input = string | {
type Ai_Cf_Openai_Whisper_Tiny_En_Output (line 4197) | interface Ai_Cf_Openai_Whisper_Tiny_En_Output {
type Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input (line 4220) | interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Input {
type Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output (line 4246) | interface Ai_Cf_Openai_Whisper_Large_V3_Turbo_Output {
type Ai_Cf_Baai_Bge_M3_Input (line 4326) | type Ai_Cf_Baai_Bge_M3_Input = Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts...
type Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts (line 4332) | interface Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts {
type Ai_Cf_Baai_Bge_M3_Input_Embedding (line 4351) | interface Ai_Cf_Baai_Bge_M3_Input_Embedding {
type Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts_1 (line 4358) | interface Ai_Cf_Baai_Bge_M3_Input_QueryAnd_Contexts_1 {
type Ai_Cf_Baai_Bge_M3_Input_Embedding_1 (line 4377) | interface Ai_Cf_Baai_Bge_M3_Input_Embedding_1 {
type Ai_Cf_Baai_Bge_M3_Output (line 4384) | type Ai_Cf_Baai_Bge_M3_Output = Ai_Cf_Baai_Bge_M3_Ouput_Query | Ai_Cf_Ba...
type Ai_Cf_Baai_Bge_M3_Ouput_Query (line 4385) | interface Ai_Cf_Baai_Bge_M3_Ouput_Query {
type Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts (line 4397) | interface Ai_Cf_Baai_Bge_M3_Output_EmbeddingFor_Contexts {
type Ai_Cf_Baai_Bge_M3_Ouput_Embedding (line 4405) | interface Ai_Cf_Baai_Bge_M3_Ouput_Embedding {
type Ai_Cf_Baai_Bge_M3_AsyncResponse (line 4416) | interface Ai_Cf_Baai_Bge_M3_AsyncResponse {
type Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input (line 4426) | interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Input {
type Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output (line 4436) | interface Ai_Cf_Black_Forest_Labs_Flux_1_Schnell_Output {
type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input (line 4446) | type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Input = Ai_Cf_Meta_Llama_3...
type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Prompt (line 4447) | interface Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Prompt {
type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Messages (line 4498) | interface Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Messages {
type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output (line 4666) | type Ai_Cf_Meta_Llama_3_2_11B_Vision_Instruct_Output = {
type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input (line 4689) | type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Input = Ai_Cf_Meta_Llama...
type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt (line 4690) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Prompt {
type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode (line 4741) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode {
type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages (line 4745) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Messages {
type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_1 (line 4892) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_1 {
type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Async_Batch (line 4896) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Async_Batch {
type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_2 (line 4941) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_JSON_Mode_2 {
type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output (line 4945) | type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_Output = {
type Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_AsyncResponse (line 4981) | interface Ai_Cf_Meta_Llama_3_3_70B_Instruct_Fp8_Fast_AsyncResponse {
type Ai_Cf_Meta_Llama_Guard_3_8B_Input (line 4991) | interface Ai_Cf_Meta_Llama_Guard_3_8B_Input {
type Ai_Cf_Meta_Llama_Guard_3_8B_Output (line 5023) | interface Ai_Cf_Meta_Llama_Guard_3_8B_Output {
type Ai_Cf_Baai_Bge_Reranker_Base_Input (line 5056) | interface Ai_Cf_Baai_Bge_Reranker_Base_Input {
type Ai_Cf_Baai_Bge_Reranker_Base_Output (line 5074) | interface Ai_Cf_Baai_Bge_Reranker_Base_Output {
type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input (line 5090) | type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Input = Ai_Cf_Qwen_Qwen2_5_Co...
type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Prompt (line 5091) | interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Prompt {
type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode (line 5142) | interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode {
type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Messages (line 5146) | interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Messages {
type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode_1 (line 5293) | interface Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_JSON_Mode_1 {
type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output (line 5297) | type Ai_Cf_Qwen_Qwen2_5_Coder_32B_Instruct_Output = {
type Ai_Cf_Qwen_Qwq_32B_Input (line 5337) | type Ai_Cf_Qwen_Qwq_32B_Input = Ai_Cf_Qwen_Qwq_32B_Prompt | Ai_Cf_Qwen_Q...
type Ai_Cf_Qwen_Qwq_32B_Prompt (line 5338) | interface Ai_Cf_Qwen_Qwq_32B_Prompt {
type Ai_Cf_Qwen_Qwq_32B_Messages (line 5388) | interface Ai_Cf_Qwen_Qwq_32B_Messages {
type Ai_Cf_Qwen_Qwq_32B_Output (line 5563) | type Ai_Cf_Qwen_Qwq_32B_Output = {
type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input (line 5603) | type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Input = Ai_Cf_Mistra...
type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Prompt (line 5604) | interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Prompt {
type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages (line 5654) | interface Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Messages {
type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output (line 5829) | type Ai_Cf_Mistralai_Mistral_Small_3_1_24B_Instruct_Output = {
type Ai_Cf_Google_Gemma_3_12B_It_Input (line 5869) | type Ai_Cf_Google_Gemma_3_12B_It_Input = Ai_Cf_Google_Gemma_3_12B_It_Pro...
type Ai_Cf_Google_Gemma_3_12B_It_Prompt (line 5870) | interface Ai_Cf_Google_Gemma_3_12B_It_Prompt {
type Ai_Cf_Google_Gemma_3_12B_It_Messages (line 5920) | interface Ai_Cf_Google_Gemma_3_12B_It_Messages {
type Ai_Cf_Google_Gemma_3_12B_It_Output (line 6079) | type Ai_Cf_Google_Gemma_3_12B_It_Output = {
type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input (line 6119) | type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Input = Ai_Cf_Meta_Llama_...
type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt (line 6120) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt {
type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode (line 6171) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_JSON_Mode {
type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages (line 6175) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages {
type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Async_Batch (line 6351) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Async_Batch {
type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt_Inner (line 6354) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Prompt_Inner {
type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner (line 6405) | interface Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Messages_Inner {
type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output (line 6581) | type Ai_Cf_Meta_Llama_4_Scout_17B_16E_Instruct_Output = {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Input (line 6634) | type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Input = Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_P...
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt (line 6635) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode (line 6686) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages (line 6690) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_1 (line 6837) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_1 {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Async_Batch (line 6841) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Async_Batch {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt_1 (line 6844) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Prompt_1 {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_2 (line 6895) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_2 {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages_1 (line 6899) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Messages_1 {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_3 (line 7046) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_JSON_Mode_3 {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Output (line 7050) | type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Output = Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_...
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Chat_Completion_Response (line 7051) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Chat_Completion_Response {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Text_Completion_Response (line 7151) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_Text_Completion_Response {
type Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_AsyncResponse (line 7215) | interface Ai_Cf_Qwen_Qwen3_30B_A3B_Fp8_AsyncResponse {
type Ai_Cf_Deepgram_Nova_3_Input (line 7225) | interface Ai_Cf_Deepgram_Nova_3_Input {
type Ai_Cf_Deepgram_Nova_3_Output (line 7371) | interface Ai_Cf_Deepgram_Nova_3_Output {
type Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input (line 7408) | interface Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Input {
type Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Output (line 7417) | interface Ai_Cf_Qwen_Qwen3_Embedding_0_6B_Output {
type Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input (line 7425) | type Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Input = {
type Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output (line 7447) | interface Ai_Cf_Pipecat_Ai_Smart_Turn_V2_Output {
type Ai_Cf_Leonardo_Phoenix_1_0_Input (line 7469) | interface Ai_Cf_Leonardo_Phoenix_1_0_Input {
type Ai_Cf_Leonardo_Phoenix_1_0_Output (line 7502) | type Ai_Cf_Leonardo_Phoenix_1_0_Output = string;
type Ai_Cf_Leonardo_Lucid_Origin_Input (line 7507) | interface Ai_Cf_Leonardo_Lucid_Origin_Input {
type Ai_Cf_Leonardo_Lucid_Origin_Output (line 7537) | interface Ai_Cf_Leonardo_Lucid_Origin_Output {
type Ai_Cf_Deepgram_Aura_1_Input (line 7547) | interface Ai_Cf_Deepgram_Aura_1_Input {
type Ai_Cf_Deepgram_Aura_1_Output (line 7576) | type Ai_Cf_Deepgram_Aura_1_Output = string;
type Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input (line 7581) | interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Input {
type Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Output (line 7591) | interface Ai_Cf_Ai4Bharat_Indictrans2_En_Indic_1B_Output {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Input (line 7601) | type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Input = Ai_Cf_Aisingapor...
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt (line 7602) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode (line 7653) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages (line 7657) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_1 (line 7804) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_1 {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Async_Batch (line 7808) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Async_Batch {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt_1 (line 7811) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Prompt_1 {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_2 (line 7862) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_2 {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages_1 (line 7866) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Messages_1 {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_3 (line 8013) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_JSON_Mode_3 {
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Output (line 8017) | type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Output = Ai_Cf_Aisingapo...
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Chat_Completion_Response (line 8018) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Chat_Completion_Res...
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Text_Completion_Response (line 8118) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_Text_Completion_Res...
type Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_AsyncResponse (line 8182) | interface Ai_Cf_Aisingapore_Gemma_Sea_Lion_V4_27B_It_AsyncResponse {
type Ai_Cf_Pfnet_Plamo_Embedding_1B_Input (line 8192) | interface Ai_Cf_Pfnet_Plamo_Embedding_1B_Input {
type Ai_Cf_Pfnet_Plamo_Embedding_1B_Output (line 8198) | interface Ai_Cf_Pfnet_Plamo_Embedding_1B_Output {
type Ai_Cf_Deepgram_Flux_Input (line 8218) | interface Ai_Cf_Deepgram_Flux_Input {
type Ai_Cf_Deepgram_Flux_Output (line 8255) | interface Ai_Cf_Deepgram_Flux_Output {
type Ai_Cf_Deepgram_Aura_2_En_Input (line 8306) | interface Ai_Cf_Deepgram_Aura_2_En_Input {
type Ai_Cf_Deepgram_Aura_2_En_Output (line 8335) | type Ai_Cf_Deepgram_Aura_2_En_Output = string;
type Ai_Cf_Deepgram_Aura_2_Es_Input (line 8340) | interface Ai_Cf_Deepgram_Aura_2_Es_Input {
type Ai_Cf_Deepgram_Aura_2_Es_Output (line 8369) | type Ai_Cf_Deepgram_Aura_2_Es_Output = string;
type AiModels (line 8374) | interface AiModels {
type AiOptions (line 8460) | type AiOptions = {
type AiModelsSearchParams (line 8485) | type AiModelsSearchParams = {
type AiModelsSearchObject (line 8494) | type AiModelsSearchObject = {
type InferenceUpstreamError (line 8510) | interface InferenceUpstreamError extends Error {
type AiInternalError (line 8512) | interface AiInternalError extends Error {
type AiModelListType (line 8514) | type AiModelListType = Record<string, any>;
type GatewayRetries (line 8531) | type GatewayRetries = {
type GatewayOptions (line 8536) | type GatewayOptions = {
type UniversalGatewayOptions (line 8547) | type UniversalGatewayOptions = Exclude<GatewayOptions, 'id'> & {
type AiGatewayPatchLog (line 8553) | type AiGatewayPatchLog = {
type AiGatewayLog (line 8558) | type AiGatewayLog = {
type AIGatewayProviders (line 8585) | type AIGatewayProviders = 'workers-ai' | 'anthropic' | 'aws-bedrock' | '...
type AIGatewayHeaders (line 8586) | type AIGatewayHeaders = {
type AIGatewayUniversalRequest (line 8607) | type AIGatewayUniversalRequest = {
type AiGatewayInternalError (line 8613) | interface AiGatewayInternalError extends Error {
type AiGatewayLogNotFound (line 8615) | interface AiGatewayLogNotFound extends Error {
type AutoRAGInternalError (line 8626) | interface AutoRAGInternalError extends Error {
type AutoRAGNotFoundError (line 8628) | interface AutoRAGNotFoundError extends Error {
type AutoRAGUnauthorizedError (line 8630) | interface AutoRAGUnauthorizedError extends Error {
type AutoRAGNameNotSetError (line 8632) | interface AutoRAGNameNotSetError extends Error {
type ComparisonFilter (line 8634) | type ComparisonFilter = {
type CompoundFilter (line 8639) | type CompoundFilter = {
type AutoRagSearchRequest (line 8643) | type AutoRagSearchRequest = {
type AutoRagAiSearchRequest (line 8657) | type AutoRagAiSearchRequest = AutoRagSearchRequest & {
type AutoRagAiSearchRequestStreaming (line 8661) | type AutoRagAiSearchRequestStreaming = Omit<AutoRagAiSearchRequest, 'str...
type AutoRagSearchResponse (line 8664) | type AutoRagSearchResponse = {
type AutoRagListResponse (line 8680) | type AutoRagListResponse = {
type AutoRagAiSearchResponse (line 8689) | type AutoRagAiSearchResponse = AutoRagSearchResponse & {
type BasicImageTransformations (line 8699) | interface BasicImageTransformations {
type BasicImageTransformationsGravityCoordinates (line 8766) | interface BasicImageTransformationsGravityCoordinates {
type RequestInitCfProperties (line 8780) | interface RequestInitCfProperties extends Record<string, unknown> {
type RequestInitCfPropertiesImageDraw (line 8831) | interface RequestInitCfPropertiesImageDraw extends BasicImageTransformat...
type RequestInitCfPropertiesImage (line 8868) | interface RequestInitCfPropertiesImage extends BasicImageTransformations {
type RequestInitCfPropertiesImageMinify (line 9019) | interface RequestInitCfPropertiesImageMinify {
type RequestInitCfPropertiesR2 (line 9024) | interface RequestInitCfPropertiesR2 {
type IncomingRequestCfProperties (line 9033) | type IncomingRequestCfProperties<HostMetadata = unknown> = IncomingReque...
type IncomingRequestCfPropertiesBase (line 9034) | interface IncomingRequestCfPropertiesBase extends Record<string, unknown> {
type IncomingRequestCfPropertiesBotManagementBase (line 9112) | interface IncomingRequestCfPropertiesBotManagementBase {
type IncomingRequestCfPropertiesBotManagement (line 9139) | interface IncomingRequestCfPropertiesBotManagement {
type IncomingRequestCfPropertiesBotManagementEnterprise (line 9151) | interface IncomingRequestCfPropertiesBotManagementEnterprise extends Inc...
type IncomingRequestCfPropertiesCloudflareForSaaSEnterprise (line 9163) | interface IncomingRequestCfPropertiesCloudflareForSaaSEnterprise<HostMet...
type IncomingRequestCfPropertiesCloudflareAccessOrApiShield (line 9172) | interface IncomingRequestCfPropertiesCloudflareAccessOrApiShield {
type IncomingRequestCfPropertiesExportedAuthenticatorMetadata (line 9192) | interface IncomingRequestCfPropertiesExportedAuthenticatorMetadata {
type IncomingRequestCfPropertiesGeographicInformation (line 9221) | interface IncomingRequestCfPropertiesGeographicInformation {
type IncomingRequestCfPropertiesTLSClientAuth (line 9298) | interface IncomingRequestCfPropertiesTLSClientAuth {
type IncomingRequestCfPropertiesTLSClientAuthPlaceholder (line 9391) | interface IncomingRequestCfPropertiesTLSClientAuthPlaceholder {
type CertVerificationStatus (line 9411) | type CertVerificationStatus =
type IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus (line 9429) | type IncomingRequestCfPropertiesEdgeRequestKeepAliveStatus = 0 /** Unkno...
type Iso3166Alpha2Code (line 9431) | type Iso3166Alpha2Code = "AD" | "AE" | "AF" | "AG" | "AI" | "AL" | "AM" ...
type ContinentCode (line 9433) | type ContinentCode = "AF" | "AN" | "AS" | "EU" | "NA" | "OC" | "SA";
type CfProperties (line 9434) | type CfProperties<HostMetadata = unknown> = IncomingRequestCfProperties<...
type D1Meta (line 9435) | interface D1Meta {
type D1Response (line 9463) | interface D1Response {
type D1Result (line 9468) | type D1Result<T = unknown> = D1Response & {
type D1ExecResult (line 9471) | interface D1ExecResult {
type D1SessionConstraint (line 9475) | type D1SessionConstraint =
type D1SessionBookmark (line 9484) | type D1SessionBookmark = string;
type Disposable (line 9533) | interface Disposable {
type EmailMessage (line 9538) | interface EmailMessage {
type ForwardableEmailMessage (line 9551) | interface ForwardableEmailMessage extends EmailMessage {
type SendEmail (line 9587) | interface SendEmail {
type EmailExportedHandler (line 9593) | type EmailExportedHandler<Env = unknown> = (message: ForwardableEmailMes...
type HelloWorldBinding (line 9604) | interface HelloWorldBinding {
type Hyperdrive (line 9617) | interface Hyperdrive {
type ImageInfoResponse (line 9667) | type ImageInfoResponse = {
type ImageTransform (line 9675) | type ImageTransform = {
type ImageDrawOptions (line 9717) | type ImageDrawOptions = {
type ImageInputOptions (line 9725) | type ImageInputOptions = {
type ImageOutputOptions (line 9728) | type ImageOutputOptions = {
type ImagesBinding (line 9734) | interface ImagesBinding {
type ImageTransformer (line 9748) | interface ImageTransformer {
type ImageTransformationOutputOptions (line 9769) | type ImageTransformationOutputOptions = {
type ImageTransformationResult (line 9772) | interface ImageTransformationResult {
type ImagesError (line 9786) | interface ImagesError extends Error {
type MediaBinding (line 9795) | interface MediaBinding {
type MediaTransformer (line 9807) | interface MediaTransformer {
type MediaTransformationGenerator (line 9819) | interface MediaTransformationGenerator {
type MediaTransformationResult (line 9831) | interface MediaTransformationResult {
type MediaTransformationInputOptions (line 9852) | type MediaTransformationInputOptions = {
type MediaTransformationOutputOptions (line 9864) | type MediaTransformationOutputOptions = {
type MediaError (line 9892) | interface MediaError extends Error {
type NodeStyleServer (line 9898) | interface NodeStyleServer {
type Params (line 9910) | type Params<P extends string = any> = Record<P, string | string[]>;
type EventContext (line 9911) | type EventContext<Env, P extends string, Data> = {
type PagesFunction (line 9925) | type PagesFunction<Env = unknown, Params extends string = any, Data exte...
type EventPluginContext (line 9926) | type EventPluginContext<Env, P extends string, Data, PluginArgs> = {
type PagesPluginFunction (line 9941) | type PagesPluginFunction<Env = unknown, Params extends string = any, Dat...
type PipelineRecord (line 9962) | type PipelineRecord = Record<string, unknown>;
type PipelineBatchMetadata (line 9963) | type PipelineBatchMetadata = {
type Pipeline (line 9967) | interface Pipeline<T extends PipelineRecord = PipelineRecord> {
type PubSubMessage (line 9980) | interface PubSubMessage {
type JsonWebKeyWithKid (line 10006) | interface JsonWebKeyWithKid extends JsonWebKey {
type RateLimitOptions (line 10010) | interface RateLimitOptions {
type RateLimitOutcome (line 10013) | interface RateLimitOutcome {
type RateLimit (line 10016) | interface RateLimit {
type RpcTargetBranded (line 10037) | interface RpcTargetBranded {
type WorkerEntrypointBranded (line 10040) | interface WorkerEntrypointBranded {
type DurableObjectBranded (line 10043) | interface DurableObjectBranded {
type WorkflowEntrypointBranded (line 10046) | interface WorkflowEntrypointBranded {
type EntrypointBranded (line 10049) | type EntrypointBranded = WorkerEntrypointBranded | DurableObjectBranded ...
type Stubable (line 10051) | type Stubable = RpcTargetBranded | ((...args: any[]) => any);
type Serializable (line 10056) | type Serializable<T> =
type StubBase (line 10069) | interface StubBase<T extends Stubable> extends Disposable {
type Stub (line 10073) | type Stub<T extends Stubable> = Provider<T> & StubBase<T>;
type BaseType (line 10075) | type BaseType = void | undefined | null | boolean | number | bigint | st...
type Stubify (line 10078) | type Stubify<T> = T extends Stubable ? Stub<T> : T extends Map<infer K, ...
type Unstubify (line 10087) | type Unstubify<T> = T extends StubBase<infer V> ? V : T extends Map<infe...
type UnstubifyAll (line 10092) | type UnstubifyAll<A extends any[]> = {
type MaybeProvider (line 10097) | type MaybeProvider<T> = T extends object ? Provider<T> : unknown;
type MaybeDisposable (line 10098) | type MaybeDisposable<T> = T extends object ? Disposable : unknown;
type Result (line 10107) | type Result<R> = R extends Stubable ? Promise<Stub<R>> & Provider<R> : R...
type MethodOrProperty (line 10113) | type MethodOrProperty<V> = V extends (...args: infer P) => infer R ? (.....
type MaybeCallableProvider (line 10116) | type MaybeCallableProvider<T> = T extends (...args: any[]) => any ? Meth...
type Provider (line 10120) | type Provider<T extends object, Reserved extends string = never> = Maybe...
type Env (line 10131) | interface Env {
type GlobalProps (line 10151) | interface GlobalProps {
type GlobalProp (line 10155) | type GlobalProp<K extends string, Default> = K extends keyof GlobalProps...
type MainModule (line 10158) | type MainModule = GlobalProp<"mainModule", {}>;
type Exports (line 10160) | type Exports = {
type RpcStub (line 10168) | type RpcStub<T extends Rpc.Stubable> = Rpc.Stub<T>;
type WorkflowDurationLabel (line 10201) | type WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'wee...
type WorkflowSleepDuration (line 10202) | type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ...
type WorkflowDelayDuration (line 10203) | type WorkflowDelayDuration = WorkflowSleepDuration;
type WorkflowTimeoutDuration (line 10204) | type WorkflowTimeoutDuration = WorkflowSleepDuration;
type WorkflowRetentionDuration (line 10205) | type WorkflowRetentionDuration = WorkflowSleepDuration;
type WorkflowBackoff (line 10206) | type WorkflowBackoff = 'constant' | 'linear' | 'exponential';
type WorkflowStepConfig (line 10207) | type WorkflowStepConfig = {
type WorkflowEvent (line 10215) | type WorkflowEvent<T> = {
type WorkflowStepEvent (line 10220) | type WorkflowStepEvent<T> = {
type SecretsStoreSecret (line 10252) | interface SecretsStoreSecret {
type MarkdownDocument (line 10263) | type MarkdownDocument = {
type ConversionResponse (line 10267) | type ConversionResponse = {
type ImageConversionOptions (line 10279) | type ImageConversionOptions = {
type EmbeddedImageConversionOptions (line 10282) | type EmbeddedImageConversionOptions = ImageConversionOptions & {
type ConversionOptions (line 10286) | type ConversionOptions = {
type ConversionRequestOptions (line 10301) | type ConversionRequestOptions = {
type SupportedFileFormat (line 10306) | type SupportedFileFormat = {
type Header (line 10316) | interface Header {
type FetchEventInfo (line 10320) | interface FetchEventInfo {
type JsRpcEventInfo (line 10327) | interface JsRpcEventInfo {
type ScheduledEventInfo (line 10330) | interface ScheduledEventInfo {
type AlarmEventInfo (line 10335) | interface AlarmEventInfo {
type QueueEventInfo (line 10339) | interface QueueEventInfo {
type EmailEventInfo (line 10344) | interface EmailEventInfo {
type TraceEventInfo (line 10350) | interface TraceEventInfo {
type HibernatableWebSocketEventInfoMessage (line 10354) | interface HibernatableWebSocketEventInfoMessage {
type HibernatableWebSocketEventInfoError (line 10357) | interface HibernatableWebSocketEventInfoError {
type HibernatableWebSocketEventInfoClose (line 10360) | interface HibernatableWebSocketEventInfoClose {
type HibernatableWebSocketEventInfo (line 10365) | interface HibernatableWebSocketEventInfo {
type CustomEventInfo (line 10369) | interface CustomEventInfo {
type FetchResponseInfo (line 10372) | interface FetchResponseInfo {
type EventOutcome (line 10376) | type EventOutcome = "ok" | "canceled" | "exception" | "unknown" | "killS...
type ScriptVersion (line 10377) | interface ScriptVersion {
type Onset (line 10382) | interface Onset {
type Outcome (line 10395) | interface Outcome {
type SpanOpen (line 10401) | interface SpanOpen {
type SpanClose (line 10408) | interface SpanClose {
type DiagnosticChannelEvent (line 10412) | interface DiagnosticChannelEvent {
type Exception (line 10417) | interface Exception {
type Log (line 10423) | interface Log {
type Return (line 10432) | interface Return {
type Attribute (line 10436) | interface Attribute {
type Attributes (line 10440) | interface Attributes {
type EventType (line 10444) | type EventType = Onset | Outcome | SpanOpen | SpanClose | DiagnosticChan...
type SpanContext (line 10446) | interface SpanContext {
type TailEvent (line 10461) | interface TailEvent<Event extends EventType> {
type TailEventHandler (line 10471) | type TailEventHandler<Event extends EventType = EventType> = (event: Tai...
type TailEventHandlerObject (line 10472) | type TailEventHandlerObject = {
type TailEventHandlerType (line 10482) | type TailEventHandlerType = TailEventHandler | TailEventHandlerObject;
type VectorizeVectorMetadataValue (line 10490) | type VectorizeVectorMetadataValue = string | number | boolean | string[];
type VectorizeVectorMetadata (line 10494) | type VectorizeVectorMetadata = VectorizeVectorMetadataValue | Record<str...
type VectorFloatArray (line 10495) | type VectorFloatArray = Float32Array | Float64Array;
type VectorizeError (line 10496) | interface VectorizeError {
type VectorizeVectorMetadataFilterOp (line 10505) | type VectorizeVectorMetadataFilterOp = '$eq' | '$ne' | '$lt' | '$lte' | ...
type VectorizeVectorMetadataFilterCollectionOp (line 10506) | type VectorizeVectorMetadataFilterCollectionOp = '$in' | '$nin';
type VectorizeVectorMetadataFilter (line 10510) | type VectorizeVectorMetadataFilter = {
type VectorizeDistanceMetric (line 10521) | type VectorizeDistanceMetric = "euclidean" | "cosine" | "dot-product";
type VectorizeMetadataRetrievalLevel (line 10531) | type VectorizeMetadataRetrievalLevel = "all" | "indexed" | "none";
type VectorizeQueryOptions (line 10532) | interface VectorizeQueryOptions {
type VectorizeIndexConfig (line 10542) | type VectorizeIndexConfig = {
type VectorizeIndexDetails (line 10554) | interface VectorizeIndexDetails {
type VectorizeIndexInfo (line 10569) | interface VectorizeIndexInfo {
type VectorizeVector (line 10582) | interface VectorizeVector {
type VectorizeMatch (line 10595) | type VectorizeMatch = Pick<Partial<VectorizeVector>, "values"> & Omit<Ve...
type VectorizeMatches (line 10602) | interface VectorizeMatches {
type VectorizeVectorMutation (line 10613) | interface VectorizeVectorMutation {
type VectorizeAsyncMutation (line 10623) | interface VectorizeAsyncMutation {
type WorkerVersionMetadata (line 10725) | type WorkerVersionMetadata = {
type DynamicDispatchLimits (line 10733) | interface DynamicDispatchLimits {
type DynamicDispatchOptions (line 10743) | interface DynamicDispatchOptions {
type DispatchNamespace (line 10755) | interface DispatchNamespace {
class NonRetryableError (line 10772) | class NonRetryableError extends Error {
type WorkflowDurationLabel (line 10797) | type WorkflowDurationLabel = 'second' | 'minute' | 'hour' | 'day' | 'wee...
type WorkflowSleepDuration (line 10798) | type WorkflowSleepDuration = `${number} ${WorkflowDurationLabel}${'s' | ...
type WorkflowRetentionDuration (line 10799) | type WorkflowRetentionDuration = WorkflowSleepDuration;
type WorkflowInstanceCreateOptions (line 10800) | interface WorkflowInstanceCreateOptions<PARAMS = unknown> {
type InstanceStatus (line 10818) | type InstanceStatus = {
type WorkflowError (line 10830) | interface WorkflowError {
FILE: worker/BoardDO.ts
type Board (line 18) | interface Board {
type Column (line 28) | interface Column {
type Task (line 35) | interface Task {
type ScheduledRunRecord (line 51) | interface ScheduledRunRecord {
type WorkflowPlan (line 63) | interface WorkflowPlan {
type Credential (line 78) | interface Credential {
type MCPServer (line 87) | interface MCPServer {
type MCPTool (line 103) | interface MCPTool {
class BoardDO (line 121) | class BoardDO extends DurableObject<Env> {
method constructor (line 132) | constructor(ctx: DurableObjectState, env: Env) {
method alarm (line 184) | async alarm(): Promise<void> {
method scheduleNextAlarm (line 254) | private async scheduleNextAlarm(boardId: string): Promise<void> {
method fetch (line 269) | async fetch(request: Request): Promise<Response> {
method handleWebSocketUpgrade (line 279) | private handleWebSocketUpgrade(url: URL): Response {
method webSocketMessage (line 290) | async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
method webSocketClose (line 303) | async webSocketClose() {
method broadcast (line 307) | private broadcast(boardId: string, type: string, data: Record<string, ...
method initBoard (line 327) | async initBoard(data: { id: string; name: string; ownerId: string }): ...
method getBoardInfo (line 332) | async getBoardInfo(): Promise<{ id: string; name: string; ownerId: str...
method getBoard (line 337) | async getBoard(boardId: string): Promise<Board> {
method updateBoard (line 342) | async updateBoard(boardId: string, data: { name?: string }): Promise<B...
method deleteBoard (line 347) | async deleteBoard(boardId: string): Promise<{ success: boolean }> {
method createColumn (line 356) | async createColumn(boardId: string, data: { name: string }): Promise<C...
method updateColumn (line 361) | async updateColumn(columnId: string, data: { name?: string; position?:...
method deleteColumn (line 366) | async deleteColumn(columnId: string): Promise<{ success: boolean }> {
method createTask (line 375) | async createTask(data: {
method getTask (line 398) | async getTask(taskId: string): Promise<Task> {
method updateTask (line 403) | async updateTask(taskId: string, data: {
method deleteTask (line 420) | async deleteTask(taskId: string): Promise<{ success: boolean }> {
method moveTask (line 425) | async moveTask(taskId: string, data: { columnId: string; position: num...
method getScheduledTasks (line 434) | async getScheduledTasks(boardId: string): Promise<Task[]> {
method getScheduledRuns (line 439) | async getScheduledRuns(taskId: string, limit?: number): Promise<Schedu...
method getRunTasks (line 444) | async getRunTasks(runId: string): Promise<Task[]> {
method getChildTasks (line 449) | async getChildTasks(parentTaskId: string): Promise<Task[]> {
method triggerScheduledRun (line 454) | async triggerScheduledRun(taskId: string): Promise<{ runId: string }> {
method updateScheduledRun (line 516) | async updateScheduledRun(runId: string, data: {
method deleteScheduledRun (line 528) | async deleteScheduledRun(runId: string): Promise<void> {
method getCredentials (line 536) | async getCredentials(boardId: string): Promise<Credential[]> {
method createCredential (line 541) | async createCredential(boardId: string, data: {
method deleteCredential (line 551) | async deleteCredential(boardId: string, credentialId: string): Promise...
method getCredentialValue (line 556) | async getCredentialValue(boardId: string, type: string): Promise<strin...
method getCredentialFull (line 560) | async getCredentialFull(boardId: string, type: string): Promise<{ valu...
method updateCredentialValue (line 566) | async updateCredentialValue(
method getCredentialById (line 576) | async getCredentialById(boardId: string, credentialId: string): Promis...
method findCredentialId (line 582) | findCredentialId(boardId: string, type: string): string | undefined {
method getTaskWorkflowPlan (line 590) | async getTaskWorkflowPlan(taskId: string): Promise<WorkflowPlan | null> {
method getBoardWorkflowPlans (line 596) | async getBoardWorkflowPlans(boardId: string): Promise<WorkflowPlan[]> {
method createWorkflowPlan (line 601) | async createWorkflowPlan(taskId: string, data: {
method getWorkflowPlan (line 612) | async getWorkflowPlan(planId: string): Promise<WorkflowPlan> {
method updateWorkflowPlan (line 617) | async updateWorkflowPlan(planId: string, data: {
method deleteWorkflowPlan (line 630) | async deleteWorkflowPlan(planId: string): Promise<{ success: boolean }> {
method approveWorkflowPlan (line 635) | async approveWorkflowPlan(planId: string): Promise<WorkflowPlan> {
method resolveWorkflowCheckpoint (line 640) | async resolveWorkflowCheckpoint(planId: string, data: {
method getWorkflowLogs (line 652) | async getWorkflowLogs(planId: string, limit?: number, offset?: number)...
method addWorkflowLog (line 660) | addWorkflowLog(
method broadcastStreamChunk (line 670) | broadcastStreamChunk(boardId: string, planId: string, turnIndex: numbe...
method getMCPServers (line 678) | async getMCPServers(boardId: string): Promise<MCPServer[]> {
method getMCPServer (line 683) | async getMCPServer(serverId: string): Promise<MCPServer> {
method createMCPServer (line 688) | async createMCPServer(boardId: string, data: {
method createAccountMCP (line 702) | async createAccountMCP(boardId: string, data: {
method updateMCPServer (line 710) | async updateMCPServer(serverId: string, data: {
method deleteMCPServer (line 723) | async deleteMCPServer(serverId: string): Promise<{ success: boolean }> {
method getMCPServerTools (line 728) | async getMCPServerTools(serverId: string): Promise<MCPTool[]> {
method cacheMCPServerTools (line 733) | async cacheMCPServerTools(serverId: string, data: {
method connectMCPServer (line 745) | async connectMCPServer(serverId: string): Promise<{
method discoverMCPOAuth (line 758) | async discoverMCPOAuth(serverId: string): Promise<object> {
method getMCPOAuthUrl (line 763) | async getMCPOAuthUrl(serverId: string, redirectUri: string): Promise<{...
method exchangeMCPOAuthCode (line 770) | async exchangeMCPOAuthCode(serverId: string, data: {
method getGitHubRepos (line 783) | async getGitHubRepos(boardId: string): Promise<Array<{
method getLinkMetadata (line 796) | async getLinkMetadata(boardId: string, data: { url: string }): Promise<{
method extractData (line 810) | private async extractData<T>(response: Response): Promise<T> {
FILE: worker/UserDO.ts
type UserInfo (line 13) | interface UserInfo {
type UserBoard (line 20) | interface UserBoard {
type AccessResult (line 29) | interface AccessResult {
class UserDO (line 34) | class UserDO extends DurableObject<Env> {
method constructor (line 37) | constructor(ctx: DurableObjectState, env: Env) {
method initSchema (line 43) | private initSchema(): void {
method initUser (line 68) | async initUser(id: string, email: string): Promise<{ success: boolean ...
method getUserInfo (line 101) | async getUserInfo(): Promise<UserInfo | null> {
method getBoards (line 119) | async getBoards(): Promise<UserBoard[]> {
method addBoard (line 137) | async addBoard(boardId: string, name: string, role?: string): Promise<...
method hasAccess (line 163) | async hasAccess(boardId: string): Promise<AccessResult> {
method updateBoardName (line 179) | async updateBoardName(boardId: string, name: string): Promise<{ succes...
method removeBoard (line 191) | async removeBoard(boardId: string): Promise<{ success: boolean }> {
FILE: worker/auth.ts
type AuthUser (line 12) | interface AuthUser {
type AccessJWTPayload (line 17) | interface AccessJWTPayload extends JWTPayload {
type AuthEnv (line 23) | interface AuthEnv {
function getJWKS (line 34) | function getJWKS(teamName: string) {
function verifyAccessJWT (line 46) | async function verifyAccessJWT(
function getAuthenticatedUser (line 85) | async function getAuthenticatedUser(
function getLogoutUrl (line 110) | function getLogoutUrl(teamName: string): string {
FILE: worker/constants.ts
constant CREDENTIAL_TYPES (line 6) | const CREDENTIAL_TYPES = {
type CredentialType (line 12) | type CredentialType = typeof CREDENTIAL_TYPES[keyof typeof CREDENTIAL_TY...
constant MCP_SERVER_NAMES (line 15) | const MCP_SERVER_NAMES = {
constant ACCOUNT_IDS (line 22) | const ACCOUNT_IDS = {
FILE: worker/db/schema.ts
function initSchema (line 5) | function initSchema(sql: SqlStorage): void {
function runMigrations (line 150) | function runMigrations(sql: SqlStorage): void {
FILE: worker/github/GitHubMCP.ts
constant GITHUB_API_BASE (line 12) | const GITHUB_API_BASE = 'https://api.github.com';
constant DEFAULT_BRANCH (line 13) | const DEFAULT_BRANCH = 'main';
constant LEGACY_TOOL_NAMES (line 16) | const LEGACY_TOOL_NAMES: Record<string, string> = {
constant LEGACY_ARG_NAMES (line 34) | const LEGACY_ARG_NAMES: Record<string, string> = {
class GitHubMCPServer (line 43) | class GitHubMCPServer extends HostedMCPServer {
method constructor (line 49) | constructor(accessToken: string) {
method getTools (line 54) | getTools(): MCPToolSchema[] {
method callTool (line 58) | async callTool(name: string, args: Record<string, unknown>): Promise<M...
method readFile (line 108) | private async readFile(args: Record<string, unknown>): Promise<MCPTool...
method createBranch (line 137) | private async createBranch(args: Record<string, unknown>): Promise<MCP...
method createPullRequest (line 173) | private async createPullRequest(args: Record<string, unknown>): Promis...
method listIssues (line 207) | private async listIssues(args: Record<string, unknown>): Promise<MCPTo...
method listPullRequests (line 254) | private async listPullRequests(args: Record<string, unknown>): Promise...
method getIssue (line 294) | private async getIssue(args: Record<string, unknown>): Promise<MCPTool...
method getPullRequest (line 321) | private async getPullRequest(args: Record<string, unknown>): Promise<M...
method listPullRequestFiles (line 369) | private async listPullRequestFiles(args: Record<string, unknown>): Pro...
method submitPullRequestReview (line 405) | private async submitPullRequestReview(args: Record<string, unknown>): ...
method listIssueComments (line 476) | private async listIssueComments(args: Record<string, unknown>): Promis...
method listPullRequestComments (line 501) | private async listPullRequestComments(args: Record<string, unknown>): ...
method getRepository (line 551) | private async getRepository(args: Record<string, unknown>): Promise<MC...
method listRepositories (line 588) | private async listRepositories(args: Record<string, unknown>): Promise...
method githubFetch (line 623) | private async githubFetch(url: string, options: RequestInit = {}): Pro...
method decodeBase64 (line 657) | private decodeBase64(data: string): string {
FILE: worker/github/githubTools.ts
type GitHubToolName (line 315) | type GitHubToolName = keyof typeof githubTools;
FILE: worker/github/oauth.ts
type GitHubEnv (line 4) | interface GitHubEnv {
type GitHubTokenResponse (line 9) | interface GitHubTokenResponse {
type GitHubUser (line 15) | interface GitHubUser {
type GitHubRepo (line 22) | interface GitHubRepo {
constant GITHUB_SCOPES (line 35) | const GITHUB_SCOPES = ['repo', 'read:user'];
function getOAuthUrl (line 40) | function getOAuthUrl(
function exchangeCodeForToken (line 58) | async function exchangeCodeForToken(
function getUser (line 94) | async function getUser(accessToken: string): Promise<GitHubUser> {
function listRepos (line 113) | async function listRepos(
function generateState (line 146) | function generateState(): string {
FILE: worker/google/DocsMCP.ts
constant DOCS_API_BASE (line 18) | const DOCS_API_BASE = 'https://docs.googleapis.com/v1';
constant DRIVE_API_BASE (line 19) | const DRIVE_API_BASE = 'https://www.googleapis.com/drive/v3';
constant DEFAULT_MAX_RESULTS (line 20) | const DEFAULT_MAX_RESULTS = 10;
type GoogleDoc (line 22) | interface GoogleDoc {
type DriveFile (line 38) | interface DriveFile {
class DocsMCPServer (line 46) | class DocsMCPServer extends HostedMCPServer {
method constructor (line 52) | constructor(accessToken: string) {
method getTools (line 57) | getTools(): MCPToolSchema[] {
method callTool (line 61) | async callTool(name: string, args: Record<string, unknown>): Promise<M...
method getDocument (line 84) | private async getDocument(args: Record<string, unknown>): Promise<MCPT...
method listDocuments (line 117) | private async listDocuments(args: Record<string, unknown>): Promise<MC...
method createDocument (line 164) | private async createDocument(args: Record<string, unknown>): Promise<M...
method appendToDocument (line 204) | private async appendToDocument(args: Record<string, unknown>): Promise...
method searchDocuments (line 240) | private async searchDocuments(args: Record<string, unknown>): Promise<...
method replaceDocumentContent (line 245) | private async replaceDocumentContent(args: Record<string, unknown>): P...
method insertFormattedText (line 315) | private async insertFormattedText(documentId: string, markdown: string...
method extractTextFromDoc (line 340) | private extractTextFromDoc(doc: GoogleDoc): string {
FILE: worker/google/GmailMCP.ts
constant GMAIL_API_BASE (line 17) | const GMAIL_API_BASE = 'https://gmail.googleapis.com/gmail/v1';
constant DEFAULT_MAX_RESULTS (line 18) | const DEFAULT_MAX_RESULTS = 10;
type GmailMessage (line 20) | interface GmailMessage {
type GmailThread (line 36) | interface GmailThread {
class GmailMCPServer (line 42) | class GmailMCPServer extends HostedMCPServer {
method constructor (line 48) | constructor(accessToken: string) {
method getTools (line 53) | getTools(): MCPToolSchema[] {
method callTool (line 57) | async callTool(name: string, args: Record<string, unknown>): Promise<M...
method listMessages (line 80) | private async listMessages(args: Record<string, unknown>): Promise<MCP...
method getMessage (line 144) | private async getMessage(args: Record<string, unknown>): Promise<MCPTo...
method sendEmail (line 193) | private async sendEmail(args: Record<string, unknown>): Promise<MCPToo...
method searchMessages (line 248) | private async searchMessages(args: Record<string, unknown>): Promise<M...
method getThread (line 256) | private async getThread(args: Record<string, unknown>): Promise<MCPToo...
method getAuthenticatedUser (line 302) | private async getAuthenticatedUser(): Promise<MCPToolCallResult> {
method decodeBase64Url (line 325) | private decodeBase64Url(data: string): string {
method encodeBase64Url (line 335) | private encodeBase64Url(str: string): string {
FILE: worker/google/SheetsMCP.ts
constant SHEETS_API_BASE (line 19) | const SHEETS_API_BASE = 'https://sheets.googleapis.com/v4/spreadsheets';
constant DRIVE_API_BASE (line 20) | const DRIVE_API_BASE = 'https://www.googleapis.com/drive/v3';
constant DEFAULT_MAX_RESULTS (line 21) | const DEFAULT_MAX_RESULTS = 10;
type SpreadsheetMetadata (line 23) | interface SpreadsheetMetadata {
type SheetValues (line 44) | interface SheetValues {
type DriveFile (line 50) | interface DriveFile {
class SheetsMCPServer (line 58) | class SheetsMCPServer extends HostedMCPServer {
method constructor (line 64) | constructor(accessToken: string) {
method getTools (line 69) | getTools(): MCPToolSchema[] {
method callTool (line 73) | async callTool(name: string, args: Record<string, unknown>): Promise<M...
method getSpreadsheet (line 100) | private async getSpreadsheet(args: Record<string, unknown>): Promise<M...
method getSheetData (line 136) | private async getSheetData(args: Record<string, unknown>): Promise<MCP...
method listSpreadsheets (line 165) | private async listSpreadsheets(args: Record<string, unknown>): Promise...
method searchSpreadsheets (line 212) | private async searchSpreadsheets(args: Record<string, unknown>): Promi...
method createSpreadsheet (line 217) | private async createSpreadsheet(args: Record<string, unknown>): Promis...
method appendRows (line 260) | private async appendRows(args: Record<string, unknown>): Promise<MCPTo...
method updateCells (line 323) | private async updateCells(args: Record<string, unknown>): Promise<MCPT...
method replaceSheetContent (line 381) | private async replaceSheetContent(args: Record<string, unknown>): Prom...
method writeValues (line 455) | private async writeValues(spreadsheetId: string, sheetName: string, va...
FILE: worker/google/docsTools.ts
type DocsToolName (line 108) | type DocsToolName = keyof typeof docsTools;
FILE: worker/google/gmailTools.ts
type GmailToolName (line 132) | type GmailToolName = keyof typeof gmailTools;
FILE: worker/google/markdownToDocs.ts
type InsertTextRequest (line 8) | interface InsertTextRequest {
type UpdateTextStyleRequest (line 15) | interface UpdateTextStyleRequest {
type UpdateParagraphStyleRequest (line 28) | interface UpdateParagraphStyleRequest {
type CreateParagraphBulletsRequest (line 38) | interface CreateParagraphBulletsRequest {
type DocsRequest (line 45) | type DocsRequest =
type TextRange (line 51) | interface TextRange {
type FormatInfo (line 56) | interface FormatInfo {
type ParagraphInfo (line 63) | interface ParagraphInfo {
function markdownToDocsRequests (line 75) | function markdownToDocsRequests(markdown: string, startIndex: number = 1...
function parseMarkdown (line 193) | function parseMarkdown(markdown: string): {
function processInlineFormatting (line 263) | function processInlineFormatting(
function markdownToPlainText (line 350) | function markdownToPlainText(markdown: string): string {
FILE: worker/google/oauth.ts
type GoogleEnv (line 4) | interface GoogleEnv {
type GoogleTokenResponse (line 9) | interface GoogleTokenResponse {
type GoogleUserInfo (line 17) | interface GoogleUserInfo {
constant GOOGLE_SCOPES (line 25) | const GOOGLE_SCOPES = [
function getOAuthUrl (line 41) | function getOAuthUrl(
function exchangeCodeForToken (line 62) | async function exchangeCodeForToken(
function refreshAccessToken (line 99) | async function refreshAccessToken(
function getUserInfo (line 128) | async function getUserInfo(accessToken: string): Promise<GoogleUserInfo> {
function generateState (line 145) | function generateState(): string {
FILE: worker/google/sheetsTools.ts
type SheetsToolName (line 169) | type SheetsToolName = keyof typeof sheetsTools;
FILE: worker/handlers/boards.ts
type BoardDOStub (line 11) | type BoardDOStub = DurableObjectStub<BoardDO>;
type UserDOStub (line 12) | type UserDOStub = DurableObjectStub<UserDO>;
function routeBoardRequest (line 17) | async function routeBoardRequest(
FILE: worker/handlers/oauth.ts
type BoardDOStub (line 23) | type BoardDOStub = DurableObjectStub<BoardDO>;
type OAuthProvider (line 29) | interface OAuthProvider {
type OAuthTokenData (line 44) | interface OAuthTokenData {
type OAuthUserData (line 51) | interface OAuthUserData {
function handleOAuthUrl (line 106) | async function handleOAuthUrl(
function handleOAuthExchange (line 146) | async function handleOAuthExchange(
function storeCredentialAndCreateMCPs (line 204) | async function storeCredentialAndCreateMCPs(
function ensureMCPServersExist (line 245) | async function ensureMCPServersExist(
function handleGitHubOAuthUrl (line 296) | function handleGitHubOAuthUrl(request: Request, env: Env, url: URL): Pro...
function handleGitHubOAuthExchange (line 300) | function handleGitHubOAuthExchange(_request: Request, env: Env, url: URL...
function handleGitHubOAuthCallback (line 304) | async function handleGitHubOAuthCallback(
function handleGoogleOAuthUrl (line 355) | function handleGoogleOAuthUrl(request: Request, env: Env, url: URL): Pro...
function handleGoogleOAuthExchange (line 359) | function handleGoogleOAuthExchange(_request: Request, env: Env, url: URL...
function redirectWithError (line 363) | function redirectWithError(origin: string, error: string): Response {
FILE: worker/handlers/workflows.ts
type BoardDOStub (line 10) | type BoardDOStub = DurableObjectStub<BoardDO>;
function handleGeneratePlan (line 15) | async function handleGeneratePlan(
function handleResolveCheckpoint (line 90) | async function handleResolveCheckpoint(
function handleCancelWorkflow (line 172) | async function handleCancelWorkflow(
FILE: worker/index.ts
type BoardDOStub (line 27) | type BoardDOStub = DurableObjectStub<BoardDO>;
type UserDOStub (line 28) | type UserDOStub = DurableObjectStub<UserDO>;
method fetch (line 31) | async fetch(request: Request, env: Env, _ctx: ExecutionContext): Promise...
FILE: worker/mcp/AccountMCPRegistry.ts
type CredentialAuthType (line 27) | type CredentialAuthType = 'oauth' | 'api_key' | 'env_binding' | 'none';
type ArtifactType (line 30) | type ArtifactType = 'google_doc' | 'google_sheet' | 'gmail_message' | 'g...
type UrlPatternType (line 33) | type UrlPatternType = 'google_doc' | 'google_sheet' | 'github_pr' | 'git...
type MCPUrlPattern (line 36) | interface MCPUrlPattern {
type MCPEnvBindings (line 46) | interface MCPEnvBindings {
type MCPCredentials (line 54) | interface MCPCredentials {
type MCPDefinition (line 65) | interface MCPDefinition {
type AccountDefinition (line 97) | interface AccountDefinition {
constant GMAIL_GUIDANCE (line 154) | const GMAIL_GUIDANCE = `## Gmail Workflow
constant GOOGLE_DOCS_GUIDANCE (line 171) | const GOOGLE_DOCS_GUIDANCE = `## Google Docs Workflow
constant GOOGLE_SHEETS_GUIDANCE (line 210) | const GOOGLE_SHEETS_GUIDANCE = `## Google Sheets Workflow
constant SANDBOX_GUIDANCE (line 252) | const SANDBOX_GUIDANCE = `## Code Change Workflow (Sandbox)
constant GITHUB_GUIDANCE (line 305) | const GITHUB_GUIDANCE = `## GitHub Workflow
constant ACCOUNT_REGISTRY (line 354) | const ACCOUNT_REGISTRY: AccountDefinition[] = [
function getAccountById (line 472) | function getAccountById(id: string): AccountDefinition | undefined {
function getAccountByCredentialType (line 479) | function getAccountByCredentialType(credentialType: string): AccountDefi...
function getMCPsForAccount (line 486) | function getMCPsForAccount(accountId: string): MCPDefinition[] {
function getMCPDefinition (line 493) | function getMCPDefinition(accountId: string, mcpId: string): MCPDefiniti...
function getAccountForMCPName (line 501) | function getAccountForMCPName(mcpName: string): AccountDefinition | unde...
function getMCPByName (line 510) | function getMCPByName(mcpName: string): MCPDefinition | undefined {
function getMCPByServerName (line 522) | function getMCPByServerName(serverName: string): {
function getCredentialRequirements (line 539) | function getCredentialRequirements(serverNames: string[]): Array<{
function getAllAccountIds (line 569) | function getAllAccountIds(): string[] {
function isAccountCredentialType (line 576) | function isAccountCredentialType(credentialType: string): boolean {
function getMCPTools (line 584) | function getMCPTools(accountId: string, mcpId: string): MCPToolSchema[] {
function getAlwaysEnabledAccounts (line 595) | function getAlwaysEnabledAccounts(): AccountDefinition[] {
function getOAuthAccounts (line 602) | function getOAuthAccounts(): AccountDefinition[] {
function getWorkflowGuidance (line 610) | function getWorkflowGuidance(serverNames: string[]): string {
function getRequiredEnvBindingKeys (line 629) | function getRequiredEnvBindingKeys(serverNames: string[]): string[] {
function getAdditionalCredentialKeys (line 648) | function getAdditionalCredentialKeys(serverNames: string[]): string[] {
function getCredentialTypeForUrlPattern (line 667) | function getCredentialTypeForUrlPattern(patternType: UrlPatternType): st...
FILE: worker/mcp/MCPBridge.ts
type MCPBridgeConfig (line 13) | interface MCPBridgeConfig {
type ToolCallRequest (line 28) | interface ToolCallRequest {
type ToolCallResponse (line 34) | interface ToolCallResponse {
class MCPBridge (line 43) | class MCPBridge {
method constructor (line 48) | constructor(config: MCPBridgeConfig) {
method initialize (line 56) | async initialize(): Promise<void> {
method initializeHostedServer (line 77) | private async initializeHostedServer(server: {
method initializeRemoteServer (line 109) | private async initializeRemoteServer(server: {
method callTool (line 150) | async callTool(request: ToolCallRequest): Promise<ToolCallResponse> {
method getAllTools (line 189) | async getAllTools(): Promise<Map<string, { name: string; tools: string...
method getServerTools (line 213) | async getServerTools(serverIdOrName: string): Promise<string[]> {
function createWorkflowContext (line 238) | function createWorkflowContext(
function createMCPProxies (line 276) | async function createMCPProxies(
FILE: worker/mcp/MCPClient.ts
type MCPToolSchema (line 11) | interface MCPToolSchema {
type JSONSchema (line 25) | interface JSONSchema {
type JSONSchemaProperty (line 39) | type JSONSchemaProperty = JSONSchema;
type MCPToolCallResult (line 41) | interface MCPToolCallResult {
type MCPContent (line 47) | interface MCPContent {
type MCPServerConfig (line 55) | interface MCPServerConfig {
type JSONRPCRequest (line 70) | interface JSONRPCRequest {
type JSONRPCResponse (line 77) | interface JSONRPCResponse {
type SSEEvent (line 89) | interface SSEEvent {
class SSETransport (line 105) | class SSETransport {
method constructor (line 111) | constructor(
method sendRequest (line 125) | async sendRequest(request: JSONRPCRequest): Promise<JSONRPCResponse> {
method connectWithTimeout (line 187) | private async connectWithTimeout(signal: AbortSignal): Promise<Respons...
method waitForEndpointEvent (line 221) | private async waitForEndpointEvent(
method waitForResponse (line 267) | private async waitForResponse(
method parseSSEBuffer (line 317) | private parseSSEBuffer(buffer: string): {
class StreamableHTTPTransport (line 368) | class StreamableHTTPTransport {
method constructor (line 375) | constructor(
method sendRequest (line 389) | async sendRequest(request: JSONRPCRequest): Promise<JSONRPCResponse> {
method parseSSEResponse (line 448) | private async parseSSEResponse(
method parseSSEBuffer (line 495) | private parseSSEBuffer(buffer: string): {
class MCPClient (line 540) | class MCPClient {
method constructor (line 546) | constructor(config: MCPServerConfig) {
method buildAuthHeaders (line 565) | private buildAuthHeaders(): Record<string, string> {
method initialize (line 583) | async initialize(): Promise<{ protocolVersion: string; capabilities: u...
method listTools (line 604) | async listTools(): Promise<MCPToolSchema[]> {
method callTool (line 613) | async callTool(name: string, args: Record<string, unknown>): Promise<M...
method sendRequest (line 624) | private async sendRequest(method: string, params: unknown): Promise<un...
method sendViaTransport (line 654) | private async sendViaTransport(
method sendNotification (line 670) | private async sendNotification(method: string, params: unknown): Promi...
method textContent (line 721) | protected textContent(text: string): MCPToolCallResult {
method errorContent (line 730) | protected errorContent(message: string): MCPToolCallResult {
method jsonContent (line 740) | protected jsonContent(data: unknown): MCPToolCallResult {
class MCPRegistry (line 750) | class MCPRegistry {
method registerRemote (line 758) | registerRemote(config: MCPServerConfig): MCPClient {
method registerHosted (line 767) | registerHosted(id: string, server: HostedMCPServer): void {
method getAllTools (line 774) | async getAllTools(): Promise<Map<string, MCPToolSchema[]>> {
method callTool (line 805) | async callTool(
method getClient (line 826) | getClient(serverId: string): MCPClient | undefined {
method getHosted (line 833) | getHosted(serverId: string): HostedMCPServer | undefined {
method remove (line 840) | remove(serverId: string): void {
FILE: worker/mcp/SchemaConverter.ts
type TypeScriptDeclaration (line 10) | interface TypeScriptDeclaration {
function mcpToolsToTypeScript (line 19) | function mcpToolsToTypeScript(
function generateServerDeclaration (line 43) | function generateServerDeclaration(
function generateToolMethodDeclaration (line 69) | function generateToolMethodDeclaration(tool: MCPToolSchema): string {
function jsonSchemaToTypeScript (line 113) | function jsonSchemaToTypeScript(schema: JSONSchema, indent = 0): string {
function generateObjectType (line 167) | function generateObjectType(schema: JSONSchema, indent: number): string {
function generateWorkflowContextDeclaration (line 197) | function generateWorkflowContextDeclaration(): string {
function sanitizeIdentifier (line 338) | function sanitizeIdentifier(name: string): string {
function sanitizePropertyName (line 365) | function sanitizePropertyName(name: string): string {
function generateToolsSummary (line 378) | function generateToolsSummary(
FILE: worker/mcp/oauth/discovery.ts
constant DISCOVERY_TIMEOUT (line 16) | const DISCOVERY_TIMEOUT = 10000;
function discoverOAuthEndpoints (line 26) | async function discoverOAuthEndpoints(mcpServerUrl: string): Promise<OAu...
function fetchProtectedResourceMetadata (line 108) | async function fetchProtectedResourceMetadata(baseUrl: string): Promise<...
function fetchAuthorizationServerMetadata (line 137) | async function fetchAuthorizationServerMetadata(authServerUrl: string): ...
function fetchWithTimeout (line 185) | async function fetchWithTimeout(url: string, options: RequestInit = {}):...
FILE: worker/mcp/oauth/flow.ts
constant TOKEN_EXCHANGE_TIMEOUT (line 16) | const TOKEN_EXCHANGE_TIMEOUT = 30000;
constant REGISTRATION_TIMEOUT (line 17) | const REGISTRATION_TIMEOUT = 10000;
function registerClient (line 22) | async function registerClient(
function buildAuthorizationUrl (line 98) | function buildAuthorizationUrl(
function exchangeCodeForTokens (line 141) | async function exchangeCodeForTokens(
function refreshAccessToken (line 222) | async function refreshAccessToken(
FILE: worker/mcp/oauth/pkce.ts
function generateCodeVerifier (line 11) | function generateCodeVerifier(): string {
function generateCodeChallenge (line 21) | async function generateCodeChallenge(verifier: string): Promise<string> {
function generateState (line 32) | function generateState(boardId: string, serverId: string): string {
function parseState (line 47) | function parseState(state: string): { boardId: string; serverId: string;...
function generateNonce (line 66) | function generateNonce(): string {
function base64UrlEncode (line 77) | function base64UrlEncode(data: Uint8Array): string {
function base64UrlDecode (line 88) | function base64UrlDecode(str: string): Uint8Array {
FILE: worker/mcp/oauth/types.ts
type OAuthProtectedResourceMetadata (line 12) | interface OAuthProtectedResourceMetadata {
type OAuthAuthorizationServerMetadata (line 30) | interface OAuthAuthorizationServerMetadata {
type MCPOAuthMetadata (line 57) | interface MCPOAuthMetadata {
type MCPOAuthPending (line 78) | interface MCPOAuthPending {
type OAuthTokenResponse (line 93) | interface OAuthTokenResponse {
type OAuthErrorResponse (line 104) | interface OAuthErrorResponse {
type OAuthDiscoveryResult (line 113) | interface OAuthDiscoveryResult {
type OAuthAuthorizationUrlResult (line 122) | interface OAuthAuthorizationUrlResult {
type OAuthTokenExchangeResult (line 132) | interface OAuthTokenExchangeResult {
type OAuthClientRegistrationRequest (line 144) | interface OAuthClientRegistrationRequest {
type OAuthClientRegistrationResponse (line 156) | interface OAuthClientRegistrationResponse {
type OAuthClientRegistrationResult (line 171) | interface OAuthClientRegistrationResult {
FILE: worker/sandbox/SandboxMCP.ts
type SandboxCredentials (line 26) | interface SandboxCredentials {
type ForkInfo (line 31) | interface ForkInfo {
type SessionInfo (line 38) | interface SessionInfo {
constant GIT_ADD_EXCLUSIONS (line 50) | const GIT_ADD_EXCLUSIONS = [
class SandboxMCPServer (line 69) | class SandboxMCPServer extends HostedMCPServer {
method githubHeaders (line 76) | private get githubHeaders(): Record<string, string> {
method constructor (line 86) | constructor(
method getTools (line 95) | getTools(): MCPToolSchema[] {
method callTool (line 99) | async callTool(name: string, args: Record<string, unknown>): Promise<M...
method createSession (line 145) | private async createSession(args: {
method parseGitHubUrl (line 223) | private parseGitHubUrl(url: string): { owner: string; repo: string } |...
method createOrGetFork (line 235) | private async createOrGetFork(owner: string, repo: string): Promise<{ ...
method waitForForkReady (line 270) | private async waitForForkReady(forkOwner: string, forkRepo: string, ma...
method runClaude (line 290) | private async runClaude(args: {
method getDiff (line 435) | private async getDiff(args: {
method commit (line 482) | private async commit(args: {
method push (line 542) | private async push(args: {
method pushWithForkFallback (line 576) | private async pushWithForkFallback(
method createPullRequest (line 680) | private async createPullRequest(args: {
method readFile (line 816) | private async readFile(args: {
method writeFile (line 841) | private async writeFile(args: {
method exec (line 868) | private async exec(args: {
method destroySession (line 916) | private async destroySession(args: { sessionId: string }): Promise<MCP...
FILE: worker/sandbox/sandboxTools.ts
type SandboxToolName (line 216) | type SandboxToolName = keyof typeof sandboxTools;
FILE: worker/services/BoardService.ts
type TaskRow (line 11) | interface TaskRow {
class BoardService (line 27) | class BoardService {
method constructor (line 32) | constructor(
method initBoard (line 49) | initBoard(data: { id: string; name: string; ownerId: string }): Respon...
method getBoardInfo (line 78) | getBoardInfo(): Response {
method getBoard (line 98) | getBoard(id: string): Response {
method updateBoard (line 127) | updateBoard(id: string, data: { name?: string }): Response {
method deleteBoard (line 139) | deleteBoard(id: string): Response {
method createColumn (line 153) | createColumn(boardId: string, data: { name: string }): Response {
method updateColumn (line 174) | updateColumn(id: string, data: { name?: string; position?: number }): ...
method deleteColumn (line 215) | deleteColumn(id: string): Response {
method createTask (line 232) | createTask(data: {
method getTask (line 277) | getTask(id: string): Response {
method updateTask (line 288) | updateTask(id: string, data: {
method deleteTask (line 337) | deleteTask(id: string): Response {
method moveTask (line 349) | moveTask(id: string, data: { columnId: string; position: number }): Re...
method validateTaskExists (line 406) | validateTaskExists(taskId: string): Response {
method getLinkMetadata (line 421) | async getLinkMetadata(boardId: string, data: { url: string }): Promise...
method fetchLinkMetadataFromMCP (line 483) | private async fetchLinkMetadataFromMCP(
method getGitHubRepos (line 566) | async getGitHubRepos(boardId: string): Promise<Response> {
FILE: worker/services/CredentialService.ts
constant TOKEN_REFRESH_BUFFER_MS (line 8) | const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
type OAuthEnv (line 12) | interface OAuthEnv {
class CredentialService (line 17) | class CredentialService {
method constructor (line 23) | constructor(
method getCredentials (line 38) | getCredentials(boardId: string): Response {
method createCredential (line 63) | async createCredential(boardId: string, data: {
method deleteCredential (line 102) | deleteCredential(boardId: string, credentialId: string): Response {
method getCredentialValue (line 126) | async getCredentialValue(boardId: string, type: string): Promise<strin...
method getValidAccessToken (line 141) | async getValidAccessToken(
method getCredentialValueResponse (line 207) | async getCredentialValueResponse(boardId: string, type: string): Promi...
method getCredentialById (line 221) | async getCredentialById(boardId: string, credentialId: string): Promis...
method getCredentialFullResponse (line 244) | async getCredentialFullResponse(boardId: string, type: string): Promis...
method updateCredentialValue (line 270) | async updateCredentialValue(
method getCredentialRowById (line 313) | getCredentialRowById(credentialId: string): { encrypted_value: string;...
method findCredentialId (line 323) | findCredentialId(boardId: string, type: string): string | undefined {
method getOAuthClientCredentials (line 336) | private getOAuthClientCredentials(accountId: string): {
method encrypt (line 354) | async encrypt(value: string): Promise<string> {
method decrypt (line 361) | async decrypt(encrypted: string): Promise<string> {
FILE: worker/services/MCPOAuthService.ts
class MCPOAuthService (line 18) | class MCPOAuthService {
method constructor (line 24) | constructor(
method discoverMCPOAuth (line 39) | async discoverMCPOAuth(serverId: string): Promise<Response> {
method getMCPOAuthUrl (line 80) | async getMCPOAuthUrl(serverId: string, params: URLSearchParams): Promi...
method exchangeMCPOAuthCode (line 177) | async exchangeMCPOAuthCode(serverId: string, data: {
FILE: worker/services/MCPService.ts
class MCPService (line 13) | class MCPService {
method constructor (line 18) | constructor(
method getMCPServers (line 35) | getMCPServers(boardId: string): Response {
method getMCPServer (line 50) | getMCPServer(serverId: string): Response {
method createMCPServer (line 69) | createMCPServer(boardId: string, data: {
method createAccountMCP (line 105) | async createAccountMCP(boardId: string, data: {
method updateMCPServer (line 187) | updateMCPServer(serverId: string, data: {
method deleteMCPServer (line 231) | deleteMCPServer(serverId: string): Response {
method getMCPServerTools (line 250) | async getMCPServerTools(serverId: string): Promise<Response> {
method cacheMCPServerTools (line 292) | async cacheMCPServerTools(serverId: string, data: {
method connectMCPServer (line 329) | async connectMCPServer(serverId: string): Promise<Response> {
method getServerRow (line 440) | getServerRow(serverId: string): Record<string, unknown> | undefined {
method updateOAuthMetadata (line 447) | updateOAuthMetadata(serverId: string, metadata: object): void {
method updateServerCredential (line 460) | updateServerCredential(serverId: string, credentialId: string): void {
method transformServer (line 470) | private transformServer(row: Record<string, unknown>): Record<string, ...
method transformTool (line 482) | private transformTool(row: Record<string, unknown>): Record<string, un...
FILE: worker/services/ScheduleService.ts
type ScheduleConfig (line 4) | interface ScheduleConfig {
type ScheduledRunStatus (line 14) | type ScheduledRunStatus = 'pending' | 'running' | 'completed' | 'failed';
type ScheduledRun (line 16) | interface ScheduledRun {
type TaskRow (line 28) | interface TaskRow {
class ScheduleService (line 44) | class ScheduleService {
method constructor (line 48) | constructor(sql: SqlStorage, generateId: () => string) {
method getScheduledTasks (line 53) | getScheduledTasks(boardId: string): Response {
method getScheduledRuns (line 78) | getScheduledRuns(taskId: string, limit = 10): Response {
method getRunTasks (line 94) | getRunTasks(runId: string): Response {
method getChildTasks (line 106) | getChildTasks(parentTaskId: string): Response {
method createScheduledRun (line 118) | createScheduledRun(taskId: string): Response {
method updateScheduledRun (line 135) | updateScheduledRun(runId: string, data: {
method deleteScheduledRun (line 172) | deleteScheduledRun(runId: string): void {
method getNextScheduledRunTime (line 176) | getNextScheduledRunTime(boardId: string): number | null {
method getTasksDueForRun (line 208) | getTasksDueForRun(boardId: string, alarmTimestamp: number): Array<{
method isTaskDueAt (line 253) | private isTaskDueAt(config: ScheduleConfig, alarmDate: Date, lastRunAt...
method calculateNextRunTime (line 300) | calculateNextRunTime(config: ScheduleConfig): number | null {
method getLocalTimeParts (line 349) | private getLocalTimeParts(timestamp: number, timezone: string): {
method localTimeToUTC (line 384) | private localTimeToUTC(
method getTimezoneOffsetMs (line 393) | private getTimezoneOffsetMs(timestamp: number, timezone: string): numb...
method parseCronNextRun (line 429) | private parseCronNextRun(cron: string, timezone: string): number | null {
method parseCronField (line 464) | private parseCronField(field: string, min: number, max: number): Set<n...
FILE: worker/services/WorkflowService.ts
type BroadcastFn (line 4) | type BroadcastFn = (boardId: string, type: string, data: Record<string, ...
class WorkflowService (line 6) | class WorkflowService {
method constructor (line 11) | constructor(
method getTaskWorkflowPlan (line 28) | getTaskWorkflowPlan(taskId: string): Response {
method getBoardWorkflowPlans (line 52) | getBoardWorkflowPlans(boardId: string): Response {
method getWorkflowPlan (line 73) | getWorkflowPlan(planId: string): Response {
method createWorkflowPlan (line 92) | createWorkflowPlan(taskId: string, data: {
method updateWorkflowPlan (line 121) | updateWorkflowPlan(planId: string, data: {
method deleteWorkflowPlan (line 172) | deleteWorkflowPlan(planId: string): Response {
method approveWorkflowPlan (line 184) | approveWorkflowPlan(planId: string): Response {
method resolveWorkflowCheckpoint (line 197) | resolveWorkflowCheckpoint(planId: string, data: {
method getWorkflowLogs (line 231) | getWorkflowLogs(planId: string, params: URLSearchParams): Response {
method handleAddWorkflowLog (line 256) | handleAddWorkflowLog(planId: string, data: {
method addWorkflowLog (line 269) | addWorkflowLog(
FILE: worker/utils/crypto.ts
function encryptValue (line 12) | async function encryptValue(value: string, encryptionKey: string): Promi...
function decryptValue (line 34) | async function decryptValue(encrypted: string, encryptionKey: string): P...
function deriveKey (line 54) | async function deriveKey(keyString: string): Promise<CryptoKey> {
function arrayBufferToBase64 (line 83) | function arrayBufferToBase64(buffer: Uint8Array): string {
function base64ToArrayBuffer (line 94) | function base64ToArrayBuffer(base64: string): Uint8Array {
FILE: worker/utils/logger.ts
type LogLevel (line 8) | type LogLevel = 'debug' | 'info' | 'warn' | 'error';
type LogContext (line 10) | interface LogContext {
type Logger (line 14) | interface Logger {
function formatLog (line 21) | function formatLog(level: LogLevel, component: string, message: string, ...
function createLogger (line 35) | function createLogger(component: string): Logger {
FILE: worker/utils/oauth-state.ts
type OAuthStatePayload (line 8) | interface OAuthStatePayload {
constant STATE_MAX_AGE_MS (line 14) | const STATE_MAX_AGE_MS = 10 * 60 * 1000;
function encodeOAuthState (line 19) | async function encodeOAuthState(
function decodeOAuthState (line 42) | async function decodeOAuthState(
function createHmacSignature (line 79) | async function createHmacSignature(data: string, secretKey: string): Pro...
function timingSafeEqual (line 105) | function timingSafeEqual(a: string, b: string): boolean {
FILE: worker/utils/response.ts
function jsonResponse (line 8) | function jsonResponse(data: object, status = 200): Response {
FILE: worker/utils/transformations.ts
function toCamelCase (line 11) | function toCamelCase(obj: Record<string, unknown>): Record<string, unkno...
function transformBoard (line 23) | function transformBoard(board: Record<string, unknown>): Record<string, ...
function transformColumn (line 30) | function transformColumn(column: Record<string, unknown>): Record<string...
function transformTask (line 38) | function transformTask(task: Record<string, unknown>): Record<string, un...
function transformWorkflowPlan (line 64) | function transformWorkflowPlan(plan: Record<string, unknown>): Record<st...
function transformWorkflowLog (line 98) | function transformWorkflowLog(log: Record<string, unknown>): Record<stri...
function transformScheduledRun (line 116) | function transformScheduledRun(run: Record<string, unknown>): Record<str...
FILE: worker/utils/zodTools.ts
type ToolDefinition (line 35) | interface ToolDefinition<
type ToolDefinitions (line 58) | type ToolDefinitions = Record<string, ToolDefinition>;
function defineTools (line 64) | function defineTools<T extends ToolDefinitions>(tools: T): T {
function zodSchemaToJsonSchema (line 72) | function zodSchemaToJsonSchema(schema: z.ZodTypeAny): JSONSchema {
function toolsToMCPSchemas (line 85) | function toolsToMCPSchemas(tools: ToolDefinitions): MCPToolSchema[] {
function parseToolArgs (line 119) | function parseToolArgs<T extends z.ZodTypeAny>(
function getToolInputSchema (line 136) | function getToolInputSchema<T extends ToolDefinitions>(
FILE: worker/workflows/AgentWorkflow.ts
constant TOKEN_REFRESH_BUFFER_MS (line 34) | const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
constant DEFAULT_MODEL (line 35) | const DEFAULT_MODEL = 'claude-sonnet-4-5-20250929';
type AgentWorkflowParams (line 38) | interface AgentWorkflowParams {
type CheckpointEvent (line 55) | interface CheckpointEvent {
type WorkflowEnv (line 64) | interface WorkflowEnv {
type AgentStep (line 71) | interface AgentStep {
type WorkflowArtifact (line 92) | interface WorkflowArtifact {
type MCPServerInfo (line 109) | interface MCPServerInfo {
type CredentialStore (line 129) | interface CredentialStore {
function formatToolName (line 143) | function formatToolName(toolName: string): string {
class AgentWorkflow (line 155) | class AgentWorkflow extends WorkflowEntrypoint<WorkflowEnv, AgentWorkflo...
method run (line 156) | async run(event: WorkflowEvent<AgentWorkflowParams>, step: WorkflowSte...
method buildClaudeTools (line 991) | private buildClaudeTools(servers: MCPServerInfo[], isScheduledRun = fa...
method buildSystemPrompt (line 1120) | private buildSystemPrompt(
method ensureValidToken (line 1240) | private async ensureValidToken(
method executeMcpTool (line 1297) | private async executeMcpTool(
method executeHostedMcpTool (line 1332) | private async executeHostedMcpTool(
method executeRemoteMcpTool (line 1372) | private async executeRemoteMcpTool(
method extractArtifact (line 1409) | private extractArtifact(toolName: string, result: unknown): WorkflowAr...
Condensed preview — 171 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,675K chars).
[
{
"path": ".env.example",
"chars": 566,
"preview": "# OAuth Credentials\n# Copy this file to .env and fill in your values\n\n# GitHub OAuth App credentials\n# Create at: https:"
},
{
"path": ".gitignore",
"chars": 415,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
},
{
"path": "Dockerfile",
"chars": 250,
"preview": "FROM docker.io/cloudflare/sandbox:0.6.7\n\n# Install Claude Code CLI\nRUN npm install -g @anthropic-ai/claude-code\n\n# Longe"
},
{
"path": "LICENSE",
"chars": 10762,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 10371,
"preview": "# Weft\n\nTask management, but AI agents do your tasks.\n\n\n\n## What is Weft?\n\nWeft"
},
{
"path": "eslint.config.js",
"chars": 1024,
"preview": "import js from '@eslint/js'\nimport globals from 'globals'\nimport reactHooks from 'eslint-plugin-react-hooks'\nimport reac"
},
{
"path": "index.html",
"chars": 613,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/"
},
{
"path": "package.json",
"chars": 1332,
"preview": "{\n\t\"name\": \"weft\",\n\t\"private\": true,\n\t\"version\": \"0.0.0\",\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"dev\": \"vite\",\n\t\t\"dev:wrang"
},
{
"path": "src/App.css",
"chars": 1078,
"preview": ".app {\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n}\n\n.app-main {\n flex: 1;\n display: flex;\n over"
},
{
"path": "src/App.tsx",
"chars": 4661,
"preview": "import { useState, useEffect, useCallback } from 'react';\nimport { BrowserRouter, Routes, Route, Navigate, useLocation }"
},
{
"path": "src/api/client.ts",
"chars": 15883,
"preview": "import type {\n Board,\n Column,\n Task,\n ApiResponse,\n TaskPriority,\n BoardCredential,\n MCPServer,\n MCPTool,\n Wor"
},
{
"path": "src/components/Approval/Approval.css",
"chars": 10098,
"preview": "/**\n * Approval View Styles\n *\n * Shared styles for all approval view components.\n * These styles are used by both Defau"
},
{
"path": "src/components/Approval/ApprovalFooter.tsx",
"chars": 2154,
"preview": "/**\n * Approval Footer\n *\n * Shared footer for all approval views with three actions:\n * - Cancel (abort workflow)\n * - "
},
{
"path": "src/components/Approval/ApprovalViewRegistry.tsx",
"chars": 2081,
"preview": "/**\n * Approval View Registry\n *\n * Maps MCP tool names to specialized approval view components.\n * This allows differen"
},
{
"path": "src/components/Approval/DefaultApproval.tsx",
"chars": 15266,
"preview": "/**\n * Default Approval View\n *\n * Generic key-value display for tool approvals.\n * Uses CommentableText for multi-line "
},
{
"path": "src/components/Approval/EmailApproval.css",
"chars": 5596,
"preview": "/**\n * Email Approval View Styles\n *\n * Email-client style layout for Gmail send/draft approvals.\n */\n\n.email-approval-v"
},
{
"path": "src/components/Approval/EmailApproval.tsx",
"chars": 11211,
"preview": "/**\n * Email Approval View\n *\n * Dedicated approval view for Gmail send/draft operations.\n * Displays email in familiar "
},
{
"path": "src/components/Approval/GitHubPRApproval.css",
"chars": 5232,
"preview": "/**\n * GitHub PR Approval View Styles\n *\n * Full-width diff viewer layout for PR approval.\n * Similar to ExecutionReview"
},
{
"path": "src/components/Approval/GitHubPRApproval.tsx",
"chars": 10616,
"preview": "/**\n * GitHub PR Approval View\n *\n * Full diff viewer for approving pull request creation.\n * Shows the complete diff wi"
},
{
"path": "src/components/Approval/GitHubPRReviewApproval.css",
"chars": 6534,
"preview": ".pr-review-approval-view {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 520px;\n}\n\n.pr-review"
},
{
"path": "src/components/Approval/GitHubPRReviewApproval.tsx",
"chars": 15004,
"preview": "/**\n * GitHub PR Review Approval View\n *\n * Review-focused approval UI for posting PR reviews:\n * - Shows PR metadata an"
},
{
"path": "src/components/Approval/GoogleDocsApproval.css",
"chars": 11371,
"preview": "/**\n * Google Docs Approval View Styles\n *\n * Full-width side-by-side diff layout for document approval.\n * Similar to G"
},
{
"path": "src/components/Approval/GoogleDocsApproval.tsx",
"chars": 16598,
"preview": "/**\n * Google Docs Approval View\n *\n * For createDocument: Single-panel document preview with commenting support.\n * For"
},
{
"path": "src/components/Approval/GoogleSheetsApproval.css",
"chars": 12642,
"preview": "/**\n * Google Sheets Approval View Styles\n *\n * Row-based diff layout for spreadsheet approval.\n * Shows data in table f"
},
{
"path": "src/components/Approval/GoogleSheetsApproval.tsx",
"chars": 20219,
"preview": "/**\n * Google Sheets Approval View\n *\n * Row-based diff for approving Google Sheets changes.\n * Shows current rows vs ne"
},
{
"path": "src/components/Approval/index.ts",
"chars": 389,
"preview": "/**\n * Approval Views Module\n *\n * Composable approval views for different MCP tools.\n */\n\nexport { getApprovalView } fr"
},
{
"path": "src/components/Board/Board.css",
"chars": 3450,
"preview": ".board {\n flex: 1;\n display: flex;\n overflow-x: auto;\n padding: var(--space-4);\n background: var(--color-bg-seconda"
},
{
"path": "src/components/Board/Board.tsx",
"chars": 3918,
"preview": "import { useEffect, useState, useCallback, type DragEvent } from 'react';\nimport { useParams } from 'react-router-dom';\n"
},
{
"path": "src/components/Column/Column.css",
"chars": 6340,
"preview": ".column {\n width: var(--column-width);\n min-width: var(--column-width);\n background: var(--color-bg-tertiary);\n bord"
},
{
"path": "src/components/Column/Column.tsx",
"chars": 9557,
"preview": "import { useState, useEffect, useRef, type DragEvent } from 'react';\nimport type { Column as ColumnType } from '../../ty"
},
{
"path": "src/components/CommandPalette/CommandPalette.css",
"chars": 4135,
"preview": ".palette-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n display: flex;\n justify-content: "
},
{
"path": "src/components/CommandPalette/CommandPalette.tsx",
"chars": 6304,
"preview": "import { useState, useEffect, useRef, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport"
},
{
"path": "src/components/CommandPalette/index.ts",
"chars": 51,
"preview": "export { CommandPalette } from './CommandPalette';\n"
},
{
"path": "src/components/CommentableText/CommentableText.css",
"chars": 6453,
"preview": "/**\n * CommentableText Styles\n *\n * Styles for line-selectable text with inline comments.\n * Similar visual language to "
},
{
"path": "src/components/CommentableText/CommentableText.tsx",
"chars": 7823,
"preview": "/**\n * CommentableText - Line-selectable text with inline comments\n *\n * Used for adding line-level comments to text con"
},
{
"path": "src/components/DiffViewer/DiffViewer.css",
"chars": 10730,
"preview": ".diff-viewer {\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n font-family: var(--font-mono);\n font"
},
{
"path": "src/components/DiffViewer/DiffViewer.tsx",
"chars": 22850,
"preview": "import { useState, useRef, useCallback, useEffect, useMemo } from 'react';\nimport type { DiffFile, DiffHunk, DiffLine } "
},
{
"path": "src/components/DiffViewer/index.ts",
"chars": 53,
"preview": "export { DiffViewer, FileTree } from './DiffViewer';\n"
},
{
"path": "src/components/GitHubCallback.tsx",
"chars": 1848,
"preview": "import { useEffect, useState } from 'react';\nimport { useSearchParams, useNavigate } from 'react-router-dom';\n\n/**\n * Ha"
},
{
"path": "src/components/GoogleCallback.tsx",
"chars": 1848,
"preview": "import { useEffect, useState } from 'react';\nimport { useSearchParams, useNavigate } from 'react-router-dom';\n\n/**\n * Ha"
},
{
"path": "src/components/Header/Header.css",
"chars": 13639,
"preview": ".header {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: space-between;\n height: var("
},
{
"path": "src/components/Header/Header.tsx",
"chars": 11026,
"preview": "import { useState, useEffect, useRef } from 'react';\nimport { useNavigate, useLocation, useSearchParams } from 'react-ro"
},
{
"path": "src/components/Header/WeftLogo.tsx",
"chars": 2756,
"preview": "/**\n * Weft Logo Component\n *\n * Abstract weaving-inspired logo representing interwoven threads.\n * Clean, minimal desig"
},
{
"path": "src/components/Home/Home.css",
"chars": 4873,
"preview": ".home {\n flex: 1;\n display: flex;\n justify-content: center;\n padding: var(--space-6);\n background: var(--color-bg-s"
},
{
"path": "src/components/Home/Home.tsx",
"chars": 7659,
"preview": "import { useState, useRef, useEffect } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { useBoard }"
},
{
"path": "src/components/MCP/MCP.css",
"chars": 5291,
"preview": "/* MCP Server List */\n.mcp-server-list {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.mcp-loading,\n.mcp-e"
},
{
"path": "src/components/MCP/MCPOAuthCallback.css",
"chars": 2005,
"preview": "/* MCP OAuth Callback Page */\n.mcp-oauth-callback {\n display: flex;\n align-items: center;\n justify-content: center;\n "
},
{
"path": "src/components/MCP/MCPOAuthCallback.tsx",
"chars": 4486,
"preview": "/**\n * MCPOAuthCallback - Handles OAuth callback for MCP server authentication\n *\n * After the user authorizes with the "
},
{
"path": "src/components/MCP/MCPServerConnect.css",
"chars": 2175,
"preview": "/* MCP Server Connect Form */\n.mcp-form {\n display: flex;\n flex-direction: column;\n gap: var(--space-3);\n}\n\n.mcp-form"
},
{
"path": "src/components/MCP/MCPServerConnect.tsx",
"chars": 5406,
"preview": "import { useState } from 'react';\nimport { Modal, Input, Button } from '../common';\nimport type { MCPServer } from '../."
},
{
"path": "src/components/MCP/index.ts",
"chars": 110,
"preview": "export { MCPServerConnect } from './MCPServerConnect';\nexport { MCPOAuthCallback } from './MCPOAuthCallback';\n"
},
{
"path": "src/components/Settings/AccountsSection.css",
"chars": 1771,
"preview": ".accounts-section {\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n}\n\n.account-card {\n display: flex"
},
{
"path": "src/components/Settings/AccountsSection.tsx",
"chars": 4415,
"preview": "import { Button } from '../common';\nimport type { BoardCredential } from '../../types';\nimport './AccountsSection.css';\n"
},
{
"path": "src/components/Settings/BoardSettings.css",
"chars": 11366,
"preview": "/* ============================================\n Board Settings - Sidebar Layout\n =================================="
},
{
"path": "src/components/Settings/BoardSettings.tsx",
"chars": 9596,
"preview": "import { useState, useEffect, useCallback } from 'react';\nimport { useSearchParams, useNavigate } from 'react-router-dom"
},
{
"path": "src/components/Settings/MCPSection.css",
"chars": 4589,
"preview": "/* MCP List */\n.mcp-list {\n display: flex;\n flex-direction: column;\n gap: var(--space-2);\n}\n\n/* MCP Item */\n.mcp-item"
},
{
"path": "src/components/Settings/MCPSection.tsx",
"chars": 14200,
"preview": "import { useState, useEffect, useCallback, useRef } from 'react';\nimport { Button } from '../common';\nimport { MCPServer"
},
{
"path": "src/components/Settings/index.ts",
"chars": 49,
"preview": "export { BoardSettings } from './BoardSettings';\n"
},
{
"path": "src/components/Settings/sections/CredentialsSection.tsx",
"chars": 6129,
"preview": "import { useState } from 'react';\nimport { Button, Input } from '../../common';\nimport { AccountsSection } from '../Acco"
},
{
"path": "src/components/Settings/sections/DangerSection.tsx",
"chars": 2661,
"preview": "import { useState } from 'react';\nimport { Button, Input } from '../../common';\nimport type { Board } from '../../../typ"
},
{
"path": "src/components/Settings/sections/GeneralSection.tsx",
"chars": 3779,
"preview": "import { useState } from 'react';\nimport { Button, Input } from '../../common';\nimport type { BoardWithDetails } from '."
},
{
"path": "src/components/Settings/sections/IntegrationsSection.tsx",
"chars": 16506,
"preview": "import { useState, useEffect, useRef } from 'react';\nimport { MCPServerConnect } from '../../MCP/MCPServerConnect';\nimpo"
},
{
"path": "src/components/Settings/sections/index.ts",
"chars": 220,
"preview": "export { GeneralSection } from './GeneralSection';\nexport { CredentialsSection } from './CredentialsSection';\nexport { I"
},
{
"path": "src/components/Task/AgentSection.css",
"chars": 8849,
"preview": "/* Agent Section */\n.agent-section {\n padding: var(--space-3);\n background: var(--color-bg-subtle);\n border-radius: v"
},
{
"path": "src/components/Task/AgentSection.tsx",
"chars": 16822,
"preview": "import { useState, useEffect, useMemo, useRef } from 'react';\nimport { Button, AgentIcon, McpIcon } from '../common';\nim"
},
{
"path": "src/components/Task/RunHistory.css",
"chars": 5524,
"preview": ".run-history-overlay {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.5)"
},
{
"path": "src/components/Task/RunHistory.tsx",
"chars": 7822,
"preview": "import { useState, useEffect, useCallback } from 'react';\nimport { createPortal } from 'react-dom';\nimport type { Schedu"
},
{
"path": "src/components/Task/TaskCard.css",
"chars": 4229,
"preview": ".task-card {\n display: flex;\n flex-direction: column;\n gap: var(--space-1);\n background: var(--color-bg-elevated);\n "
},
{
"path": "src/components/Task/TaskCard.tsx",
"chars": 8077,
"preview": "import { useState, useEffect, useRef, type DragEvent, type MouseEvent } from 'react';\nimport type { Task, ScheduleConfig"
},
{
"path": "src/components/Task/TaskModal.css",
"chars": 11861,
"preview": ".task-modal {\n display: flex;\n flex-direction: column;\n gap: var(--space-4);\n}\n\n.task-modal-form {\n display: flex;\n "
},
{
"path": "src/components/Task/TaskModal.tsx",
"chars": 19400,
"preview": "import { useState, useEffect, useCallback, useRef, type ClipboardEvent } from 'react';\nimport type { Task, WorkflowPlan,"
},
{
"path": "src/components/Toast/Toast.css",
"chars": 3000,
"preview": "/* Toast Container - top right, below header dropdowns */\n.toast-container {\n position: fixed;\n top: calc(var(--header"
},
{
"path": "src/components/Toast/Toast.tsx",
"chars": 2085,
"preview": "import { useEffect, useRef, useState } from 'react';\nimport type { Toast as ToastType, ToastType as ToastVariant } from "
},
{
"path": "src/components/Toast/ToastContainer.tsx",
"chars": 465,
"preview": "import { useToast } from '../../context/ToastContext';\nimport { Toast } from './Toast';\nimport './Toast.css';\n\nexport fu"
},
{
"path": "src/components/Toast/WorkflowToastListener.tsx",
"chars": 2448,
"preview": "import { useEffect, useRef } from 'react';\nimport { useBoard } from '../../context/BoardContext';\nimport { useToast } fr"
},
{
"path": "src/components/Toast/index.ts",
"chars": 149,
"preview": "export { Toast } from './Toast';\nexport { ToastContainer } from './ToastContainer';\nexport { WorkflowToastListener } fro"
},
{
"path": "src/components/Workflow/EmailViewerModal.css",
"chars": 1319,
"preview": "/**\n * Email Viewer Modal Styles\n */\n\n.email-viewer {\n display: flex;\n flex-direction: column;\n gap: var(--space-4);\n"
},
{
"path": "src/components/Workflow/EmailViewerModal.tsx",
"chars": 2339,
"preview": "/**\n * Email Viewer\n *\n * Displays sent email content.\n * Used for viewing email artifacts.\n */\n\nimport { McpIcon } from"
},
{
"path": "src/components/Workflow/PlanReviewView.css",
"chars": 4032,
"preview": "/* Plan Review Modal */\n.plan-review {\n display: flex;\n flex-direction: column;\n gap: var(--space-4);\n}\n\n.plan-sectio"
},
{
"path": "src/components/Workflow/PlanReviewView.tsx",
"chars": 3244,
"preview": "import { useState } from 'react';\nimport { Button } from '../common';\nimport type { WorkflowPlan } from '../../types';\ni"
},
{
"path": "src/components/Workflow/Workflow.css",
"chars": 24012,
"preview": "/* Checkpoint Dialog */\n.checkpoint-dialog {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-4);\n}\n\n.che"
},
{
"path": "src/components/Workflow/WorkflowProgress.tsx",
"chars": 24364,
"preview": "import { useState, useEffect, useRef } from 'react';\nimport { Button } from '../common';\nimport type { WorkflowPlan, Wor"
},
{
"path": "src/components/Workflow/index.ts",
"chars": 171,
"preview": "export { PlanReviewView } from './PlanReviewView';\nexport { WorkflowProgress, WorkflowBadge } from './WorkflowProgress';"
},
{
"path": "src/components/common/Button.css",
"chars": 2485,
"preview": ".btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-2);\n font-family: v"
},
{
"path": "src/components/common/Button.tsx",
"chars": 674,
"preview": "import { forwardRef, type ButtonHTMLAttributes } from 'react';\nimport './Button.css';\n\nexport interface ButtonProps exte"
},
{
"path": "src/components/common/ErrorBoundary.css",
"chars": 605,
"preview": ".error-boundary {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 200px;\n padding: 2re"
},
{
"path": "src/components/common/ErrorBoundary.tsx",
"chars": 1619,
"preview": "import { Component, type ReactNode, type ErrorInfo } from 'react';\nimport './ErrorBoundary.css';\n\ninterface ErrorBoundar"
},
{
"path": "src/components/common/Input.css",
"chars": 1347,
"preview": ".input-wrapper {\n display: flex;\n flex-direction: column;\n gap: var(--space-1);\n}\n\n.input-label {\n font-size: 12px;\n"
},
{
"path": "src/components/common/Input.tsx",
"chars": 1651,
"preview": "import { forwardRef, type InputHTMLAttributes, type TextareaHTMLAttributes } from 'react';\nimport './Input.css';\n\nexport"
},
{
"path": "src/components/common/McpIcon.tsx",
"chars": 5771,
"preview": "/**\n * Reusable MCP service icon component\n */\n\ninterface McpIconProps {\n type: 'google-docs' | 'google-sheets' | 'gmai"
},
{
"path": "src/components/common/Modal.css",
"chars": 4771,
"preview": ".modal-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n display: flex;\n align-items: center"
},
{
"path": "src/components/common/Modal.tsx",
"chars": 2955,
"preview": "import { useEffect, useRef, useState, useCallback, type ReactNode } from 'react';\nimport { createPortal } from 'react-do"
},
{
"path": "src/components/common/RichTextEditor.css",
"chars": 7906,
"preview": "/* RichTextEditor - contenteditable with inline pills */\n\n.rte-container {\n display: flex;\n flex-direction: column;\n "
},
{
"path": "src/components/common/RichTextEditor.tsx",
"chars": 47302,
"preview": "/**\n * RichTextEditor - contenteditable editor with inline pill rendering\n *\n * Renders pill markdown as visual LinkPill"
},
{
"path": "src/components/common/index.ts",
"chars": 504,
"preview": "export { Button } from './Button';\nexport type { ButtonProps } from './Button';\n\nexport { Input, Textarea } from './Inpu"
},
{
"path": "src/constants.ts",
"chars": 337,
"preview": "/**\n * Shared constants for the frontend\n */\n\n// Credential types - must match worker/constants.ts\nexport const CREDENTI"
},
{
"path": "src/context/AuthContext.tsx",
"chars": 2197,
"preview": "/**\n * AuthContext - Authentication state management\n *\n * Provides current user info and auth state to the app.\n * Fetc"
},
{
"path": "src/context/BoardContext.tsx",
"chars": 17627,
"preview": "import {\n createContext,\n useContext,\n useReducer,\n useCallback,\n useEffect,\n useState,\n useRef,\n type ReactNode"
},
{
"path": "src/context/ToastContext.tsx",
"chars": 1834,
"preview": "import {\n createContext,\n useContext,\n useState,\n useCallback,\n type ReactNode,\n} from 'react';\n\n// ==============="
},
{
"path": "src/context/boardReducer.ts",
"chars": 9125,
"preview": "/**\n * Board reducer - State management logic for BoardContext\n *\n * Extracted from BoardContext.tsx for better maintain"
},
{
"path": "src/hooks/index.ts",
"chars": 620,
"preview": "export {\n useTitleEdit,\n useRowSelection,\n useCommentInput,\n useComments,\n useFieldComments,\n generateCommentId,\n "
},
{
"path": "src/hooks/useApprovalComments.ts",
"chars": 10414,
"preview": "/**\n * Shared hook for approval comment management\n *\n * Extracts common comment handling logic used across approval com"
},
{
"path": "src/hooks/useIsMobile.ts",
"chars": 841,
"preview": "import { useState, useEffect } from 'react';\n\nconst MOBILE_BREAKPOINT = 768;\n\n/**\n * Hook to detect if the current viewp"
},
{
"path": "src/hooks/useUrlDetection.ts",
"chars": 2188,
"preview": "/**\n * Hook for detecting and enriching URLs with metadata from connected MCPs\n *\n * Used by TaskModal to detect pasted "
},
{
"path": "src/index.css",
"chars": 3706,
"preview": "/* ============================================\n WEFT - CLI-Inspired Kanban Board\n Design System\n ================"
},
{
"path": "src/main.tsx",
"chars": 230,
"preview": "import { StrictMode } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport './index.css'\nimport App from '."
},
{
"path": "src/types/index.ts",
"chars": 8370,
"preview": "import type { CredentialType } from '../constants';\n\n// ============================================\n// CORE ENTITIES\n//"
},
{
"path": "src/utils/diffParser.ts",
"chars": 4219,
"preview": "/**\n * Unified diff parser\n * Parses git diff output into structured data\n */\n\nexport interface DiffFile {\n path: strin"
},
{
"path": "src/vite-env.d.ts",
"chars": 38,
"preview": "/// <reference types=\"vite/client\" />\n"
},
{
"path": "tests/mocks/cloudflare-sandbox.ts",
"chars": 156,
"preview": "// Mock for @cloudflare/sandbox - not supported in vitest-pool-workers\nexport class Sandbox {\n static fromClass() {\n "
},
{
"path": "tests/worker/auth.test.ts",
"chars": 5549,
"preview": "/**\n * Auth Layer Security Tests\n *\n * Proves that:\n * - AUTH_MODE=access requires valid JWT\n * - AUTH_MODE=none returns"
},
{
"path": "tests/worker/board-access.integration.test.ts",
"chars": 1098,
"preview": "/**\n * Board Access Control Integration Tests\n *\n * Proves that:\n * - Users cannot access boards they don't own (403)\n *"
},
{
"path": "tests/worker/user-isolation.test.ts",
"chars": 7175,
"preview": "/**\n * User Isolation Security Tests\n *\n * Proves that:\n * - Users can only see their own boards\n * - Users cannot acces"
},
{
"path": "tsconfig.app.json",
"chars": 702,
"preview": "{\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.app.tsbuildinfo\",\n \"target\": \"ES2022\",\n"
},
{
"path": "tsconfig.json",
"chars": 242,
"preview": "{\n\t\"files\": [],\n\t\"references\": [\n\t\t{\n\t\t\t\"path\": \"./tsconfig.app.json\"\n\t\t},\n\t\t{\n\t\t\t\"path\": \"./tsconfig.node.json\"\n\t\t},\n\t\t"
},
{
"path": "tsconfig.node.json",
"chars": 630,
"preview": "{\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.node.tsbuildinfo\",\n \"target\": \"ES2023\","
},
{
"path": "tsconfig.worker.json",
"chars": 261,
"preview": "{\n \"extends\": \"./tsconfig.node.json\",\n \"compilerOptions\": {\n \"tsBuildInfoFile\": \"./node_modules/.tmp/tsconfig.worke"
},
{
"path": "vite.config.ts",
"chars": 266,
"preview": "import { defineConfig } from 'vite'\nimport react from '@vitejs/plugin-react-swc'\n\nimport { cloudflare } from \"@cloudflar"
},
{
"path": "vitest.config.ts",
"chars": 187,
"preview": "import { defineConfig } from 'vitest/config'\n\nexport default defineConfig({\n test: {\n include: ['tests/**/*.{test,sp"
},
{
"path": "vitest.config.workers.ts",
"chars": 610,
"preview": "import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';\nimport path from 'path';\n\nexport default d"
},
{
"path": "worker/BoardDO.ts",
"chars": 25242,
"preview": "import { DurableObject } from 'cloudflare:workers';\nimport { initSchema } from './db/schema';\nimport {\n BoardService,\n "
},
{
"path": "worker/UserDO.ts",
"chars": 4664,
"preview": "/**\n * UserDO - Durable Object for user data\n *\n * Keyed by user ID (from Cloudflare Access JWT `sub` claim)\n * Stores: "
},
{
"path": "worker/auth.ts",
"chars": 2794,
"preview": "/**\n * Authentication utilities\n *\n * AUTH_MODE controls authentication:\n * - 'access': Cloudflare Access JWT verificati"
},
{
"path": "worker/constants.ts",
"chars": 578,
"preview": "/**\n * Shared constants for the worker\n */\n\n// Credential types\nexport const CREDENTIAL_TYPES = {\n GITHUB_OAUTH: 'githu"
},
{
"path": "worker/db/index.ts",
"chars": 39,
"preview": "export { initSchema } from './schema';\n"
},
{
"path": "worker/db/schema.ts",
"chars": 7278,
"preview": "/**\n * Database schema initialization and migrations for BoardDO\n */\n\nexport function initSchema(sql: SqlStorage): void "
},
{
"path": "worker/github/GitHubMCP.ts",
"chars": 20817,
"preview": "/**\n * GitHubMCP - Hosted MCP wrapper for GitHub operations\n *\n * Enables workflows to interact with GitHub repositories"
},
{
"path": "worker/github/githubTools.ts",
"chars": 12355,
"preview": "/**\n * GitHub MCP Tool Definitions\n *\n * Single source of truth for GitHub tool schemas using Zod.\n * Used for both JSON"
},
{
"path": "worker/github/index.ts",
"chars": 213,
"preview": "/**\n * GitHub services module\n *\n * Provides OAuth and MCP wrapper for GitHub:\n * - Repository operations\n * - Issues an"
},
{
"path": "worker/github/oauth.ts",
"chars": 3371,
"preview": "// GitHub OAuth Configuration\n// Requires secrets: GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET\n\nexport interface GitHubEnv {\n"
},
{
"path": "worker/google/DocsMCP.ts",
"chars": 10722,
"preview": "/**\n * DocsMCP - Hosted MCP wrapper for Google Docs API\n *\n * Provides MCP-compatible tools for Google Docs operations:\n"
},
{
"path": "worker/google/GmailMCP.ts",
"chars": 10520,
"preview": "/**\n * GmailMCP - Hosted MCP wrapper for Gmail API\n *\n * Provides MCP-compatible tools for Gmail operations:\n * - listMe"
},
{
"path": "worker/google/SheetsMCP.ts",
"chars": 14470,
"preview": "/**\n * SheetsMCP - Hosted MCP wrapper for Google Sheets API\n *\n * Provides MCP-compatible tools for Google Sheets operat"
},
{
"path": "worker/google/docsTools.ts",
"chars": 4316,
"preview": "/**\n * Google Docs MCP Tool Definitions\n *\n * Single source of truth for Google Docs tool schemas using Zod.\n * Used for"
},
{
"path": "worker/google/gmailTools.ts",
"chars": 4793,
"preview": "/**\n * Gmail MCP Tool Definitions\n *\n * Single source of truth for Gmail tool schemas using Zod.\n * Used for both JSON S"
},
{
"path": "worker/google/index.ts",
"chars": 235,
"preview": "/**\n * Google services module\n *\n * Provides OAuth and MCP wrappers for Google services:\n * - Gmail\n * - Google Docs\n */"
},
{
"path": "worker/google/markdownToDocs.ts",
"chars": 8756,
"preview": "/**\n * Markdown to Google Docs API Converter\n *\n * Converts markdown text to Google Docs API batchUpdate requests\n * wit"
},
{
"path": "worker/google/oauth.ts",
"chars": 3883,
"preview": "// Google OAuth Configuration\n// Requires secrets: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET\n\nexport interface GoogleEnv {\n"
},
{
"path": "worker/google/sheetsTools.ts",
"chars": 6840,
"preview": "/**\n * Google Sheets MCP Tool Definitions\n *\n * Single source of truth for Google Sheets tool schemas using Zod.\n * Used"
},
{
"path": "worker/handlers/boards.ts",
"chars": 29243,
"preview": "/**\n * Board-scoped route handlers\n */\n\nimport { jsonResponse } from '../utils/response';\nimport { handleGeneratePlan, h"
},
{
"path": "worker/handlers/oauth.ts",
"chars": 10956,
"preview": "/**\n * OAuth handlers for GitHub and Google\n */\n\nimport {\n getOAuthUrl as getGitHubOAuthUrl,\n exchangeCodeForToken as "
},
{
"path": "worker/handlers/workflows.ts",
"chars": 6507,
"preview": "/**\n * Workflow handlers for plan generation, checkpoints, and cancellation\n */\n\nimport { type AgentWorkflowParams } fro"
},
{
"path": "worker/index.ts",
"chars": 7890,
"preview": "/**\n * Cloudflare Worker entry point - thin dispatcher\n */\n\nimport { Sandbox } from '@cloudflare/sandbox';\nimport { Agen"
},
{
"path": "worker/mcp/AccountMCPRegistry.ts",
"chars": 21639,
"preview": "/**\n * AccountMCPRegistry - Pluggable registry for accounts with multiple MCPs\n *\n * Defines accounts (Google, future: M"
},
{
"path": "worker/mcp/MCPBridge.ts",
"chars": 8670,
"preview": "/**\n * MCPBridge - Bridge between workflow execution and MCP servers\n *\n * Routes tool calls from generated workflow cod"
},
{
"path": "worker/mcp/MCPClient.ts",
"chars": 23235,
"preview": "/**\n * MCPClient - Client for connecting to MCP (Model Context Protocol) servers\n *\n * Supports both remote MCP servers "
},
{
"path": "worker/mcp/SchemaConverter.ts",
"chars": 11288,
"preview": "/**\n * SchemaConverter - Convert MCP tool schemas to TypeScript declarations\n *\n * Takes MCP tool schemas (JSON Schema f"
},
{
"path": "worker/mcp/index.ts",
"chars": 684,
"preview": "/**\n * MCP (Model Context Protocol) module\n *\n * Provides infrastructure for connecting to MCP servers and converting\n *"
},
{
"path": "worker/mcp/oauth/discovery.ts",
"chars": 6599,
"preview": "/**\n * OAuth endpoint discovery for MCP servers\n *\n * Implements RFC 9728 (Protected Resource Metadata) and\n * RFC 8414 "
},
{
"path": "worker/mcp/oauth/flow.ts",
"chars": 8306,
"preview": "/**\n * OAuth flow utilities for MCP servers\n *\n * Handles authorization URL building and token exchange.\n */\n\nimport typ"
},
{
"path": "worker/mcp/oauth/index.ts",
"chars": 231,
"preview": "/**\n * MCP OAuth module exports\n */\n\nexport * from './types';\nexport * from './pkce';\nexport * from './discovery';\nexpor"
},
{
"path": "worker/mcp/oauth/pkce.ts",
"chars": 2639,
"preview": "/**\n * PKCE (Proof Key for Code Exchange) utilities for MCP OAuth\n *\n * Implements RFC 7636 for secure OAuth authorizati"
},
{
"path": "worker/mcp/oauth/types.ts",
"chars": 4165,
"preview": "/**\n * OAuth types for MCP server authentication\n *\n * Based on RFC 8414 (OAuth Authorization Server Metadata),\n * RFC 9"
},
{
"path": "worker/sandbox/SandboxMCP.ts",
"chars": 30583,
"preview": "/**\n * SandboxMCP - Hosted MCP wrapper for Cloudflare Sandbox operations\n *\n * Enables workflows to use Claude Code and "
},
{
"path": "worker/sandbox/sandboxTools.ts",
"chars": 8052,
"preview": "/**\n * Sandbox MCP Tool Definitions\n *\n * Single source of truth for Sandbox tool schemas using Zod.\n * Used for both JS"
},
{
"path": "worker/services/BoardService.ts",
"chars": 20085,
"preview": "import { jsonResponse } from '../utils/response';\nimport { logger } from '../utils/logger';\nimport { CREDENTIAL_TYPES } "
},
{
"path": "worker/services/CredentialService.ts",
"chars": 10801,
"preview": "import { encryptValue, decryptValue } from '../utils/crypto';\nimport { jsonResponse } from '../utils/response';\nimport {"
},
{
"path": "worker/services/MCPOAuthService.ts",
"chars": 8846,
"preview": "import {\n discoverOAuthEndpoints,\n buildAuthorizationUrl,\n exchangeCodeForTokens,\n registerClient,\n generateCodeVer"
},
{
"path": "worker/services/MCPService.ts",
"chars": 14845,
"preview": "import { MCPClient, type MCPServerConfig } from '../mcp/MCPClient';\nimport {\n getAccountById,\n getMCPDefinition,\n get"
},
{
"path": "worker/services/ScheduleService.ts",
"chars": 15418,
"preview": "import { jsonResponse } from '../utils/response';\nimport { transformScheduledRun, transformTask } from '../utils/transfo"
},
{
"path": "worker/services/WorkflowService.ts",
"chars": 8703,
"preview": "import { jsonResponse } from '../utils/response';\nimport { transformWorkflowPlan, transformWorkflowLog } from '../utils/"
},
{
"path": "worker/services/index.ts",
"chars": 397,
"preview": "export { BoardService } from './BoardService';\nexport { CredentialService } from './CredentialService';\nexport { MCPServ"
},
{
"path": "worker/utils/crypto.ts",
"chars": 2559,
"preview": "/**\n * Encryption utilities using Web Crypto API\n *\n * Uses AES-256-GCM for authenticated encryption.\n * Key is derived "
},
{
"path": "worker/utils/logger.ts",
"chars": 1857,
"preview": "/**\n * Structured logger for Cloudflare Workers\n *\n * Provides consistent log formatting with context.\n * In production,"
},
{
"path": "worker/utils/oauth-state.ts",
"chars": 2812,
"preview": "/**\n * Secure OAuth state encoding/decoding with HMAC signature\n *\n * Uses HMAC-SHA256 to sign the state, preventing tam"
},
{
"path": "worker/utils/response.ts",
"chars": 277,
"preview": "/**\n * HTTP response utilities\n */\n\n/**\n * Create a JSON response with proper headers\n */\nexport function jsonResponse(d"
},
{
"path": "worker/utils/transformations.ts",
"chars": 3356,
"preview": "/**\n * Data transformation utilities for API responses\n *\n * Converts snake_case database records to camelCase for clien"
},
{
"path": "worker/utils/zodTools.ts",
"chars": 5529,
"preview": "/**\n * Zod-based tool definitions for MCP servers\n *\n * This utility provides a single source of truth for MCP tool defi"
},
{
"path": "worker/workflows/AgentWorkflow.ts",
"chars": 55313,
"preview": "/**\n * AgentWorkflow - Dynamic agent-driven workflow execution\n *\n * Runs a Claude agent loop that dynamically creates s"
},
{
"path": "worker-configuration.d.ts",
"chars": 411648,
"preview": "/* eslint-disable */\n// Generated by Wrangler by running `wrangler types` (hash: d320f551ae3092561d41e7d1ed89d621)\n// Ru"
},
{
"path": "wrangler.jsonc",
"chars": 2369,
"preview": "{\n \"$schema\": \"node_modules/wrangler/config-schema.json\",\n \"name\": \"weft-dev\",\n \"main\": \"worker/index.ts\",\n \"compati"
}
]
About this extraction
This page contains the full source code of the jonesphillip/weft GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 171 files (1.5 MB), approximately 393.0k tokens, and a symbol index with 1515 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.