mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-28 16:27:55 -05:00
Compare commits
6 Commits
feat/termi
...
cleanup
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
276ce665e4 | ||
|
|
08bad4da9f | ||
|
|
37dbfe393a | ||
|
|
503f676910 | ||
|
|
f2ca90ae6f | ||
|
|
fe4fd47b9d |
@@ -55,21 +55,21 @@ export const {serviceName}{Action}Tool: ToolConfig<
|
||||
},
|
||||
|
||||
params: {
|
||||
// Hidden params (system-injected, only use hidden for oauth accessToken)
|
||||
// Hidden params (system-injected)
|
||||
accessToken: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'hidden',
|
||||
description: 'OAuth access token',
|
||||
},
|
||||
// User-only params (credentials, api key, IDs user must provide)
|
||||
// User-only params (credentials, IDs user must provide)
|
||||
someId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'The ID of the resource',
|
||||
},
|
||||
// User-or-LLM params (everything else, can be provided by user OR computed by LLM)
|
||||
// User-or-LLM params (can be provided by user OR computed by LLM)
|
||||
query: {
|
||||
type: 'string',
|
||||
required: false, // Use false for optional
|
||||
@@ -114,8 +114,8 @@ export const {serviceName}{Action}Tool: ToolConfig<
|
||||
|
||||
### Visibility Options
|
||||
- `'hidden'` - System-injected (OAuth tokens, internal params). User never sees.
|
||||
- `'user-only'` - User must provide (credentials, api keys, account-specific IDs)
|
||||
- `'user-or-llm'` - User provides OR LLM can compute (search queries, content, filters, most fall into this category)
|
||||
- `'user-only'` - User must provide (credentials, account-specific IDs)
|
||||
- `'user-or-llm'` - User provides OR LLM can compute (search queries, content, filters)
|
||||
|
||||
### Parameter Types
|
||||
- `'string'` - Text values
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,6 @@ import {
|
||||
CalendlyIcon,
|
||||
CirclebackIcon,
|
||||
ClayIcon,
|
||||
ClerkIcon,
|
||||
ConfluenceIcon,
|
||||
CursorIcon,
|
||||
DatadogIcon,
|
||||
@@ -144,7 +143,6 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
calendly: CalendlyIcon,
|
||||
circleback: CirclebackIcon,
|
||||
clay: ClayIcon,
|
||||
clerk: ClerkIcon,
|
||||
confluence_v2: ConfluenceIcon,
|
||||
cursor_v2: CursorIcon,
|
||||
datadog: DatadogIcon,
|
||||
|
||||
@@ -168,10 +168,10 @@ Search for tasks in an Asana workspace
|
||||
| `success` | boolean | Operation success status |
|
||||
| `ts` | string | Timestamp of the response |
|
||||
| `tasks` | array | Array of matching tasks |
|
||||
| ↳ `gid` | string | Task GID |
|
||||
| ↳ `gid` | string | Assignee GID |
|
||||
| ↳ `resource_type` | string | Resource type |
|
||||
| ↳ `resource_subtype` | string | Resource subtype |
|
||||
| ↳ `name` | string | Task name |
|
||||
| ↳ `name` | string | Assignee name |
|
||||
| ↳ `notes` | string | Task notes |
|
||||
| ↳ `completed` | boolean | Completion status |
|
||||
| ↳ `assignee` | object | Assignee details |
|
||||
|
||||
@@ -116,7 +116,7 @@ Get detailed information about a specific event type
|
||||
| --------- | ---- | ----------- |
|
||||
| `resource` | object | Event type details |
|
||||
| ↳ `uri` | string | Canonical reference to the event type |
|
||||
| ↳ `name` | string | Event type name |
|
||||
| ↳ `name` | string | Question text |
|
||||
| ↳ `active` | boolean | Whether the event type is active |
|
||||
| ↳ `booking_method` | string | Booking method |
|
||||
| ↳ `color` | string | Hex color code |
|
||||
@@ -128,12 +128,16 @@ Get detailed information about a specific event type
|
||||
| ↳ `enabled` | boolean | Whether question is enabled |
|
||||
| ↳ `required` | boolean | Whether question is required |
|
||||
| ↳ `answer_choices` | array | Available answer choices |
|
||||
| ↳ `type` | string | Event type classification |
|
||||
| ↳ `position` | number | Question order |
|
||||
| ↳ `enabled` | boolean | Whether question is enabled |
|
||||
| ↳ `required` | boolean | Whether question is required |
|
||||
| ↳ `answer_choices` | array | Available answer choices |
|
||||
| ↳ `description_html` | string | HTML formatted description |
|
||||
| ↳ `description_plain` | string | Plain text description |
|
||||
| ↳ `duration` | number | Duration in minutes |
|
||||
| ↳ `scheduling_url` | string | URL to scheduling page |
|
||||
| ↳ `slug` | string | Unique identifier for URLs |
|
||||
| ↳ `type` | string | Event type classification |
|
||||
| ↳ `updated_at` | string | ISO timestamp of last update |
|
||||
|
||||
### `calendly_list_scheduled_events`
|
||||
@@ -166,14 +170,16 @@ Retrieve a list of scheduled events for a user or organization
|
||||
| ↳ `start_time` | string | ISO timestamp of event start |
|
||||
| ↳ `end_time` | string | ISO timestamp of event end |
|
||||
| ↳ `event_type` | string | URI of the event type |
|
||||
| ↳ `location` | object | Event location details |
|
||||
| ↳ `type` | string | Location type \(e.g., |
|
||||
| ↳ `location` | string | Location description |
|
||||
| ↳ `type` | string | Location type \(e.g., |
|
||||
| ↳ `join_url` | string | URL to join online meeting \(if applicable\) |
|
||||
| ↳ `invitees_counter` | object | Invitee count information |
|
||||
| ↳ `total` | number | Total number of invitees |
|
||||
| ↳ `active` | number | Number of active invitees |
|
||||
| ↳ `limit` | number | Maximum number of invitees |
|
||||
| ↳ `total` | number | Total number of invitees |
|
||||
| ↳ `active` | number | Number of active invitees |
|
||||
| ↳ `limit` | number | Maximum number of invitees |
|
||||
| ↳ `created_at` | string | ISO timestamp of event creation |
|
||||
| ↳ `updated_at` | string | ISO timestamp of last update |
|
||||
| `pagination` | object | Pagination information |
|
||||
@@ -205,22 +211,28 @@ Get detailed information about a specific scheduled event
|
||||
| ↳ `start_time` | string | ISO timestamp of event start |
|
||||
| ↳ `end_time` | string | ISO timestamp of event end |
|
||||
| ↳ `event_type` | string | URI of the event type |
|
||||
| ↳ `location` | object | Event location details |
|
||||
| ↳ `type` | string | Location type |
|
||||
| ↳ `location` | string | Location description |
|
||||
| ↳ `join_url` | string | URL to join online meeting |
|
||||
| ↳ `location` | string | Location description |
|
||||
| ↳ `type` | string | Location type |
|
||||
| ↳ `join_url` | string | URL to join online meeting |
|
||||
| ↳ `invitees_counter` | object | Invitee count information |
|
||||
| ↳ `total` | number | Total number of invitees |
|
||||
| ↳ `active` | number | Number of active invitees |
|
||||
| ↳ `limit` | number | Maximum number of invitees |
|
||||
| ↳ `total` | number | Total number of invitees |
|
||||
| ↳ `active` | number | Number of active invitees |
|
||||
| ↳ `limit` | number | Maximum number of invitees |
|
||||
| ↳ `event_memberships` | array | Event hosts/members |
|
||||
| ↳ `user` | string | User URI |
|
||||
| ↳ `user_email` | string | User email |
|
||||
| ↳ `user_name` | string | User name |
|
||||
| ↳ `user` | string | User URI |
|
||||
| ↳ `user_email` | string | User email |
|
||||
| ↳ `user_name` | string | User name |
|
||||
| ↳ `event_guests` | array | Additional guests |
|
||||
| ↳ `email` | string | Guest email |
|
||||
| ↳ `created_at` | string | When guest was added |
|
||||
| ↳ `updated_at` | string | When guest info was updated |
|
||||
| ↳ `email` | string | Guest email |
|
||||
| ↳ `created_at` | string | ISO timestamp of event creation |
|
||||
| ↳ `updated_at` | string | ISO timestamp of last update |
|
||||
|
||||
@@ -255,6 +267,9 @@ Retrieve a list of invitees for a scheduled event
|
||||
| ↳ `question` | string | Question text |
|
||||
| ↳ `answer` | string | Invitee answer |
|
||||
| ↳ `position` | number | Question order |
|
||||
| ↳ `question` | string | Question text |
|
||||
| ↳ `answer` | string | Invitee answer |
|
||||
| ↳ `position` | number | Question order |
|
||||
| ↳ `timezone` | string | Invitee timezone |
|
||||
| ↳ `event` | string | URI of the scheduled event |
|
||||
| ↳ `created_at` | string | ISO timestamp when invitee was created |
|
||||
|
||||
@@ -1,442 +0,0 @@
|
||||
---
|
||||
title: Clerk
|
||||
description: Manage users, organizations, and sessions in Clerk
|
||||
---
|
||||
|
||||
import { BlockInfoCard } from "@/components/ui/block-info-card"
|
||||
|
||||
<BlockInfoCard
|
||||
type="clerk"
|
||||
color="#131316"
|
||||
/>
|
||||
|
||||
{/* MANUAL-CONTENT-START:intro */}
|
||||
[Clerk](https://clerk.com/) is a comprehensive identity infrastructure platform that helps you manage users, authentication, and sessions for your applications.
|
||||
|
||||
In Sim, the Clerk integration lets your agents automate user and session management through easy-to-use API-based tools. Agents can securely list users, update user profiles, manage organizations, monitor sessions, and revoke access directly in your workflow.
|
||||
|
||||
With Clerk, you can:
|
||||
|
||||
- **Authenticate users and manage sessions**: Seamlessly control sign-in, sign-up, and session lifecycle for your users.
|
||||
- **List and update users**: Automatically pull user lists, update user attributes, or view profile details as part of your agent tasks.
|
||||
- **Manage organizations and memberships**: Add or update organizations and administer user memberships with clarity.
|
||||
- **Monitor and revoke sessions**: See active or past user sessions, and revoke access immediately if needed for security.
|
||||
|
||||
The integration enables real-time, auditable management of your user base—all from within Sim. Connected agents can automate onboarding, enforce policies, keep directories up to date, and react to authentication events or organizational changes, helping you run secure and flexible processes using Clerk as your identity engine.
|
||||
{/* MANUAL-CONTENT-END */}
|
||||
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
Integrate Clerk authentication and user management into your workflow. Create, update, delete, and list users. Manage organizations and their memberships. Monitor and control user sessions.
|
||||
|
||||
|
||||
|
||||
## Tools
|
||||
|
||||
### `clerk_list_users`
|
||||
|
||||
List all users in your Clerk application with optional filtering and pagination
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `limit` | number | No | Number of results per page \(1-500, default: 10\) |
|
||||
| `offset` | number | No | Number of results to skip for pagination |
|
||||
| `orderBy` | string | No | Sort field with optional +/- prefix for direction \(default: -created_at\) |
|
||||
| `emailAddress` | string | No | Filter by email address \(comma-separated for multiple\) |
|
||||
| `phoneNumber` | string | No | Filter by phone number \(comma-separated for multiple\) |
|
||||
| `externalId` | string | No | Filter by external ID \(comma-separated for multiple\) |
|
||||
| `username` | string | No | Filter by username \(comma-separated for multiple\) |
|
||||
| `userId` | string | No | Filter by user ID \(comma-separated for multiple\) |
|
||||
| `query` | string | No | Search query to match across email, phone, username, and names |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `users` | array | Array of Clerk user objects |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username |
|
||||
| ↳ `firstName` | string | First name |
|
||||
| ↳ `lastName` | string | Last name |
|
||||
| ↳ `imageUrl` | string | Profile image URL |
|
||||
| ↳ `hasImage` | boolean | Whether user has a profile image |
|
||||
| ↳ `primaryEmailAddressId` | string | Primary email address ID |
|
||||
| ↳ `primaryPhoneNumberId` | string | Primary phone number ID |
|
||||
| ↳ `emailAddresses` | array | User email addresses |
|
||||
| ↳ `id` | string | Email address ID |
|
||||
| ↳ `emailAddress` | string | Email address |
|
||||
| ↳ `phoneNumbers` | array | User phone numbers |
|
||||
| ↳ `id` | string | Phone number ID |
|
||||
| ↳ `phoneNumber` | string | Phone number |
|
||||
| ↳ `externalId` | string | External system ID |
|
||||
| ↳ `passwordEnabled` | boolean | Whether password is enabled |
|
||||
| ↳ `twoFactorEnabled` | boolean | Whether 2FA is enabled |
|
||||
| ↳ `banned` | boolean | Whether user is banned |
|
||||
| ↳ `locked` | boolean | Whether user is locked |
|
||||
| ↳ `lastSignInAt` | number | Last sign-in timestamp |
|
||||
| ↳ `lastActiveAt` | number | Last activity timestamp |
|
||||
| ↳ `createdAt` | number | Creation timestamp |
|
||||
| ↳ `updatedAt` | number | Last update timestamp |
|
||||
| ↳ `publicMetadata` | json | Public metadata |
|
||||
| `totalCount` | number | Total number of users matching the query |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_get_user`
|
||||
|
||||
Retrieve a single user by their ID from Clerk
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `userId` | string | Yes | The ID of the user to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | User ID |
|
||||
| `username` | string | Username |
|
||||
| `firstName` | string | First name |
|
||||
| `lastName` | string | Last name |
|
||||
| `imageUrl` | string | Profile image URL |
|
||||
| `hasImage` | boolean | Whether user has a profile image |
|
||||
| `primaryEmailAddressId` | string | Primary email address ID |
|
||||
| `primaryPhoneNumberId` | string | Primary phone number ID |
|
||||
| `primaryWeb3WalletId` | string | Primary Web3 wallet ID |
|
||||
| `emailAddresses` | array | User email addresses |
|
||||
| ↳ `id` | string | Email address ID |
|
||||
| ↳ `emailAddress` | string | Email address |
|
||||
| ↳ `verified` | boolean | Whether email is verified |
|
||||
| `phoneNumbers` | array | User phone numbers |
|
||||
| ↳ `id` | string | Phone number ID |
|
||||
| ↳ `phoneNumber` | string | Phone number |
|
||||
| ↳ `verified` | boolean | Whether phone is verified |
|
||||
| `externalId` | string | External system ID |
|
||||
| `passwordEnabled` | boolean | Whether password is enabled |
|
||||
| `twoFactorEnabled` | boolean | Whether 2FA is enabled |
|
||||
| `totpEnabled` | boolean | Whether TOTP is enabled |
|
||||
| `backupCodeEnabled` | boolean | Whether backup codes are enabled |
|
||||
| `banned` | boolean | Whether user is banned |
|
||||
| `locked` | boolean | Whether user is locked |
|
||||
| `deleteSelfEnabled` | boolean | Whether user can delete themselves |
|
||||
| `createOrganizationEnabled` | boolean | Whether user can create organizations |
|
||||
| `lastSignInAt` | number | Last sign-in timestamp |
|
||||
| `lastActiveAt` | number | Last activity timestamp |
|
||||
| `createdAt` | number | Creation timestamp |
|
||||
| `updatedAt` | number | Last update timestamp |
|
||||
| `publicMetadata` | json | Public metadata \(readable from frontend\) |
|
||||
| `privateMetadata` | json | Private metadata \(backend only\) |
|
||||
| `unsafeMetadata` | json | Unsafe metadata \(modifiable from frontend\) |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_create_user`
|
||||
|
||||
Create a new user in your Clerk application
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `emailAddress` | string | No | Email addresses for the user \(comma-separated for multiple\) |
|
||||
| `phoneNumber` | string | No | Phone numbers for the user \(comma-separated for multiple\) |
|
||||
| `username` | string | No | Username for the user \(must be unique\) |
|
||||
| `password` | string | No | Password for the user \(minimum 8 characters\) |
|
||||
| `firstName` | string | No | First name of the user |
|
||||
| `lastName` | string | No | Last name of the user |
|
||||
| `externalId` | string | No | External system identifier \(must be unique\) |
|
||||
| `publicMetadata` | json | No | Public metadata \(JSON object, readable from frontend\) |
|
||||
| `privateMetadata` | json | No | Private metadata \(JSON object, backend only\) |
|
||||
| `unsafeMetadata` | json | No | Unsafe metadata \(JSON object, modifiable from frontend\) |
|
||||
| `skipPasswordChecks` | boolean | No | Skip password validation checks |
|
||||
| `skipPasswordRequirement` | boolean | No | Make password optional |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created user ID |
|
||||
| `username` | string | Username |
|
||||
| `firstName` | string | First name |
|
||||
| `lastName` | string | Last name |
|
||||
| `imageUrl` | string | Profile image URL |
|
||||
| `primaryEmailAddressId` | string | Primary email address ID |
|
||||
| `primaryPhoneNumberId` | string | Primary phone number ID |
|
||||
| `emailAddresses` | array | User email addresses |
|
||||
| ↳ `id` | string | Email address ID |
|
||||
| ↳ `emailAddress` | string | Email address |
|
||||
| ↳ `verified` | boolean | Whether email is verified |
|
||||
| `phoneNumbers` | array | User phone numbers |
|
||||
| ↳ `id` | string | Phone number ID |
|
||||
| ↳ `phoneNumber` | string | Phone number |
|
||||
| ↳ `verified` | boolean | Whether phone is verified |
|
||||
| `externalId` | string | External system ID |
|
||||
| `createdAt` | number | Creation timestamp |
|
||||
| `updatedAt` | number | Last update timestamp |
|
||||
| `publicMetadata` | json | Public metadata |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_update_user`
|
||||
|
||||
Update an existing user in your Clerk application
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `userId` | string | Yes | The ID of the user to update |
|
||||
| `firstName` | string | No | First name of the user |
|
||||
| `lastName` | string | No | Last name of the user |
|
||||
| `username` | string | No | Username \(must be unique\) |
|
||||
| `password` | string | No | New password \(minimum 8 characters\) |
|
||||
| `externalId` | string | No | External system identifier |
|
||||
| `primaryEmailAddressId` | string | No | ID of verified email to set as primary |
|
||||
| `primaryPhoneNumberId` | string | No | ID of verified phone to set as primary |
|
||||
| `publicMetadata` | json | No | Public metadata \(JSON object\) |
|
||||
| `privateMetadata` | json | No | Private metadata \(JSON object\) |
|
||||
| `unsafeMetadata` | json | No | Unsafe metadata \(JSON object\) |
|
||||
| `skipPasswordChecks` | boolean | No | Skip password validation checks |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Updated user ID |
|
||||
| `username` | string | Username |
|
||||
| `firstName` | string | First name |
|
||||
| `lastName` | string | Last name |
|
||||
| `imageUrl` | string | Profile image URL |
|
||||
| `primaryEmailAddressId` | string | Primary email address ID |
|
||||
| `primaryPhoneNumberId` | string | Primary phone number ID |
|
||||
| `emailAddresses` | array | User email addresses |
|
||||
| ↳ `id` | string | Email address ID |
|
||||
| ↳ `emailAddress` | string | Email address |
|
||||
| ↳ `verified` | boolean | Whether email is verified |
|
||||
| `phoneNumbers` | array | User phone numbers |
|
||||
| ↳ `id` | string | Phone number ID |
|
||||
| ↳ `phoneNumber` | string | Phone number |
|
||||
| ↳ `verified` | boolean | Whether phone is verified |
|
||||
| `externalId` | string | External system ID |
|
||||
| `banned` | boolean | Whether user is banned |
|
||||
| `locked` | boolean | Whether user is locked |
|
||||
| `createdAt` | number | Creation timestamp |
|
||||
| `updatedAt` | number | Last update timestamp |
|
||||
| `publicMetadata` | json | Public metadata |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_delete_user`
|
||||
|
||||
Delete a user from your Clerk application
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `userId` | string | Yes | The ID of the user to delete |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Deleted user ID |
|
||||
| `object` | string | Object type \(user\) |
|
||||
| `deleted` | boolean | Whether the user was deleted |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_list_organizations`
|
||||
|
||||
List all organizations in your Clerk application with optional filtering
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `limit` | number | No | Number of results per page \(1-500, default: 10\) |
|
||||
| `offset` | number | No | Number of results to skip for pagination |
|
||||
| `includeMembersCount` | boolean | No | Include member count for each organization |
|
||||
| `query` | string | No | Search by organization ID, name, or slug |
|
||||
| `orderBy` | string | No | Sort field \(name, created_at, members_count\) with +/- prefix |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `organizations` | array | Array of Clerk organization objects |
|
||||
| ↳ `id` | string | Organization ID |
|
||||
| ↳ `name` | string | Organization name |
|
||||
| ↳ `slug` | string | Organization slug |
|
||||
| ↳ `imageUrl` | string | Organization image URL |
|
||||
| ↳ `hasImage` | boolean | Whether organization has an image |
|
||||
| ↳ `membersCount` | number | Number of members |
|
||||
| ↳ `pendingInvitationsCount` | number | Number of pending invitations |
|
||||
| ↳ `maxAllowedMemberships` | number | Max allowed memberships |
|
||||
| ↳ `adminDeleteEnabled` | boolean | Whether admin delete is enabled |
|
||||
| ↳ `createdBy` | string | Creator user ID |
|
||||
| ↳ `createdAt` | number | Creation timestamp |
|
||||
| ↳ `updatedAt` | number | Last update timestamp |
|
||||
| ↳ `publicMetadata` | json | Public metadata |
|
||||
| `totalCount` | number | Total number of organizations |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_get_organization`
|
||||
|
||||
Retrieve a single organization by ID or slug from Clerk
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `organizationId` | string | Yes | The ID or slug of the organization to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Organization ID |
|
||||
| `name` | string | Organization name |
|
||||
| `slug` | string | Organization slug |
|
||||
| `imageUrl` | string | Organization image URL |
|
||||
| `hasImage` | boolean | Whether organization has an image |
|
||||
| `membersCount` | number | Number of members |
|
||||
| `pendingInvitationsCount` | number | Number of pending invitations |
|
||||
| `maxAllowedMemberships` | number | Max allowed memberships |
|
||||
| `adminDeleteEnabled` | boolean | Whether admin delete is enabled |
|
||||
| `createdBy` | string | Creator user ID |
|
||||
| `createdAt` | number | Creation timestamp |
|
||||
| `updatedAt` | number | Last update timestamp |
|
||||
| `publicMetadata` | json | Public metadata |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_create_organization`
|
||||
|
||||
Create a new organization in your Clerk application
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `name` | string | Yes | Name of the organization |
|
||||
| `createdBy` | string | Yes | User ID of the creator \(will become admin\) |
|
||||
| `slug` | string | No | Slug identifier for the organization |
|
||||
| `maxAllowedMemberships` | number | No | Maximum member capacity \(0 for unlimited\) |
|
||||
| `publicMetadata` | json | No | Public metadata \(JSON object\) |
|
||||
| `privateMetadata` | json | No | Private metadata \(JSON object\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Created organization ID |
|
||||
| `name` | string | Organization name |
|
||||
| `slug` | string | Organization slug |
|
||||
| `imageUrl` | string | Organization image URL |
|
||||
| `hasImage` | boolean | Whether organization has an image |
|
||||
| `membersCount` | number | Number of members |
|
||||
| `pendingInvitationsCount` | number | Number of pending invitations |
|
||||
| `maxAllowedMemberships` | number | Max allowed memberships |
|
||||
| `adminDeleteEnabled` | boolean | Whether admin delete is enabled |
|
||||
| `createdBy` | string | Creator user ID |
|
||||
| `createdAt` | number | Creation timestamp |
|
||||
| `updatedAt` | number | Last update timestamp |
|
||||
| `publicMetadata` | json | Public metadata |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_list_sessions`
|
||||
|
||||
List sessions for a user or client in your Clerk application
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `userId` | string | No | User ID to list sessions for \(required if clientId not provided\) |
|
||||
| `clientId` | string | No | Client ID to list sessions for \(required if userId not provided\) |
|
||||
| `status` | string | No | Filter by session status \(abandoned, active, ended, expired, pending, removed, replaced, revoked\) |
|
||||
| `limit` | number | No | Number of results per page \(1-500, default: 10\) |
|
||||
| `offset` | number | No | Number of results to skip for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `sessions` | array | Array of Clerk session objects |
|
||||
| ↳ `id` | string | Session ID |
|
||||
| ↳ `userId` | string | User ID |
|
||||
| ↳ `clientId` | string | Client ID |
|
||||
| ↳ `status` | string | Session status |
|
||||
| ↳ `lastActiveAt` | number | Last activity timestamp |
|
||||
| ↳ `lastActiveOrganizationId` | string | Last active organization ID |
|
||||
| ↳ `expireAt` | number | Expiration timestamp |
|
||||
| ↳ `abandonAt` | number | Abandon timestamp |
|
||||
| ↳ `createdAt` | number | Creation timestamp |
|
||||
| ↳ `updatedAt` | number | Last update timestamp |
|
||||
| `totalCount` | number | Total number of sessions |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_get_session`
|
||||
|
||||
Retrieve a single session by ID from Clerk
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `sessionId` | string | Yes | The ID of the session to retrieve |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Session ID |
|
||||
| `userId` | string | User ID |
|
||||
| `clientId` | string | Client ID |
|
||||
| `status` | string | Session status |
|
||||
| `lastActiveAt` | number | Last activity timestamp |
|
||||
| `lastActiveOrganizationId` | string | Last active organization ID |
|
||||
| `expireAt` | number | Expiration timestamp |
|
||||
| `abandonAt` | number | Abandon timestamp |
|
||||
| `createdAt` | number | Creation timestamp |
|
||||
| `updatedAt` | number | Last update timestamp |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
### `clerk_revoke_session`
|
||||
|
||||
Revoke a session to immediately invalidate it
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `secretKey` | string | Yes | The Clerk Secret Key for API authentication |
|
||||
| `sessionId` | string | Yes | The ID of the session to revoke |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Session ID |
|
||||
| `userId` | string | User ID |
|
||||
| `clientId` | string | Client ID |
|
||||
| `status` | string | Session status \(should be revoked\) |
|
||||
| `lastActiveAt` | number | Last activity timestamp |
|
||||
| `lastActiveOrganizationId` | string | Last active organization ID |
|
||||
| `expireAt` | number | Expiration timestamp |
|
||||
| `abandonAt` | number | Abandon timestamp |
|
||||
| `createdAt` | number | Creation timestamp |
|
||||
| `updatedAt` | number | Last update timestamp |
|
||||
| `success` | boolean | Operation success status |
|
||||
|
||||
|
||||
@@ -257,6 +257,11 @@ Search and retrieve logs from Datadog. Use for troubleshooting, analysis, or mon
|
||||
| ↳ `service` | string | Service name |
|
||||
| ↳ `message` | string | Log message |
|
||||
| ↳ `status` | string | Log status/level |
|
||||
| ↳ `timestamp` | string | Log timestamp |
|
||||
| ↳ `host` | string | Host name |
|
||||
| ↳ `service` | string | Service name |
|
||||
| ↳ `message` | string | Log message |
|
||||
| ↳ `status` | string | Log status/level |
|
||||
| `nextLogId` | string | Cursor for pagination |
|
||||
|
||||
### `datadog_send_logs`
|
||||
|
||||
@@ -64,7 +64,7 @@ Send a message to a Discord channel
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | string | Success or error message |
|
||||
| `data` | object | Discord message data |
|
||||
| ↳ `id` | string | Message ID |
|
||||
| ↳ `id` | string | Author user ID |
|
||||
| ↳ `content` | string | Message content |
|
||||
| ↳ `channel_id` | string | Channel ID where message was sent |
|
||||
| ↳ `author` | object | Message author information |
|
||||
@@ -72,6 +72,9 @@ Send a message to a Discord channel
|
||||
| ↳ `username` | string | Author username |
|
||||
| ↳ `avatar` | string | Author avatar hash |
|
||||
| ↳ `bot` | boolean | Whether author is a bot |
|
||||
| ↳ `username` | string | Author username |
|
||||
| ↳ `avatar` | string | Author avatar hash |
|
||||
| ↳ `bot` | boolean | Whether author is a bot |
|
||||
| ↳ `timestamp` | string | Message timestamp |
|
||||
| ↳ `edited_timestamp` | string | Message edited timestamp |
|
||||
| ↳ `embeds` | array | Message embeds |
|
||||
@@ -99,7 +102,7 @@ Retrieve messages from a Discord channel
|
||||
| `message` | string | Success or error message |
|
||||
| `data` | object | Container for messages data |
|
||||
| ↳ `messages` | array | Array of Discord messages with full metadata |
|
||||
| ↳ `id` | string | Message ID |
|
||||
| ↳ `id` | string | Author user ID |
|
||||
| ↳ `content` | string | Message content |
|
||||
| ↳ `channel_id` | string | Channel ID |
|
||||
| ↳ `author` | object | Message author information |
|
||||
@@ -107,6 +110,9 @@ Retrieve messages from a Discord channel
|
||||
| ↳ `username` | string | Author username |
|
||||
| ↳ `avatar` | string | Author avatar hash |
|
||||
| ↳ `bot` | boolean | Whether author is a bot |
|
||||
| ↳ `username` | string | Author username |
|
||||
| ↳ `avatar` | string | Author avatar hash |
|
||||
| ↳ `bot` | boolean | Whether author is a bot |
|
||||
| ↳ `timestamp` | string | Message timestamp |
|
||||
| ↳ `edited_timestamp` | string | Message edited timestamp |
|
||||
| ↳ `embeds` | array | Message embeds |
|
||||
@@ -114,7 +120,24 @@ Retrieve messages from a Discord channel
|
||||
| ↳ `mentions` | array | User mentions in message |
|
||||
| ↳ `mention_roles` | array | Role mentions in message |
|
||||
| ↳ `mention_everyone` | boolean | Whether message mentions everyone |
|
||||
| ↳ `id` | string | Author user ID |
|
||||
| ↳ `content` | string | Message content |
|
||||
| ↳ `channel_id` | string | Channel ID |
|
||||
| ↳ `author` | object | Message author information |
|
||||
| ↳ `id` | string | Author user ID |
|
||||
| ↳ `username` | string | Author username |
|
||||
| ↳ `avatar` | string | Author avatar hash |
|
||||
| ↳ `bot` | boolean | Whether author is a bot |
|
||||
| ↳ `username` | string | Author username |
|
||||
| ↳ `avatar` | string | Author avatar hash |
|
||||
| ↳ `bot` | boolean | Whether author is a bot |
|
||||
| ↳ `timestamp` | string | Message timestamp |
|
||||
| ↳ `edited_timestamp` | string | Message edited timestamp |
|
||||
| ↳ `embeds` | array | Message embeds |
|
||||
| ↳ `attachments` | array | Message attachments |
|
||||
| ↳ `mentions` | array | User mentions in message |
|
||||
| ↳ `mention_roles` | array | Role mentions in message |
|
||||
| ↳ `mention_everyone` | boolean | Whether message mentions everyone |
|
||||
|
||||
### `discord_get_server`
|
||||
|
||||
@@ -658,6 +681,9 @@ Get information about a member in a Discord server
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username |
|
||||
| ↳ `avatar` | string | Avatar hash |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `username` | string | Username |
|
||||
| ↳ `avatar` | string | Avatar hash |
|
||||
| ↳ `nick` | string | Server nickname |
|
||||
| ↳ `roles` | array | Array of role IDs |
|
||||
| ↳ `joined_at` | string | When the member joined |
|
||||
|
||||
@@ -105,6 +105,11 @@ Crawl entire websites and extract structured content from all accessible pages
|
||||
| ↳ `language` | string | Page language |
|
||||
| ↳ `sourceURL` | string | Source URL of the page |
|
||||
| ↳ `statusCode` | number | HTTP status code |
|
||||
| ↳ `title` | string | Page title |
|
||||
| ↳ `description` | string | Page description |
|
||||
| ↳ `language` | string | Page language |
|
||||
| ↳ `sourceURL` | string | Source URL of the page |
|
||||
| ↳ `statusCode` | number | HTTP status code |
|
||||
| `total` | number | Total number of pages found during crawl |
|
||||
| `creditsUsed` | number | Number of credits consumed by the crawl operation |
|
||||
|
||||
|
||||
@@ -1557,20 +1557,20 @@ Search for code across GitHub repositories. Use qualifiers like repo:owner/name,
|
||||
| `total_count` | number | Total matching results |
|
||||
| `incomplete_results` | boolean | Whether results are incomplete |
|
||||
| `items` | array | Array of code matches from GitHub API |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `name` | string | Repository name |
|
||||
| ↳ `path` | string | File path |
|
||||
| ↳ `sha` | string | Blob SHA |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `git_url` | string | Git blob URL |
|
||||
| ↳ `html_url` | string | GitHub web URL |
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `score` | number | Search relevance score |
|
||||
| ↳ `repository` | object | Repository containing the code |
|
||||
| ↳ `id` | number | Repository ID |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `name` | string | Repository name |
|
||||
| ↳ `full_name` | string | Full name \(owner/repo\) |
|
||||
| ↳ `private` | boolean | Whether repository is private |
|
||||
| ↳ `html_url` | string | GitHub web URL |
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `description` | string | Repository description |
|
||||
| ↳ `fork` | boolean | Whether this is a fork |
|
||||
| ↳ `url` | string | API URL |
|
||||
@@ -1583,6 +1583,29 @@ Search for code across GitHub repositories. Use qualifiers like repo:owner/name,
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `full_name` | string | Full name \(owner/repo\) |
|
||||
| ↳ `private` | boolean | Whether repository is private |
|
||||
| ↳ `description` | string | Repository description |
|
||||
| ↳ `fork` | boolean | Whether this is a fork |
|
||||
| ↳ `owner` | object | Repository owner |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `text_matches` | array | Text matches showing context |
|
||||
| ↳ `object_url` | string | Object URL |
|
||||
| ↳ `object_type` | string | Object type |
|
||||
@@ -1591,6 +1614,17 @@ Search for code across GitHub repositories. Use qualifiers like repo:owner/name,
|
||||
| ↳ `matches` | array | Match indices |
|
||||
| ↳ `text` | string | Matched text |
|
||||
| ↳ `indices` | array | Start and end indices |
|
||||
| ↳ `text` | string | Matched text |
|
||||
| ↳ `indices` | array | Start and end indices |
|
||||
| ↳ `object_url` | string | Object URL |
|
||||
| ↳ `object_type` | string | Object type |
|
||||
| ↳ `property` | string | Property matched |
|
||||
| ↳ `fragment` | string | Text fragment with match |
|
||||
| ↳ `matches` | array | Match indices |
|
||||
| ↳ `text` | string | Matched text |
|
||||
| ↳ `indices` | array | Start and end indices |
|
||||
| ↳ `text` | string | Matched text |
|
||||
| ↳ `indices` | array | Start and end indices |
|
||||
|
||||
### `github_search_commits`
|
||||
|
||||
@@ -1614,20 +1648,23 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us
|
||||
| `total_count` | number | Total matching results |
|
||||
| `incomplete_results` | boolean | Whether results are incomplete |
|
||||
| `items` | array | Array of commit objects from GitHub API |
|
||||
| ↳ `sha` | string | Commit SHA |
|
||||
| ↳ `sha` | string | Parent SHA |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `html_url` | string | Parent web URL |
|
||||
| ↳ `url` | string | Parent API URL |
|
||||
| ↳ `comments_url` | string | Comments API URL |
|
||||
| ↳ `score` | number | Search relevance score |
|
||||
| ↳ `commit` | object | Core commit data |
|
||||
| ↳ `url` | string | Commit API URL |
|
||||
| ↳ `url` | string | Tree API URL |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `comment_count` | number | Number of comments |
|
||||
| ↳ `author` | object | Git author |
|
||||
| ↳ `name` | string | Author name |
|
||||
| ↳ `email` | string | Author email |
|
||||
| ↳ `date` | string | Author date \(ISO 8601\) |
|
||||
| ↳ `name` | string | Committer name |
|
||||
| ↳ `email` | string | Committer email |
|
||||
| ↳ `date` | string | Commit date \(ISO 8601\) |
|
||||
| ↳ `committer` | object | Git committer |
|
||||
| ↳ `name` | string | Committer name |
|
||||
| ↳ `email` | string | Committer email |
|
||||
@@ -1635,6 +1672,9 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us
|
||||
| ↳ `tree` | object | Tree object |
|
||||
| ↳ `sha` | string | Tree SHA |
|
||||
| ↳ `url` | string | Tree API URL |
|
||||
| ↳ `sha` | string | Tree SHA |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `comment_count` | number | Number of comments |
|
||||
| ↳ `author` | object | GitHub user \(author\) |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
@@ -1644,6 +1684,9 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us
|
||||
| ↳ `html_url` | string | Profile URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `name` | string | Repository name |
|
||||
| ↳ `email` | string | Committer email |
|
||||
| ↳ `date` | string | Commit date \(ISO 8601\) |
|
||||
| ↳ `committer` | object | GitHub user \(committer\) |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
@@ -1653,13 +1696,38 @@ Search for commits across GitHub. Use qualifiers like repo:owner/name, author:us
|
||||
| ↳ `html_url` | string | Profile URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `tree` | object | Tree object |
|
||||
| ↳ `sha` | string | Tree SHA |
|
||||
| ↳ `url` | string | Tree API URL |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `repository` | object | Repository containing the commit |
|
||||
| ↳ `id` | number | Repository ID |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `name` | string | Repository name |
|
||||
| ↳ `full_name` | string | Full name \(owner/repo\) |
|
||||
| ↳ `private` | boolean | Whether repository is private |
|
||||
| ↳ `html_url` | string | GitHub web URL |
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `description` | string | Repository description |
|
||||
| ↳ `owner` | object | Repository owner |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `full_name` | string | Full name \(owner/repo\) |
|
||||
| ↳ `private` | boolean | Whether repository is private |
|
||||
| ↳ `description` | string | Repository description |
|
||||
| ↳ `owner` | object | Repository owner |
|
||||
| ↳ `login` | string | Username |
|
||||
@@ -1697,10 +1765,10 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne
|
||||
| `total_count` | number | Total matching results |
|
||||
| `incomplete_results` | boolean | Whether results are incomplete |
|
||||
| `items` | array | Array of issue/PR objects from GitHub API |
|
||||
| ↳ `id` | number | Issue ID |
|
||||
| ↳ `id` | number | Milestone ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `number` | number | Issue number |
|
||||
| ↳ `title` | string | Title |
|
||||
| ↳ `number` | number | Milestone number |
|
||||
| ↳ `title` | string | Milestone title |
|
||||
| ↳ `state` | string | State \(open or closed\) |
|
||||
| ↳ `locked` | boolean | Whether issue is locked |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
@@ -1722,6 +1790,10 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `labels` | array | Issue labels |
|
||||
| ↳ `id` | number | Label ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
@@ -1730,6 +1802,10 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne
|
||||
| ↳ `description` | string | Label description |
|
||||
| ↳ `color` | string | Hex color code |
|
||||
| ↳ `default` | boolean | Whether this is a default label |
|
||||
| ↳ `name` | string | Label name |
|
||||
| ↳ `description` | string | Milestone description |
|
||||
| ↳ `color` | string | Hex color code |
|
||||
| ↳ `default` | boolean | Whether this is a default label |
|
||||
| ↳ `assignee` | object | Primary assignee |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
@@ -1757,11 +1833,14 @@ Search for issues and pull requests across GitHub. Use qualifiers like repo:owne
|
||||
| ↳ `state` | string | State \(open or closed\) |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `due_on` | string | Due date |
|
||||
| ↳ `due_on` | string | Due date |
|
||||
| ↳ `pull_request` | object | Pull request details \(if this is a PR\) |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `diff_url` | string | Diff URL |
|
||||
| ↳ `patch_url` | string | Patch URL |
|
||||
| ↳ `diff_url` | string | Diff URL |
|
||||
| ↳ `patch_url` | string | Patch URL |
|
||||
|
||||
### `github_search_repos`
|
||||
|
||||
@@ -1785,13 +1864,13 @@ Search for repositories across GitHub. Use qualifiers like language:python, star
|
||||
| `total_count` | number | Total matching results |
|
||||
| `incomplete_results` | boolean | Whether results are incomplete |
|
||||
| `items` | array | Array of repository objects from GitHub API |
|
||||
| ↳ `id` | number | Repository ID |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `name` | string | Repository name |
|
||||
| ↳ `name` | string | License name |
|
||||
| ↳ `full_name` | string | Full name \(owner/repo\) |
|
||||
| ↳ `private` | boolean | Whether repository is private |
|
||||
| ↳ `description` | string | Repository description |
|
||||
| ↳ `html_url` | string | GitHub web URL |
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `fork` | boolean | Whether this is a fork |
|
||||
| ↳ `created_at` | string | Creation timestamp |
|
||||
@@ -1810,6 +1889,8 @@ Search for repositories across GitHub. Use qualifiers like language:python, star
|
||||
| ↳ `key` | string | License key \(e.g., mit\) |
|
||||
| ↳ `name` | string | License name |
|
||||
| ↳ `spdx_id` | string | SPDX identifier |
|
||||
| ↳ `key` | string | License key \(e.g., mit\) |
|
||||
| ↳ `spdx_id` | string | SPDX identifier |
|
||||
| ↳ `owner` | object | Repository owner |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
@@ -1819,6 +1900,10 @@ Search for repositories across GitHub. Use qualifiers like language:python, star
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
|
||||
### `github_search_users`
|
||||
|
||||
@@ -1884,19 +1969,22 @@ List commits in a repository with optional filtering by SHA, path, author, commi
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of commit objects from GitHub API |
|
||||
| ↳ `sha` | string | Commit SHA |
|
||||
| ↳ `sha` | string | Parent SHA |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `html_url` | string | Parent web URL |
|
||||
| ↳ `url` | string | Parent API URL |
|
||||
| ↳ `comments_url` | string | Comments API URL |
|
||||
| ↳ `commit` | object | Core commit data |
|
||||
| ↳ `url` | string | Commit API URL |
|
||||
| ↳ `url` | string | Tree API URL |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `comment_count` | number | Number of comments |
|
||||
| ↳ `author` | object | Git author |
|
||||
| ↳ `name` | string | Author name |
|
||||
| ↳ `email` | string | Author email |
|
||||
| ↳ `date` | string | Author date \(ISO 8601\) |
|
||||
| ↳ `name` | string | Committer name |
|
||||
| ↳ `email` | string | Committer email |
|
||||
| ↳ `date` | string | Commit date \(ISO 8601\) |
|
||||
| ↳ `committer` | object | Git committer |
|
||||
| ↳ `name` | string | Committer name |
|
||||
| ↳ `email` | string | Committer email |
|
||||
@@ -1904,11 +1992,18 @@ List commits in a repository with optional filtering by SHA, path, author, commi
|
||||
| ↳ `tree` | object | Tree object |
|
||||
| ↳ `sha` | string | Tree SHA |
|
||||
| ↳ `url` | string | Tree API URL |
|
||||
| ↳ `sha` | string | Tree SHA |
|
||||
| ↳ `verification` | object | Signature verification |
|
||||
| ↳ `verified` | boolean | Whether signature is verified |
|
||||
| ↳ `reason` | string | Verification reason |
|
||||
| ↳ `signature` | string | GPG signature |
|
||||
| ↳ `payload` | string | Signed payload |
|
||||
| ↳ `verified` | boolean | Whether signature is verified |
|
||||
| ↳ `reason` | string | Verification reason |
|
||||
| ↳ `signature` | string | GPG signature |
|
||||
| ↳ `payload` | string | Signed payload |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `comment_count` | number | Number of comments |
|
||||
| ↳ `author` | object | GitHub user \(author\) |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
@@ -1918,6 +2013,9 @@ List commits in a repository with optional filtering by SHA, path, author, commi
|
||||
| ↳ `html_url` | string | Profile URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `name` | string | Committer name |
|
||||
| ↳ `email` | string | Committer email |
|
||||
| ↳ `date` | string | Commit date \(ISO 8601\) |
|
||||
| ↳ `committer` | object | GitHub user \(committer\) |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
@@ -1927,6 +2025,23 @@ List commits in a repository with optional filtering by SHA, path, author, commi
|
||||
| ↳ `html_url` | string | Profile URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `tree` | object | Tree object |
|
||||
| ↳ `sha` | string | Tree SHA |
|
||||
| ↳ `url` | string | Tree API URL |
|
||||
| ↳ `verification` | object | Signature verification |
|
||||
| ↳ `verified` | boolean | Whether signature is verified |
|
||||
| ↳ `reason` | string | Verification reason |
|
||||
| ↳ `signature` | string | GPG signature |
|
||||
| ↳ `payload` | string | Signed payload |
|
||||
| ↳ `verified` | boolean | Whether signature is verified |
|
||||
| ↳ `reason` | string | Verification reason |
|
||||
| ↳ `signature` | string | GPG signature |
|
||||
| ↳ `payload` | string | Signed payload |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `avatar_url` | string | Avatar URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `parents` | array | Parent commits |
|
||||
| ↳ `sha` | string | Parent SHA |
|
||||
| ↳ `url` | string | Parent API URL |
|
||||
@@ -1956,13 +2071,16 @@ Get detailed information about a specific commit including files changed and sta
|
||||
| `url` | string | API URL |
|
||||
| `comments_url` | string | Comments API URL |
|
||||
| `commit` | object | Core commit data |
|
||||
| ↳ `url` | string | Commit API URL |
|
||||
| ↳ `url` | string | Tree API URL |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `comment_count` | number | Number of comments |
|
||||
| ↳ `author` | object | Git author |
|
||||
| ↳ `name` | string | Author name |
|
||||
| ↳ `email` | string | Author email |
|
||||
| ↳ `date` | string | Author date \(ISO 8601\) |
|
||||
| ↳ `name` | string | Committer name |
|
||||
| ↳ `email` | string | Committer email |
|
||||
| ↳ `date` | string | Commit date \(ISO 8601\) |
|
||||
| ↳ `committer` | object | Git committer |
|
||||
| ↳ `name` | string | Committer name |
|
||||
| ↳ `email` | string | Committer email |
|
||||
@@ -1970,11 +2088,16 @@ Get detailed information about a specific commit including files changed and sta
|
||||
| ↳ `tree` | object | Tree object |
|
||||
| ↳ `sha` | string | Tree SHA |
|
||||
| ↳ `url` | string | Tree API URL |
|
||||
| ↳ `sha` | string | Tree SHA |
|
||||
| ↳ `verification` | object | Signature verification |
|
||||
| ↳ `verified` | boolean | Whether signature is verified |
|
||||
| ↳ `reason` | string | Verification reason |
|
||||
| ↳ `signature` | string | GPG signature |
|
||||
| ↳ `payload` | string | Signed payload |
|
||||
| ↳ `verified` | boolean | Whether signature is verified |
|
||||
| ↳ `reason` | string | Verification reason |
|
||||
| ↳ `signature` | string | GPG signature |
|
||||
| ↳ `payload` | string | Signed payload |
|
||||
| `author` | object | GitHub user \(author\) |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
@@ -2044,6 +2167,7 @@ Compare two commits or branches to see the diff, commits between them, and chang
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `author` | object | Git author \(name, email, date\) |
|
||||
| ↳ `committer` | object | Git committer \(name, email, date\) |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `author` | object | GitHub user \(author\) |
|
||||
| ↳ `committer` | object | GitHub user \(committer\) |
|
||||
| `merge_base_commit` | object | Merge base commit object |
|
||||
@@ -2056,6 +2180,7 @@ Compare two commits or branches to see the diff, commits between them, and chang
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `author` | object | Git author \(name, email, date\) |
|
||||
| ↳ `committer` | object | Git committer \(name, email, date\) |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `author` | object | GitHub user |
|
||||
| ↳ `committer` | object | GitHub user |
|
||||
| `files` | array | Changed files \(diff entries\) |
|
||||
@@ -2146,6 +2271,13 @@ Get a gist by ID including its file contents
|
||||
| `comments_url` | string | Comments API URL |
|
||||
| `truncated` | boolean | Whether content is truncated |
|
||||
| `files` | object | Files in the gist \(keyed by filename\) |
|
||||
| ↳ `filename` | string | File name |
|
||||
| ↳ `type` | string | MIME type |
|
||||
| ↳ `language` | string | Programming language |
|
||||
| ↳ `raw_url` | string | Raw file URL |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| ↳ `truncated` | boolean | Whether content is truncated |
|
||||
| ↳ `content` | string | File content |
|
||||
| `owner` | object | Gist owner |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
@@ -2175,10 +2307,10 @@ List gists for a user or the authenticated user
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of gist objects from GitHub API |
|
||||
| ↳ `id` | string | Gist ID |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `forks_url` | string | Forks API URL |
|
||||
| ↳ `commits_url` | string | Commits API URL |
|
||||
| ↳ `git_pull_url` | string | Git pull URL |
|
||||
@@ -2200,6 +2332,10 @@ List gists for a user or the authenticated user
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| `count` | number | Number of gists returned |
|
||||
|
||||
### `github_update_gist`
|
||||
@@ -2367,13 +2503,14 @@ Fork a repository to your account or an organization
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| `parent` | object | Parent repository \(source of the fork\) |
|
||||
| ↳ `id` | number | Repository ID |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `full_name` | string | Full name |
|
||||
| ↳ `html_url` | string | Web URL |
|
||||
| ↳ `description` | string | Description |
|
||||
| ↳ `owner` | object | Parent owner |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `login` | string | Username |
|
||||
| `source` | object | Source repository \(ultimate origin\) |
|
||||
| ↳ `id` | number | Repository ID |
|
||||
| ↳ `full_name` | string | Full name |
|
||||
@@ -2399,13 +2536,13 @@ List forks of a repository
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of fork repository objects from GitHub API |
|
||||
| ↳ `id` | number | Repository ID |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `name` | string | Repository name |
|
||||
| ↳ `full_name` | string | Full name \(owner/repo\) |
|
||||
| ↳ `private` | boolean | Whether repository is private |
|
||||
| ↳ `description` | string | Repository description |
|
||||
| ↳ `html_url` | string | GitHub web URL |
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `fork` | boolean | Whether this is a fork |
|
||||
| ↳ `created_at` | string | Creation timestamp |
|
||||
@@ -2430,6 +2567,10 @@ List forks of a repository
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| `count` | number | Number of forks returned |
|
||||
|
||||
### `github_create_milestone`
|
||||
@@ -2526,14 +2667,14 @@ List milestones in a repository
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `items` | array | Array of milestone objects from GitHub API |
|
||||
| ↳ `id` | number | Milestone ID |
|
||||
| ↳ `id` | number | User ID |
|
||||
| ↳ `node_id` | string | GraphQL node ID |
|
||||
| ↳ `number` | number | Milestone number |
|
||||
| ↳ `title` | string | Milestone title |
|
||||
| ↳ `description` | string | Milestone description |
|
||||
| ↳ `state` | string | State \(open or closed\) |
|
||||
| ↳ `url` | string | API URL |
|
||||
| ↳ `html_url` | string | GitHub web URL |
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `labels_url` | string | Labels API URL |
|
||||
| ↳ `due_on` | string | Due date \(ISO 8601\) |
|
||||
| ↳ `open_issues` | number | Number of open issues |
|
||||
@@ -2550,6 +2691,10 @@ List milestones in a repository
|
||||
| ↳ `html_url` | string | Profile page URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| ↳ `login` | string | Username |
|
||||
| ↳ `avatar_url` | string | Avatar image URL |
|
||||
| ↳ `type` | string | User or Organization |
|
||||
| ↳ `site_admin` | boolean | GitHub staff indicator |
|
||||
| `count` | number | Number of milestones returned |
|
||||
|
||||
### `github_update_milestone`
|
||||
|
||||
@@ -139,10 +139,25 @@ Apply multiple updates to a form (add items, update info, change settings, etc.)
|
||||
| ↳ `title` | string | The form title visible to responders |
|
||||
| ↳ `description` | string | The form description |
|
||||
| ↳ `documentTitle` | string | The document title visible in Drive |
|
||||
| ↳ `title` | string | Item title |
|
||||
| ↳ `description` | string | Item description |
|
||||
| ↳ `documentTitle` | string | The document title visible in Drive |
|
||||
| ↳ `settings` | object | Form settings |
|
||||
| ↳ `quizSettings` | object | Quiz settings |
|
||||
| ↳ `isQuiz` | boolean | Whether the form is a quiz |
|
||||
| ↳ `isQuiz` | boolean | Whether the form is a quiz |
|
||||
| ↳ `emailCollectionType` | string | Email collection type |
|
||||
| ↳ `quizSettings` | object | Quiz settings |
|
||||
| ↳ `isQuiz` | boolean | Whether the form is a quiz |
|
||||
| ↳ `isQuiz` | boolean | Whether the form is a quiz |
|
||||
| ↳ `emailCollectionType` | string | Email collection type |
|
||||
| ↳ `itemId` | string | Item ID |
|
||||
| ↳ `questionItem` | json | Question item configuration |
|
||||
| ↳ `questionGroupItem` | json | Question group configuration |
|
||||
| ↳ `pageBreakItem` | json | Page break configuration |
|
||||
| ↳ `textItem` | json | Text item configuration |
|
||||
| ↳ `imageItem` | json | Image item configuration |
|
||||
| ↳ `videoItem` | json | Video item configuration |
|
||||
| ↳ `revisionId` | string | The revision ID of the form |
|
||||
| ↳ `responderUri` | string | The URI to share with responders |
|
||||
| ↳ `linkedSheetId` | string | The ID of the linked Google Sheet |
|
||||
@@ -150,6 +165,13 @@ Apply multiple updates to a form (add items, update info, change settings, etc.)
|
||||
| ↳ `publishState` | object | Current publish state |
|
||||
| ↳ `isPublished` | boolean | Whether the form is published |
|
||||
| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses |
|
||||
| ↳ `isPublished` | boolean | Whether the form is published |
|
||||
| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses |
|
||||
| ↳ `publishState` | object | Current publish state |
|
||||
| ↳ `isPublished` | boolean | Whether the form is published |
|
||||
| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses |
|
||||
| ↳ `isPublished` | boolean | Whether the form is published |
|
||||
| ↳ `isAcceptingResponses` | boolean | Whether the form is accepting responses |
|
||||
|
||||
### `google_forms_set_publish_settings`
|
||||
|
||||
@@ -172,6 +194,8 @@ Update the publish settings of a form (publish/unpublish, accept responses)
|
||||
| ↳ `publishState` | object | The publish state |
|
||||
| ↳ `isPublished` | boolean | Whether the form is published |
|
||||
| ↳ `isAcceptingResponses` | boolean | Whether the form accepts responses |
|
||||
| ↳ `isPublished` | boolean | Whether the form is published |
|
||||
| ↳ `isAcceptingResponses` | boolean | Whether the form accepts responses |
|
||||
|
||||
### `google_forms_create_watch`
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ Read content from a Google Slides presentation
|
||||
| ↳ `pageSize` | object | Presentation page size |
|
||||
| ↳ `width` | json | Page width as a Dimension object |
|
||||
| ↳ `height` | json | Page height as a Dimension object |
|
||||
| ↳ `width` | json | Page width as a Dimension object |
|
||||
| ↳ `height` | json | Page height as a Dimension object |
|
||||
| ↳ `mimeType` | string | The mime type of the presentation |
|
||||
| ↳ `url` | string | URL to open the presentation |
|
||||
|
||||
|
||||
@@ -60,5 +60,8 @@ Generate completions using Hugging Face Inference API
|
||||
| ↳ `prompt_tokens` | number | Number of tokens in the prompt |
|
||||
| ↳ `completion_tokens` | number | Number of tokens in the completion |
|
||||
| ↳ `total_tokens` | number | Total number of tokens used |
|
||||
| ↳ `prompt_tokens` | number | Number of tokens in the prompt |
|
||||
| ↳ `completion_tokens` | number | Number of tokens in the completion |
|
||||
| ↳ `total_tokens` | number | Total number of tokens used |
|
||||
|
||||
|
||||
|
||||
@@ -62,5 +62,6 @@ Generate images using OpenAI
|
||||
| ↳ `image` | string | Base64 encoded image data |
|
||||
| ↳ `metadata` | object | Image generation metadata |
|
||||
| ↳ `model` | string | Model used for image generation |
|
||||
| ↳ `model` | string | Model used for image generation |
|
||||
|
||||
|
||||
|
||||
@@ -57,8 +57,8 @@ List incidents from incident.io. Returns a list of incidents with their details
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incidents` | array | List of incidents |
|
||||
| ↳ `id` | string | Incident ID |
|
||||
| ↳ `name` | string | Incident name |
|
||||
| ↳ `id` | string | Type ID |
|
||||
| ↳ `name` | string | Type name |
|
||||
| ↳ `summary` | string | Brief summary of the incident |
|
||||
| ↳ `description` | string | Detailed description of the incident |
|
||||
| ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) |
|
||||
@@ -67,10 +67,12 @@ List incidents from incident.io. Returns a list of incidents with their details
|
||||
| ↳ `id` | string | Severity ID |
|
||||
| ↳ `name` | string | Severity name |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `status` | object | Current status of the incident |
|
||||
| ↳ `id` | string | Status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `incident_type` | object | Type of the incident |
|
||||
| ↳ `id` | string | Type ID |
|
||||
| ↳ `name` | string | Type name |
|
||||
@@ -107,8 +109,8 @@ Create a new incident in incident.io. Requires idempotency_key, severity_id, and
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident` | object | The created incident object |
|
||||
| ↳ `id` | string | Incident ID |
|
||||
| ↳ `name` | string | Incident name |
|
||||
| ↳ `id` | string | Type ID |
|
||||
| ↳ `name` | string | Type name |
|
||||
| ↳ `summary` | string | Brief summary of the incident |
|
||||
| ↳ `description` | string | Detailed description of the incident |
|
||||
| ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) |
|
||||
@@ -117,10 +119,12 @@ Create a new incident in incident.io. Requires idempotency_key, severity_id, and
|
||||
| ↳ `id` | string | Severity ID |
|
||||
| ↳ `name` | string | Severity name |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `status` | object | Current status of the incident |
|
||||
| ↳ `id` | string | Status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `incident_type` | object | Type of the incident |
|
||||
| ↳ `id` | string | Type ID |
|
||||
| ↳ `name` | string | Type name |
|
||||
@@ -147,8 +151,8 @@ Retrieve detailed information about a specific incident from incident.io by its
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident` | object | Detailed incident information |
|
||||
| ↳ `id` | string | Incident ID |
|
||||
| ↳ `name` | string | Incident name |
|
||||
| ↳ `id` | string | Type ID |
|
||||
| ↳ `name` | string | Type name |
|
||||
| ↳ `summary` | string | Brief summary of the incident |
|
||||
| ↳ `description` | string | Detailed description of the incident |
|
||||
| ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) |
|
||||
@@ -158,10 +162,12 @@ Retrieve detailed information about a specific incident from incident.io by its
|
||||
| ↳ `id` | string | Severity ID |
|
||||
| ↳ `name` | string | Severity name |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `status` | object | Current status of the incident |
|
||||
| ↳ `id` | string | Status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `incident_type` | object | Type of the incident |
|
||||
| ↳ `id` | string | Type ID |
|
||||
| ↳ `name` | string | Type name |
|
||||
@@ -196,8 +202,8 @@ Update an existing incident in incident.io. Can update name, summary, severity,
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident` | object | The updated incident object |
|
||||
| ↳ `id` | string | Incident ID |
|
||||
| ↳ `name` | string | Incident name |
|
||||
| ↳ `id` | string | Type ID |
|
||||
| ↳ `name` | string | Type name |
|
||||
| ↳ `summary` | string | Brief summary of the incident |
|
||||
| ↳ `description` | string | Detailed description of the incident |
|
||||
| ↳ `mode` | string | Incident mode \(e.g., standard, retrospective\) |
|
||||
@@ -206,10 +212,12 @@ Update an existing incident in incident.io. Can update name, summary, severity,
|
||||
| ↳ `id` | string | Severity ID |
|
||||
| ↳ `name` | string | Severity name |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `status` | object | Current status of the incident |
|
||||
| ↳ `id` | string | Status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `incident_type` | object | Type of the incident |
|
||||
| ↳ `id` | string | Type ID |
|
||||
| ↳ `name` | string | Type name |
|
||||
@@ -237,12 +245,14 @@ List actions from incident.io. Optionally filter by incident ID.
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `actions` | array | List of actions |
|
||||
| ↳ `id` | string | Action ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `description` | string | Action description |
|
||||
| ↳ `assignee` | object | Assigned user |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `status` | string | Action status |
|
||||
| ↳ `due_at` | string | Due date/time |
|
||||
| ↳ `created_at` | string | Creation timestamp |
|
||||
@@ -257,6 +267,9 @@ List actions from incident.io. Optionally filter by incident ID.
|
||||
| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) |
|
||||
| ↳ `issue_name` | string | Issue identifier |
|
||||
| ↳ `issue_permalink` | string | URL to the external issue |
|
||||
| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) |
|
||||
| ↳ `issue_name` | string | Issue identifier |
|
||||
| ↳ `issue_permalink` | string | URL to the external issue |
|
||||
|
||||
### `incidentio_actions_show`
|
||||
|
||||
@@ -274,12 +287,14 @@ Get detailed information about a specific action from incident.io.
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `action` | object | Action details |
|
||||
| ↳ `id` | string | Action ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `description` | string | Action description |
|
||||
| ↳ `assignee` | object | Assigned user |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `status` | string | Action status |
|
||||
| ↳ `due_at` | string | Due date/time |
|
||||
| ↳ `created_at` | string | Creation timestamp |
|
||||
@@ -294,6 +309,9 @@ Get detailed information about a specific action from incident.io.
|
||||
| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) |
|
||||
| ↳ `issue_name` | string | Issue identifier |
|
||||
| ↳ `issue_permalink` | string | URL to the external issue |
|
||||
| ↳ `provider` | string | Issue tracking provider \(e.g., Jira, Linear\) |
|
||||
| ↳ `issue_name` | string | Issue identifier |
|
||||
| ↳ `issue_permalink` | string | URL to the external issue |
|
||||
|
||||
### `incidentio_follow_ups_list`
|
||||
|
||||
@@ -312,19 +330,22 @@ List follow-ups from incident.io. Optionally filter by incident ID.
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `follow_ups` | array | List of follow-ups |
|
||||
| ↳ `id` | string | Follow-up ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `title` | string | Follow-up title |
|
||||
| ↳ `description` | string | Follow-up description |
|
||||
| ↳ `description` | string | Priority description |
|
||||
| ↳ `assignee` | object | Assigned user |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `status` | string | Follow-up status |
|
||||
| ↳ `priority` | object | Follow-up priority |
|
||||
| ↳ `id` | string | Priority ID |
|
||||
| ↳ `name` | string | Priority name |
|
||||
| ↳ `description` | string | Priority description |
|
||||
| ↳ `rank` | number | Priority rank |
|
||||
| ↳ `rank` | number | Priority rank |
|
||||
| ↳ `created_at` | string | Creation timestamp |
|
||||
| ↳ `updated_at` | string | Last update timestamp |
|
||||
| ↳ `incident_id` | string | Associated incident ID |
|
||||
@@ -338,6 +359,9 @@ List follow-ups from incident.io. Optionally filter by incident ID.
|
||||
| ↳ `provider` | string | External provider name |
|
||||
| ↳ `issue_name` | string | External issue name or ID |
|
||||
| ↳ `issue_permalink` | string | Permalink to external issue |
|
||||
| ↳ `provider` | string | External provider name |
|
||||
| ↳ `issue_name` | string | External issue name or ID |
|
||||
| ↳ `issue_permalink` | string | Permalink to external issue |
|
||||
|
||||
### `incidentio_follow_ups_show`
|
||||
|
||||
@@ -355,19 +379,22 @@ Get detailed information about a specific follow-up from incident.io.
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `follow_up` | object | Follow-up details |
|
||||
| ↳ `id` | string | Follow-up ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `title` | string | Follow-up title |
|
||||
| ↳ `description` | string | Follow-up description |
|
||||
| ↳ `description` | string | Priority description |
|
||||
| ↳ `assignee` | object | Assigned user |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `status` | string | Follow-up status |
|
||||
| ↳ `priority` | object | Follow-up priority |
|
||||
| ↳ `id` | string | Priority ID |
|
||||
| ↳ `name` | string | Priority name |
|
||||
| ↳ `description` | string | Priority description |
|
||||
| ↳ `rank` | number | Priority rank |
|
||||
| ↳ `rank` | number | Priority rank |
|
||||
| ↳ `created_at` | string | Creation timestamp |
|
||||
| ↳ `updated_at` | string | Last update timestamp |
|
||||
| ↳ `incident_id` | string | Associated incident ID |
|
||||
@@ -381,6 +408,9 @@ Get detailed information about a specific follow-up from incident.io.
|
||||
| ↳ `provider` | string | External provider name |
|
||||
| ↳ `issue_name` | string | External issue name or ID |
|
||||
| ↳ `issue_permalink` | string | Permalink to external issue |
|
||||
| ↳ `provider` | string | External provider name |
|
||||
| ↳ `issue_name` | string | External issue name or ID |
|
||||
| ↳ `issue_permalink` | string | Permalink to external issue |
|
||||
|
||||
### `incidentio_users_list`
|
||||
|
||||
@@ -1059,21 +1089,25 @@ List all updates for a specific incident in incident.io
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `incident_updates` | array | List of incident updates |
|
||||
| ↳ `id` | string | The update ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `incident_id` | string | The incident ID |
|
||||
| ↳ `message` | string | The update message |
|
||||
| ↳ `new_severity` | object | New severity if changed |
|
||||
| ↳ `id` | string | Severity ID |
|
||||
| ↳ `name` | string | Severity name |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `rank` | number | Severity rank |
|
||||
| ↳ `new_status` | object | New status if changed |
|
||||
| ↳ `id` | string | Status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `category` | string | Status category |
|
||||
| ↳ `updater` | object | User who created the update |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `created_at` | string | When the update was created |
|
||||
| ↳ `updated_at` | string | When the update was last modified |
|
||||
| `pagination_meta` | object | Pagination information |
|
||||
@@ -1100,12 +1134,14 @@ List all entries for a specific schedule in incident.io
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `schedule_entries` | array | List of schedule entries |
|
||||
| ↳ `id` | string | The entry ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `schedule_id` | string | The schedule ID |
|
||||
| ↳ `user` | object | User assigned to this entry |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `start_at` | string | When the entry starts |
|
||||
| ↳ `end_at` | string | When the entry ends |
|
||||
| ↳ `layer_id` | string | The schedule layer ID |
|
||||
@@ -1138,13 +1174,15 @@ Create a new schedule override in incident.io
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `override` | object | The created schedule override |
|
||||
| ↳ `id` | string | The override ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `rotation_id` | string | The rotation ID |
|
||||
| ↳ `schedule_id` | string | The schedule ID |
|
||||
| ↳ `user` | object | User assigned to this override |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `start_at` | string | When the override starts |
|
||||
| ↳ `end_at` | string | When the override ends |
|
||||
| ↳ `created_at` | string | When the override was created |
|
||||
@@ -1168,7 +1206,7 @@ Create a new escalation path in incident.io
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `escalation_path` | object | The created escalation path |
|
||||
| ↳ `id` | string | The escalation path ID |
|
||||
| ↳ `id` | string | Target ID |
|
||||
| ↳ `name` | string | The escalation path name |
|
||||
| ↳ `path` | array | Array of escalation levels |
|
||||
| ↳ `targets` | array | Targets for this level |
|
||||
@@ -1177,11 +1215,30 @@ Create a new escalation path in incident.io
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `id` | string | Target ID |
|
||||
| ↳ `type` | string | Target type |
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
|
||||
| ↳ `targets` | array | Targets for this level |
|
||||
| ↳ `id` | string | Target ID |
|
||||
| ↳ `type` | string | Target type |
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `type` | string | Target type |
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
|
||||
| ↳ `working_hours` | array | Working hours configuration |
|
||||
| ↳ `weekday` | string | Day of week |
|
||||
| ↳ `start_time` | string | Start time |
|
||||
| ↳ `end_time` | string | End time |
|
||||
| ↳ `weekday` | string | Day of week |
|
||||
| ↳ `start_time` | string | Start time |
|
||||
| ↳ `end_time` | string | End time |
|
||||
| ↳ `created_at` | string | When the path was created |
|
||||
| ↳ `updated_at` | string | When the path was last updated |
|
||||
|
||||
@@ -1201,7 +1258,7 @@ Get details of a specific escalation path in incident.io
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `escalation_path` | object | The escalation path details |
|
||||
| ↳ `id` | string | The escalation path ID |
|
||||
| ↳ `id` | string | Target ID |
|
||||
| ↳ `name` | string | The escalation path name |
|
||||
| ↳ `path` | array | Array of escalation levels |
|
||||
| ↳ `targets` | array | Targets for this level |
|
||||
@@ -1210,11 +1267,30 @@ Get details of a specific escalation path in incident.io
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `id` | string | Target ID |
|
||||
| ↳ `type` | string | Target type |
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
|
||||
| ↳ `targets` | array | Targets for this level |
|
||||
| ↳ `id` | string | Target ID |
|
||||
| ↳ `type` | string | Target type |
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `type` | string | Target type |
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
|
||||
| ↳ `working_hours` | array | Working hours configuration |
|
||||
| ↳ `weekday` | string | Day of week |
|
||||
| ↳ `start_time` | string | Start time |
|
||||
| ↳ `end_time` | string | End time |
|
||||
| ↳ `weekday` | string | Day of week |
|
||||
| ↳ `start_time` | string | Start time |
|
||||
| ↳ `end_time` | string | End time |
|
||||
| ↳ `created_at` | string | When the path was created |
|
||||
| ↳ `updated_at` | string | When the path was last updated |
|
||||
|
||||
@@ -1237,7 +1313,7 @@ Update an existing escalation path in incident.io
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `escalation_path` | object | The updated escalation path |
|
||||
| ↳ `id` | string | The escalation path ID |
|
||||
| ↳ `id` | string | Target ID |
|
||||
| ↳ `name` | string | The escalation path name |
|
||||
| ↳ `path` | array | Array of escalation levels |
|
||||
| ↳ `targets` | array | Targets for this level |
|
||||
@@ -1246,11 +1322,30 @@ Update an existing escalation path in incident.io
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `id` | string | Target ID |
|
||||
| ↳ `type` | string | Target type |
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
|
||||
| ↳ `targets` | array | Targets for this level |
|
||||
| ↳ `id` | string | Target ID |
|
||||
| ↳ `type` | string | Target type |
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `type` | string | Target type |
|
||||
| ↳ `schedule_id` | string | Schedule ID if type is schedule |
|
||||
| ↳ `user_id` | string | User ID if type is user |
|
||||
| ↳ `urgency` | string | Urgency level |
|
||||
| ↳ `time_to_ack_seconds` | number | Time to acknowledge in seconds |
|
||||
| ↳ `working_hours` | array | Working hours configuration |
|
||||
| ↳ `weekday` | string | Day of week |
|
||||
| ↳ `start_time` | string | Start time |
|
||||
| ↳ `end_time` | string | End time |
|
||||
| ↳ `weekday` | string | Day of week |
|
||||
| ↳ `start_time` | string | Start time |
|
||||
| ↳ `end_time` | string | End time |
|
||||
| ↳ `created_at` | string | When the path was created |
|
||||
| ↳ `updated_at` | string | When the path was last updated |
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ Create a new contact in Intercom with email, external_id, or role. Returns API-a
|
||||
| --------- | ---- | ----------- |
|
||||
| `contact` | object | Created contact object |
|
||||
| ↳ `id` | string | Unique identifier for the contact |
|
||||
| ↳ `type` | string | Object type \(contact\) |
|
||||
| ↳ `type` | string | List type |
|
||||
| ↳ `role` | string | Role of the contact \(user or lead\) |
|
||||
| ↳ `email` | string | Email address of the contact |
|
||||
| ↳ `phone` | string | Phone number of the contact |
|
||||
@@ -82,6 +82,10 @@ Create a new contact in Intercom with email, external_id, or role. Returns API-a
|
||||
| ↳ `data` | array | Array of tag objects |
|
||||
| ↳ `has_more` | boolean | Whether there are more tags |
|
||||
| ↳ `total_count` | number | Total number of tags |
|
||||
| ↳ `url` | string | URL to fetch companies |
|
||||
| ↳ `data` | array | Array of social profile objects |
|
||||
| ↳ `has_more` | boolean | Whether there are more companies |
|
||||
| ↳ `total_count` | number | Total number of companies |
|
||||
| ↳ `notes` | object | Notes associated with the contact |
|
||||
| ↳ `type` | string | List type |
|
||||
| ↳ `url` | string | URL to fetch notes |
|
||||
@@ -101,6 +105,11 @@ Create a new contact in Intercom with email, external_id, or role. Returns API-a
|
||||
| ↳ `country` | string | Country |
|
||||
| ↳ `country_code` | string | Country code |
|
||||
| ↳ `continent_code` | string | Continent code |
|
||||
| ↳ `city` | string | City |
|
||||
| ↳ `region` | string | Region/State |
|
||||
| ↳ `country` | string | Country |
|
||||
| ↳ `country_code` | string | Country code |
|
||||
| ↳ `continent_code` | string | Continent code |
|
||||
| ↳ `social_profiles` | object | Social profiles of the contact |
|
||||
| ↳ `type` | string | List type |
|
||||
| ↳ `data` | array | Array of social profile objects |
|
||||
@@ -314,7 +323,7 @@ Create or update a company in Intercom
|
||||
| --------- | ---- | ----------- |
|
||||
| `company` | object | Created or updated company object |
|
||||
| ↳ `id` | string | Unique identifier for the company |
|
||||
| ↳ `type` | string | Object type \(company\) |
|
||||
| ↳ `type` | string | Segment list type |
|
||||
| ↳ `app_id` | string | Intercom app ID |
|
||||
| ↳ `company_id` | string | Your unique identifier for the company |
|
||||
| ↳ `name` | string | Name of the company |
|
||||
@@ -329,12 +338,8 @@ Create or update a company in Intercom
|
||||
| ↳ `updated_at` | number | Unix timestamp when company was last updated |
|
||||
| ↳ `remote_created_at` | number | Unix timestamp when company was created by you |
|
||||
| ↳ `custom_attributes` | object | Custom attributes set on the company |
|
||||
| ↳ `tags` | object | Tags associated with the company |
|
||||
| ↳ `type` | string | Tag list type |
|
||||
| ↳ `tags` | array | Array of tag objects |
|
||||
| ↳ `segments` | object | Segments the company belongs to |
|
||||
| ↳ `type` | string | Segment list type |
|
||||
| ↳ `segments` | array | Array of segment objects |
|
||||
| ↳ `tags` | array | Array of tag objects |
|
||||
| ↳ `segments` | array | Array of segment objects |
|
||||
| `companyId` | string | ID of the created/updated company |
|
||||
|
||||
### `intercom_get_company`
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
"calendly",
|
||||
"circleback",
|
||||
"clay",
|
||||
"clerk",
|
||||
"confluence",
|
||||
"cursor",
|
||||
"datadog",
|
||||
|
||||
@@ -64,10 +64,19 @@ Parse PDF documents using Mistral OCR API
|
||||
| ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels |
|
||||
| ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels |
|
||||
| ↳ `image_base64` | string | Base64-encoded image data \(when include_image_base64=true\) |
|
||||
| ↳ `id` | string | Image identifier \(e.g., img-0.jpeg\) |
|
||||
| ↳ `top_left_x` | number | Top-left X coordinate in pixels |
|
||||
| ↳ `top_left_y` | number | Top-left Y coordinate in pixels |
|
||||
| ↳ `bottom_right_x` | number | Bottom-right X coordinate in pixels |
|
||||
| ↳ `bottom_right_y` | number | Bottom-right Y coordinate in pixels |
|
||||
| ↳ `image_base64` | string | Base64-encoded image data \(when include_image_base64=true\) |
|
||||
| ↳ `dimensions` | object | Page dimensions |
|
||||
| ↳ `dpi` | number | Dots per inch |
|
||||
| ↳ `height` | number | Page height in pixels |
|
||||
| ↳ `width` | number | Page width in pixels |
|
||||
| ↳ `dpi` | number | Dots per inch |
|
||||
| ↳ `height` | number | Page height in pixels |
|
||||
| ↳ `width` | number | Page width in pixels |
|
||||
| ↳ `tables` | array | Extracted tables as HTML/markdown \(when table_format is set\). Referenced via placeholders like \[tbl-0.html\] |
|
||||
| ↳ `hyperlinks` | array | Array of URL strings detected in the page \(e.g., \[ |
|
||||
| ↳ `header` | string | Page header content \(when extract_header=true\) |
|
||||
|
||||
@@ -59,5 +59,7 @@ Generate embeddings from text using OpenAI
|
||||
| ↳ `usage` | object | Token usage information |
|
||||
| ↳ `prompt_tokens` | number | Number of tokens in the prompt |
|
||||
| ↳ `total_tokens` | number | Total number of tokens used |
|
||||
| ↳ `prompt_tokens` | number | Number of tokens in the prompt |
|
||||
| ↳ `total_tokens` | number | Total number of tokens used |
|
||||
|
||||
|
||||
|
||||
@@ -112,6 +112,9 @@ Conduct comprehensive deep research across the web using Parallel AI. Synthesize
|
||||
| ↳ `url` | string | Source URL |
|
||||
| ↳ `title` | string | Source title |
|
||||
| ↳ `excerpts` | array | Relevant excerpts from the source |
|
||||
| ↳ `url` | string | Source URL |
|
||||
| ↳ `title` | string | Source title |
|
||||
| ↳ `excerpts` | array | Relevant excerpts from the source |
|
||||
| ↳ `confidence` | string | Confidence level indicator |
|
||||
|
||||
|
||||
|
||||
@@ -323,6 +323,8 @@ Retrieve the order book summary for a specific token
|
||||
| ↳ `bids` | array | Bid orders |
|
||||
| ↳ `price` | string | Bid price |
|
||||
| ↳ `size` | string | Bid size |
|
||||
| ↳ `price` | string | Ask price |
|
||||
| ↳ `size` | string | Ask size |
|
||||
| ↳ `asks` | array | Ask orders |
|
||||
| ↳ `price` | string | Ask price |
|
||||
| ↳ `size` | string | Ask size |
|
||||
@@ -635,5 +637,15 @@ Retrieve top holders of a specific market token
|
||||
| ↳ `name` | string | Holder display name |
|
||||
| ↳ `profileImage` | string | Profile image URL |
|
||||
| ↳ `profileImageOptimized` | string | Optimized profile image URL |
|
||||
| ↳ `proxyWallet` | string | Holder wallet address |
|
||||
| ↳ `bio` | string | Holder bio |
|
||||
| ↳ `asset` | string | Asset ID |
|
||||
| ↳ `pseudonym` | string | Holder pseudonym |
|
||||
| ↳ `amount` | number | Amount held |
|
||||
| ↳ `displayUsernamePublic` | boolean | Whether username is publicly displayed |
|
||||
| ↳ `outcomeIndex` | number | Outcome index |
|
||||
| ↳ `name` | string | Holder display name |
|
||||
| ↳ `profileImage` | string | Profile image URL |
|
||||
| ↳ `profileImageOptimized` | string | Optimized profile image URL |
|
||||
|
||||
|
||||
|
||||
@@ -58,9 +58,14 @@ Retrieve accounts from Salesforce CRM
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of accounts returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `totalReturned` | number | Number of accounts returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `success` | boolean | Salesforce operation success |
|
||||
|
||||
### `salesforce_create_account`
|
||||
@@ -179,9 +184,14 @@ Get contact(s) from Salesforce - single contact if ID provided, or list if not
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of contacts returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `totalReturned` | number | Number of contacts returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `singleContact` | boolean | Whether single contact was returned |
|
||||
| ↳ `success` | boolean | Salesforce operation success |
|
||||
|
||||
@@ -301,9 +311,14 @@ Get lead(s) from Salesforce
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of leads returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `totalReturned` | number | Number of leads returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `singleLead` | boolean | Whether single lead was returned |
|
||||
| ↳ `success` | boolean | Operation success status |
|
||||
|
||||
@@ -415,9 +430,14 @@ Get opportunity(ies) from Salesforce
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of opportunities returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `totalReturned` | number | Number of opportunities returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `success` | boolean | Operation success status |
|
||||
|
||||
### `salesforce_create_opportunity`
|
||||
@@ -524,9 +544,14 @@ Get case(s) from Salesforce
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of cases returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `totalReturned` | number | Number of cases returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `success` | boolean | Operation success status |
|
||||
|
||||
### `salesforce_create_case`
|
||||
@@ -630,9 +655,14 @@ Get task(s) from Salesforce
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `nextRecordsUrl` | string | URL for next page of results |
|
||||
| ↳ `totalSize` | number | Total number of records |
|
||||
| ↳ `done` | boolean | Whether all records returned |
|
||||
| ↳ `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of tasks returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `totalReturned` | number | Number of tasks returned |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `success` | boolean | Operation success status |
|
||||
|
||||
### `salesforce_create_task`
|
||||
@@ -908,6 +938,8 @@ Execute a custom SOQL query to retrieve data from Salesforce
|
||||
| ↳ `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `success` | boolean | Salesforce operation success |
|
||||
|
||||
### `salesforce_query_more`
|
||||
@@ -935,6 +967,8 @@ Retrieve additional query results using the nextRecordsUrl from a previous query
|
||||
| ↳ `metadata` | object | Response metadata |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `totalReturned` | number | Number of records returned in this response |
|
||||
| ↳ `hasMore` | boolean | Whether more records exist |
|
||||
| ↳ `success` | boolean | Salesforce operation success |
|
||||
|
||||
### `salesforce_describe_object`
|
||||
|
||||
@@ -68,7 +68,7 @@ List issues from Sentry for a specific organization and optionally a specific pr
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issues` | array | List of Sentry issues |
|
||||
| ↳ `id` | string | Unique issue ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `shortId` | string | Short issue identifier |
|
||||
| ↳ `title` | string | Issue title |
|
||||
| ↳ `culprit` | string | Function or location that caused the issue |
|
||||
@@ -78,22 +78,27 @@ List issues from Sentry for a specific organization and optionally a specific pr
|
||||
| ↳ `status` | string | Current issue status |
|
||||
| ↳ `statusDetails` | object | Additional details about the status |
|
||||
| ↳ `isPublic` | boolean | Whether the issue is publicly visible |
|
||||
| ↳ `platform` | string | Platform where the issue occurred |
|
||||
| ↳ `platform` | string | Project platform |
|
||||
| ↳ `project` | object | Project information |
|
||||
| ↳ `id` | string | Project ID |
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `slug` | string | Project slug |
|
||||
| ↳ `platform` | string | Project platform |
|
||||
| ↳ `type` | string | Issue type |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `slug` | string | Project slug |
|
||||
| ↳ `type` | string | Type of error \(e.g., TypeError\) |
|
||||
| ↳ `metadata` | object | Error metadata |
|
||||
| ↳ `type` | string | Type of error \(e.g., TypeError\) |
|
||||
| ↳ `value` | string | Error message or value |
|
||||
| ↳ `function` | string | Function where the error occurred |
|
||||
| ↳ `value` | string | Error message or value |
|
||||
| ↳ `function` | string | Function where the error occurred |
|
||||
| ↳ `numComments` | number | Number of comments on the issue |
|
||||
| ↳ `assignedTo` | object | User assigned to the issue |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `isBookmarked` | boolean | Whether the issue is bookmarked |
|
||||
| ↳ `isSubscribed` | boolean | Whether subscribed to updates |
|
||||
| ↳ `hasSeen` | boolean | Whether the user has seen this issue |
|
||||
@@ -125,7 +130,7 @@ Retrieve detailed information about a specific Sentry issue by its ID. Returns c
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issue` | object | Detailed information about the Sentry issue |
|
||||
| ↳ `id` | string | Unique issue ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `shortId` | string | Short issue identifier |
|
||||
| ↳ `title` | string | Issue title |
|
||||
| ↳ `culprit` | string | Function or location that caused the issue |
|
||||
@@ -135,22 +140,27 @@ Retrieve detailed information about a specific Sentry issue by its ID. Returns c
|
||||
| ↳ `status` | string | Current issue status |
|
||||
| ↳ `statusDetails` | object | Additional details about the status |
|
||||
| ↳ `isPublic` | boolean | Whether the issue is publicly visible |
|
||||
| ↳ `platform` | string | Platform where the issue occurred |
|
||||
| ↳ `platform` | string | Project platform |
|
||||
| ↳ `project` | object | Project information |
|
||||
| ↳ `id` | string | Project ID |
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `slug` | string | Project slug |
|
||||
| ↳ `platform` | string | Project platform |
|
||||
| ↳ `type` | string | Issue type |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `slug` | string | Project slug |
|
||||
| ↳ `type` | string | Type of error \(e.g., TypeError, ValueError\) |
|
||||
| ↳ `metadata` | object | Error metadata |
|
||||
| ↳ `type` | string | Type of error \(e.g., TypeError, ValueError\) |
|
||||
| ↳ `value` | string | Error message or value |
|
||||
| ↳ `function` | string | Function where the error occurred |
|
||||
| ↳ `value` | string | Error message or value |
|
||||
| ↳ `function` | string | Function where the error occurred |
|
||||
| ↳ `numComments` | number | Number of comments on the issue |
|
||||
| ↳ `assignedTo` | object | User assigned to the issue \(if any\) |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `isBookmarked` | boolean | Whether the issue is bookmarked |
|
||||
| ↳ `isSubscribed` | boolean | Whether the user is subscribed to updates |
|
||||
| ↳ `hasSeen` | boolean | Whether the user has seen this issue |
|
||||
@@ -184,7 +194,7 @@ Update a Sentry issue by changing its status, assignment, bookmark state, or oth
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `issue` | object | The updated Sentry issue |
|
||||
| ↳ `id` | string | Unique issue ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `shortId` | string | Short issue identifier |
|
||||
| ↳ `title` | string | Issue title |
|
||||
| ↳ `status` | string | Updated issue status |
|
||||
@@ -192,6 +202,8 @@ Update a Sentry issue by changing its status, assignment, bookmark state, or oth
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `isBookmarked` | boolean | Whether the issue is bookmarked |
|
||||
| ↳ `isSubscribed` | boolean | Whether the user is subscribed to updates |
|
||||
| ↳ `isPublic` | boolean | Whether the issue is publicly visible |
|
||||
@@ -215,9 +227,9 @@ List all projects in a Sentry organization. Returns project details including na
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projects` | array | List of Sentry projects |
|
||||
| ↳ `id` | string | Unique project ID |
|
||||
| ↳ `slug` | string | URL-friendly project identifier |
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `slug` | string | Team slug |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `platform` | string | Platform/language \(e.g., javascript, python\) |
|
||||
| ↳ `dateCreated` | string | When the project was created \(ISO timestamp\) |
|
||||
| ↳ `isBookmarked` | boolean | Whether the project is bookmarked |
|
||||
@@ -254,9 +266,9 @@ Retrieve detailed information about a specific Sentry project by its slug. Retur
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `project` | object | Detailed information about the Sentry project |
|
||||
| ↳ `id` | string | Unique project ID |
|
||||
| ↳ `slug` | string | URL-friendly project identifier |
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `slug` | string | Team slug |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `platform` | string | Platform/language \(e.g., javascript, python\) |
|
||||
| ↳ `dateCreated` | string | When the project was created \(ISO timestamp\) |
|
||||
| ↳ `isBookmarked` | boolean | Whether the project is bookmarked |
|
||||
@@ -309,9 +321,9 @@ Create a new Sentry project in an organization. Requires a team to associate the
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `project` | object | The newly created Sentry project |
|
||||
| ↳ `id` | string | Unique project ID |
|
||||
| ↳ `slug` | string | URL-friendly project identifier |
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `slug` | string | Team slug |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `platform` | string | Platform/language |
|
||||
| ↳ `dateCreated` | string | When the project was created \(ISO timestamp\) |
|
||||
| ↳ `isBookmarked` | boolean | Whether the project is bookmarked |
|
||||
@@ -358,9 +370,9 @@ Update a Sentry project by changing its name, slug, platform, or other settings.
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `project` | object | The updated Sentry project |
|
||||
| ↳ `id` | string | Unique project ID |
|
||||
| ↳ `slug` | string | URL-friendly project identifier |
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `slug` | string | Team slug |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `platform` | string | Platform/language |
|
||||
| ↳ `isBookmarked` | boolean | Whether the project is bookmarked |
|
||||
| ↳ `organization` | object | Organization information |
|
||||
@@ -394,7 +406,7 @@ List events from a Sentry project. Can be filtered by issue ID, query, or time p
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `events` | array | List of Sentry events |
|
||||
| ↳ `id` | string | Unique event ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `eventID` | string | Event identifier |
|
||||
| ↳ `projectID` | string | Project ID |
|
||||
| ↳ `groupID` | string | Issue group ID |
|
||||
@@ -410,16 +422,23 @@ List events from a Sentry project. Can be filtered by issue ID, query, or time p
|
||||
| ↳ `username` | string | Username |
|
||||
| ↳ `ipAddress` | string | IP address |
|
||||
| ↳ `name` | string | User display name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `username` | string | Username |
|
||||
| ↳ `ipAddress` | string | IP address |
|
||||
| ↳ `name` | string | SDK name |
|
||||
| ↳ `tags` | array | Tags associated with the event |
|
||||
| ↳ `key` | string | Tag key |
|
||||
| ↳ `value` | string | Tag value |
|
||||
| ↳ `key` | string | Tag key |
|
||||
| ↳ `value` | string | Error message or value |
|
||||
| ↳ `contexts` | object | Additional context data \(device, OS, etc.\) |
|
||||
| ↳ `platform` | string | Platform where the event occurred |
|
||||
| ↳ `type` | string | Event type |
|
||||
| ↳ `type` | string | Type of error \(e.g., TypeError\) |
|
||||
| ↳ `metadata` | object | Error metadata |
|
||||
| ↳ `type` | string | Type of error \(e.g., TypeError\) |
|
||||
| ↳ `value` | string | Error message or value |
|
||||
| ↳ `function` | string | Function where the error occurred |
|
||||
| ↳ `function` | string | Function where the error occurred |
|
||||
| ↳ `entries` | array | Event entries \(exception, breadcrumbs, etc.\) |
|
||||
| ↳ `errors` | array | Processing errors |
|
||||
| ↳ `dist` | string | Distribution identifier |
|
||||
@@ -427,6 +446,7 @@ List events from a Sentry project. Can be filtered by issue ID, query, or time p
|
||||
| ↳ `sdk` | object | SDK information |
|
||||
| ↳ `name` | string | SDK name |
|
||||
| ↳ `version` | string | SDK version |
|
||||
| ↳ `version` | string | SDK version |
|
||||
| `metadata` | object | Pagination metadata |
|
||||
| ↳ `nextCursor` | string | Cursor for the next page of results \(if available\) |
|
||||
| ↳ `hasMore` | boolean | Whether there are more results available |
|
||||
@@ -449,7 +469,7 @@ Retrieve detailed information about a specific Sentry event by its ID. Returns c
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `event` | object | Detailed information about the Sentry event |
|
||||
| ↳ `id` | string | Unique event ID |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `eventID` | string | Event identifier |
|
||||
| ↳ `projectID` | string | Project ID |
|
||||
| ↳ `groupID` | string | Issue group ID this event belongs to |
|
||||
@@ -465,16 +485,23 @@ Retrieve detailed information about a specific Sentry event by its ID. Returns c
|
||||
| ↳ `username` | string | Username |
|
||||
| ↳ `ipAddress` | string | IP address |
|
||||
| ↳ `name` | string | User display name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `username` | string | Username |
|
||||
| ↳ `ipAddress` | string | IP address |
|
||||
| ↳ `name` | string | SDK name |
|
||||
| ↳ `tags` | array | Tags associated with the event |
|
||||
| ↳ `key` | string | Tag key |
|
||||
| ↳ `value` | string | Tag value |
|
||||
| ↳ `key` | string | Tag key |
|
||||
| ↳ `value` | string | Error message or value |
|
||||
| ↳ `contexts` | object | Additional context data \(device, OS, browser, etc.\) |
|
||||
| ↳ `platform` | string | Platform where the event occurred |
|
||||
| ↳ `type` | string | Event type \(error, transaction, etc.\) |
|
||||
| ↳ `type` | string | Type of error \(e.g., TypeError, ValueError\) |
|
||||
| ↳ `metadata` | object | Error metadata |
|
||||
| ↳ `type` | string | Type of error \(e.g., TypeError, ValueError\) |
|
||||
| ↳ `value` | string | Error message or value |
|
||||
| ↳ `function` | string | Function where the error occurred |
|
||||
| ↳ `function` | string | Function where the error occurred |
|
||||
| ↳ `entries` | array | Event entries including exception, breadcrumbs, and request data |
|
||||
| ↳ `errors` | array | Processing errors that occurred |
|
||||
| ↳ `dist` | string | Distribution identifier |
|
||||
@@ -482,6 +509,7 @@ Retrieve detailed information about a specific Sentry event by its ID. Returns c
|
||||
| ↳ `sdk` | object | SDK information |
|
||||
| ↳ `name` | string | SDK name |
|
||||
| ↳ `version` | string | SDK version |
|
||||
| ↳ `version` | string | SDK version |
|
||||
|
||||
### `sentry_releases_list`
|
||||
|
||||
@@ -503,30 +531,36 @@ List releases for a Sentry organization or project. Returns release details incl
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `releases` | array | List of Sentry releases |
|
||||
| ↳ `id` | string | Unique release ID |
|
||||
| ↳ `version` | string | Release version identifier |
|
||||
| ↳ `id` | string | Project ID |
|
||||
| ↳ `version` | object | Version details |
|
||||
| ↳ `raw` | string | Raw version string |
|
||||
| ↳ `shortVersion` | string | Shortened version identifier |
|
||||
| ↳ `ref` | string | Git reference \(commit SHA, tag, or branch\) |
|
||||
| ↳ `url` | string | URL to the release \(e.g., GitHub release page\) |
|
||||
| ↳ `dateReleased` | string | When the release was deployed \(ISO timestamp\) |
|
||||
| ↳ `dateCreated` | string | When the release was created \(ISO timestamp\) |
|
||||
| ↳ `dateStarted` | string | When the release started \(ISO timestamp\) |
|
||||
| ↳ `dateCreated` | string | Commit timestamp |
|
||||
| ↳ `dateStarted` | string | Deploy start timestamp |
|
||||
| ↳ `newGroups` | number | Number of new issues introduced in this release |
|
||||
| ↳ `owner` | object | Owner of the release |
|
||||
| ↳ `id` | string | User ID |
|
||||
| ↳ `name` | string | User name |
|
||||
| ↳ `email` | string | User email |
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `email` | string | Author email |
|
||||
| ↳ `commitCount` | number | Number of commits in this release |
|
||||
| ↳ `deployCount` | number | Number of deploys for this release |
|
||||
| ↳ `lastCommit` | object | Last commit in the release |
|
||||
| ↳ `id` | string | Commit SHA |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `dateCreated` | string | Commit timestamp |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `lastDeploy` | object | Last deploy of the release |
|
||||
| ↳ `id` | string | Deploy ID |
|
||||
| ↳ `environment` | string | Deploy environment |
|
||||
| ↳ `dateStarted` | string | Deploy start timestamp |
|
||||
| ↳ `dateFinished` | string | Deploy finish timestamp |
|
||||
| ↳ `environment` | string | Deploy environment |
|
||||
| ↳ `dateFinished` | string | Deploy finish timestamp |
|
||||
| ↳ `authors` | array | Authors of commits in the release |
|
||||
| ↳ `id` | string | Author ID |
|
||||
| ↳ `name` | string | Author name |
|
||||
@@ -536,12 +570,18 @@ List releases for a Sentry organization or project. Returns release details incl
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `slug` | string | Project slug |
|
||||
| ↳ `platform` | string | Project platform |
|
||||
| ↳ `slug` | string | Project slug |
|
||||
| ↳ `platform` | string | Project platform |
|
||||
| ↳ `firstEvent` | string | First event timestamp |
|
||||
| ↳ `lastEvent` | string | Last event timestamp |
|
||||
| ↳ `versionInfo` | object | Version metadata |
|
||||
| ↳ `buildHash` | string | Build hash |
|
||||
| ↳ `version` | object | Version details |
|
||||
| ↳ `raw` | string | Raw version string |
|
||||
| ↳ `raw` | string | Raw version string |
|
||||
| ↳ `package` | string | Package name |
|
||||
| ↳ `buildHash` | string | Build hash |
|
||||
| ↳ `raw` | string | Raw version string |
|
||||
| ↳ `package` | string | Package name |
|
||||
| `metadata` | object | Pagination metadata |
|
||||
| ↳ `nextCursor` | string | Cursor for the next page of results \(if available\) |
|
||||
@@ -569,14 +609,15 @@ Create a new release in Sentry. A release is a version of your code deployed to
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `release` | object | The newly created Sentry release |
|
||||
| ↳ `id` | string | Unique release ID |
|
||||
| ↳ `version` | string | Release version identifier |
|
||||
| ↳ `id` | string | Project ID |
|
||||
| ↳ `version` | object | Version details |
|
||||
| ↳ `raw` | string | Raw version string |
|
||||
| ↳ `shortVersion` | string | Shortened version identifier |
|
||||
| ↳ `ref` | string | Git reference \(commit SHA, tag, or branch\) |
|
||||
| ↳ `url` | string | URL to the release |
|
||||
| ↳ `dateReleased` | string | When the release was deployed \(ISO timestamp\) |
|
||||
| ↳ `dateCreated` | string | When the release was created \(ISO timestamp\) |
|
||||
| ↳ `dateStarted` | string | When the release started \(ISO timestamp\) |
|
||||
| ↳ `dateCreated` | string | Commit timestamp |
|
||||
| ↳ `dateStarted` | string | Deploy start timestamp |
|
||||
| ↳ `newGroups` | number | Number of new issues introduced |
|
||||
| ↳ `commitCount` | number | Number of commits in this release |
|
||||
| ↳ `deployCount` | number | Number of deploys for this release |
|
||||
@@ -584,15 +625,20 @@ Create a new release in Sentry. A release is a version of your code deployed to
|
||||
| ↳ `id` | string | Owner ID |
|
||||
| ↳ `name` | string | Owner name |
|
||||
| ↳ `email` | string | Owner email |
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `email` | string | Author email |
|
||||
| ↳ `lastCommit` | object | Last commit in the release |
|
||||
| ↳ `id` | string | Commit SHA |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `dateCreated` | string | Commit timestamp |
|
||||
| ↳ `message` | string | Commit message |
|
||||
| ↳ `lastDeploy` | object | Last deploy of the release |
|
||||
| ↳ `id` | string | Deploy ID |
|
||||
| ↳ `environment` | string | Deploy environment |
|
||||
| ↳ `dateStarted` | string | Deploy start timestamp |
|
||||
| ↳ `dateFinished` | string | Deploy finish timestamp |
|
||||
| ↳ `environment` | string | Deploy environment |
|
||||
| ↳ `dateFinished` | string | Deploy finish timestamp |
|
||||
| ↳ `authors` | array | Authors of commits in the release |
|
||||
| ↳ `id` | string | Author ID |
|
||||
| ↳ `name` | string | Author name |
|
||||
@@ -602,13 +648,19 @@ Create a new release in Sentry. A release is a version of your code deployed to
|
||||
| ↳ `name` | string | Project name |
|
||||
| ↳ `slug` | string | Project slug |
|
||||
| ↳ `platform` | string | Project platform |
|
||||
| ↳ `slug` | string | Project slug |
|
||||
| ↳ `platform` | string | Project platform |
|
||||
| ↳ `firstEvent` | string | First event timestamp |
|
||||
| ↳ `lastEvent` | string | Last event timestamp |
|
||||
| ↳ `versionInfo` | object | Version metadata |
|
||||
| ↳ `buildHash` | string | Build hash |
|
||||
| ↳ `version` | object | Version details |
|
||||
| ↳ `raw` | string | Raw version string |
|
||||
| ↳ `raw` | string | Raw version string |
|
||||
| ↳ `package` | string | Package name |
|
||||
| ↳ `buildHash` | string | Build hash |
|
||||
| ↳ `raw` | string | Raw version string |
|
||||
| ↳ `package` | string | Package name |
|
||||
|
||||
### `sentry_releases_deploy`
|
||||
|
||||
|
||||
@@ -95,7 +95,13 @@ Read a specific page from a SharePoint site
|
||||
| ↳ `pageLayout` | string | The layout type of the page |
|
||||
| ↳ `createdDateTime` | string | When the page was created |
|
||||
| ↳ `lastModifiedDateTime` | string | When the page was last modified |
|
||||
| ↳ `content` | object | Extracted text content from the page |
|
||||
| ↳ `id` | string | The unique ID of the page |
|
||||
| ↳ `name` | string | The name of the page |
|
||||
| ↳ `title` | string | The title of the page |
|
||||
| ↳ `webUrl` | string | The URL to access the page |
|
||||
| ↳ `pageLayout` | string | The layout type of the page |
|
||||
| ↳ `createdDateTime` | string | When the page was created |
|
||||
| ↳ `lastModifiedDateTime` | string | When the page was last modified |
|
||||
| ↳ `content` | string | Extracted text content from the page |
|
||||
| ↳ `canvasLayout` | object | Raw SharePoint canvas layout structure |
|
||||
| `content` | object | Content of the SharePoint page |
|
||||
@@ -129,8 +135,10 @@ List details of all SharePoint sites
|
||||
| ↳ `isPersonalSite` | boolean | Whether this is a personal site |
|
||||
| ↳ `root` | object | Server relative URL |
|
||||
| ↳ `serverRelativeUrl` | string | Server relative URL |
|
||||
| ↳ `serverRelativeUrl` | string | Server relative URL |
|
||||
| ↳ `siteCollection` | object | Site collection hostname |
|
||||
| ↳ `hostname` | string | Site collection hostname |
|
||||
| ↳ `hostname` | string | Site collection hostname |
|
||||
| `sites` | array | List of all accessible SharePoint sites |
|
||||
| ↳ `id` | string | The unique ID of the site |
|
||||
| ↳ `name` | string | The name of the site |
|
||||
@@ -185,7 +193,7 @@ Get metadata (and optionally columns/items) for a SharePoint list
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `list` | object | Information about the SharePoint list |
|
||||
| ↳ `id` | string | The unique ID of the list |
|
||||
| ↳ `id` | string | Item ID |
|
||||
| ↳ `displayName` | string | The display name of the list |
|
||||
| ↳ `name` | string | The internal name of the list |
|
||||
| ↳ `webUrl` | string | The web URL of the list |
|
||||
@@ -193,6 +201,7 @@ Get metadata (and optionally columns/items) for a SharePoint list
|
||||
| ↳ `lastModifiedDateTime` | string | When the list was last modified |
|
||||
| ↳ `list` | object | List properties \(e.g., template\) |
|
||||
| ↳ `columns` | array | List column definitions |
|
||||
| ↳ `fields` | object | Field values for the item |
|
||||
| `lists` | array | All lists in the site when no listId/title provided |
|
||||
|
||||
### `sharepoint_update_list`
|
||||
|
||||
@@ -147,9 +147,9 @@ Read the latest messages from Slack channels. Retrieve conversation history with
|
||||
| --------- | ---- | ----------- |
|
||||
| `messages` | array | Array of message objects from the channel |
|
||||
| ↳ `type` | string | Message type |
|
||||
| ↳ `ts` | string | Message timestamp |
|
||||
| ↳ `ts` | string | Edit timestamp |
|
||||
| ↳ `text` | string | Message text content |
|
||||
| ↳ `user` | string | User ID who sent the message |
|
||||
| ↳ `user` | string | User ID who edited |
|
||||
| ↳ `bot_id` | string | Bot ID if sent by a bot |
|
||||
| ↳ `username` | string | Display username |
|
||||
| ↳ `channel` | string | Channel ID |
|
||||
@@ -167,6 +167,9 @@ Read the latest messages from Slack channels. Retrieve conversation history with
|
||||
| ↳ `name` | string | Emoji name |
|
||||
| ↳ `count` | number | Number of reactions |
|
||||
| ↳ `users` | array | Array of user IDs who reacted |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `count` | number | Number of reactions |
|
||||
| ↳ `users` | array | Array of user IDs who reacted |
|
||||
| ↳ `is_starred` | boolean | Whether message is starred |
|
||||
| ↳ `pinned_to` | array | Array of channel IDs where message is pinned |
|
||||
| ↳ `files` | array | Array of files attached to message |
|
||||
@@ -177,12 +180,17 @@ Read the latest messages from Slack channels. Retrieve conversation history with
|
||||
| ↳ `url_private` | string | Private download URL |
|
||||
| ↳ `permalink` | string | Permanent link to file |
|
||||
| ↳ `mode` | string | File mode |
|
||||
| ↳ `id` | string | File ID |
|
||||
| ↳ `mimetype` | string | MIME type |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| ↳ `url_private` | string | Private download URL |
|
||||
| ↳ `permalink` | string | Permanent link to message |
|
||||
| ↳ `mode` | string | File mode |
|
||||
| ↳ `attachments` | array | Array of legacy attachments |
|
||||
| ↳ `blocks` | array | Array of Block Kit blocks |
|
||||
| ↳ `edited` | object | Edit information if message was edited |
|
||||
| ↳ `user` | string | User ID who edited |
|
||||
| ↳ `ts` | string | Edit timestamp |
|
||||
| ↳ `permalink` | string | Permanent link to message |
|
||||
|
||||
### `slack_get_message`
|
||||
|
||||
@@ -203,9 +211,9 @@ Retrieve a specific message by its timestamp. Useful for getting a thread parent
|
||||
| --------- | ---- | ----------- |
|
||||
| `message` | object | The retrieved message object |
|
||||
| ↳ `type` | string | Message type |
|
||||
| ↳ `ts` | string | Message timestamp |
|
||||
| ↳ `ts` | string | Edit timestamp |
|
||||
| ↳ `text` | string | Message text content |
|
||||
| ↳ `user` | string | User ID who sent the message |
|
||||
| ↳ `user` | string | User ID who edited |
|
||||
| ↳ `bot_id` | string | Bot ID if sent by a bot |
|
||||
| ↳ `username` | string | Display username |
|
||||
| ↳ `channel` | string | Channel ID |
|
||||
@@ -220,6 +228,9 @@ Retrieve a specific message by its timestamp. Useful for getting a thread parent
|
||||
| ↳ `name` | string | Emoji name |
|
||||
| ↳ `count` | number | Number of reactions |
|
||||
| ↳ `users` | array | User IDs who reacted |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `count` | number | Number of reactions |
|
||||
| ↳ `users` | array | User IDs who reacted |
|
||||
| ↳ `is_starred` | boolean | Whether message is starred |
|
||||
| ↳ `pinned_to` | array | Channel IDs where message is pinned |
|
||||
| ↳ `files` | array | Files attached to message |
|
||||
@@ -229,12 +240,16 @@ Retrieve a specific message by its timestamp. Useful for getting a thread parent
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| ↳ `url_private` | string | Private download URL |
|
||||
| ↳ `permalink` | string | Permanent link to file |
|
||||
| ↳ `id` | string | File ID |
|
||||
| ↳ `mimetype` | string | MIME type |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| ↳ `url_private` | string | Private download URL |
|
||||
| ↳ `permalink` | string | Permanent link to message |
|
||||
| ↳ `attachments` | array | Legacy attachments |
|
||||
| ↳ `blocks` | array | Block Kit blocks |
|
||||
| ↳ `edited` | object | Edit information if message was edited |
|
||||
| ↳ `user` | string | User ID who edited |
|
||||
| ↳ `ts` | string | Edit timestamp |
|
||||
| ↳ `permalink` | string | Permanent link to message |
|
||||
|
||||
### `slack_get_thread`
|
||||
|
||||
@@ -268,11 +283,17 @@ Retrieve an entire thread including the parent message and all replies. Useful f
|
||||
| ↳ `name` | string | Emoji name |
|
||||
| ↳ `count` | number | Number of reactions |
|
||||
| ↳ `users` | array | User IDs who reacted |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `count` | number | Number of reactions |
|
||||
| ↳ `users` | array | User IDs who reacted |
|
||||
| ↳ `files` | array | Files attached to the parent message |
|
||||
| ↳ `id` | string | File ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimetype` | string | MIME type |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| ↳ `id` | string | File ID |
|
||||
| ↳ `mimetype` | string | MIME type |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| `replies` | array | Array of reply messages in the thread \(excluding the parent\) |
|
||||
| ↳ `ts` | string | Message timestamp |
|
||||
| ↳ `text` | string | Message text content |
|
||||
|
||||
@@ -86,6 +86,9 @@ Run an autonomous web agent to complete tasks and extract structured data
|
||||
| ↳ `type` | string | Type of action performed |
|
||||
| ↳ `params` | object | Parameters used for the action |
|
||||
| ↳ `result` | object | Result of the action |
|
||||
| ↳ `type` | string | Type of action performed |
|
||||
| ↳ `params` | object | Parameters used for the action |
|
||||
| ↳ `result` | object | Result of the action |
|
||||
| `structuredOutput` | object | Extracted data matching the provided output schema |
|
||||
|
||||
|
||||
|
||||
@@ -82,11 +82,16 @@ Send messages to Telegram channels or users through the Telegram Bot API. Enable
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Chat username \(if available\) |
|
||||
| ↳ `username` | string | Chat title \(for groups and channels\) |
|
||||
| ↳ `id` | number | Bot user ID |
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Bot first name |
|
||||
| ↳ `username` | string | Bot username |
|
||||
| ↳ `chat` | object | Information about the bot that sent the message |
|
||||
| ↳ `id` | number | Bot user ID |
|
||||
| ↳ `first_name` | string | Bot first name |
|
||||
| ↳ `username` | string | Bot username |
|
||||
| ↳ `type` | string | chat type private or channel |
|
||||
| ↳ `type` | string | chat type private or channel |
|
||||
| ↳ `date` | number | Unix timestamp when message was sent |
|
||||
| ↳ `text` | string | Text content of the sent message |
|
||||
|
||||
@@ -136,11 +141,16 @@ Send photos to Telegram channels or users through the Telegram Bot API.
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Chat username \(if available\) |
|
||||
| ↳ `username` | string | Chat title \(for groups and channels\) |
|
||||
| ↳ `id` | number | Bot user ID |
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Bot first name |
|
||||
| ↳ `username` | string | Bot username |
|
||||
| ↳ `chat` | object | Information about the bot that sent the message |
|
||||
| ↳ `id` | number | Bot user ID |
|
||||
| ↳ `first_name` | string | Bot first name |
|
||||
| ↳ `username` | string | Bot username |
|
||||
| ↳ `type` | string | Chat type \(private, group, supergroup, channel\) |
|
||||
| ↳ `type` | string | Chat type \(private, group, supergroup, channel\) |
|
||||
| ↳ `date` | number | Unix timestamp when message was sent |
|
||||
| ↳ `text` | string | Text content of the sent message \(if applicable\) |
|
||||
| ↳ `photo` | array | List of photos included in the message |
|
||||
@@ -149,6 +159,11 @@ Send photos to Telegram channels or users through the Telegram Bot API.
|
||||
| ↳ `file_size` | number | Size of the photo file in bytes |
|
||||
| ↳ `width` | number | Photo width in pixels |
|
||||
| ↳ `height` | number | Photo height in pixels |
|
||||
| ↳ `file_id` | string | Unique file ID of the photo |
|
||||
| ↳ `file_unique_id` | string | Unique identifier for this file across different bots |
|
||||
| ↳ `file_size` | number | Size of the photo file in bytes |
|
||||
| ↳ `width` | number | Photo width in pixels |
|
||||
| ↳ `height` | number | Photo height in pixels |
|
||||
|
||||
### `telegram_send_video`
|
||||
|
||||
@@ -175,26 +190,25 @@ Send videos to Telegram channels or users through the Telegram Bot API.
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Sender |
|
||||
| ↳ `username` | string | Sender |
|
||||
| ↳ `id` | number | Chat ID |
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Chat first name \(if private chat\) |
|
||||
| ↳ `username` | string | Chat username \(for private or channels\) |
|
||||
| ↳ `chat` | object | Information about the chat where message was sent |
|
||||
| ↳ `id` | number | Chat ID |
|
||||
| ↳ `first_name` | string | Chat first name \(if private chat\) |
|
||||
| ↳ `username` | string | Chat username \(for private or channels\) |
|
||||
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
|
||||
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
|
||||
| ↳ `date` | number | Unix timestamp when the message was sent |
|
||||
| ↳ `text` | string | Text content of the sent message \(if applicable\) |
|
||||
| ↳ `format` | object | Media format information \(for videos, GIFs, etc.\) |
|
||||
| ↳ `file_name` | string | Media file name |
|
||||
| ↳ `mime_type` | string | Media MIME type |
|
||||
| ↳ `duration` | number | Duration of media in seconds |
|
||||
| ↳ `width` | number | Media width in pixels |
|
||||
| ↳ `height` | number | Media height in pixels |
|
||||
| ↳ `thumbnail` | object | Thumbnail image details |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `thumb` | object | Secondary thumbnail details \(duplicate of thumbnail\) |
|
||||
| ↳ `thumbnail` | object | Thumbnail image details |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
@@ -203,6 +217,32 @@ Send videos to Telegram channels or users through the Telegram Bot API.
|
||||
| ↳ `file_id` | string | Media file ID |
|
||||
| ↳ `file_unique_id` | string | Unique media file identifier |
|
||||
| ↳ `file_size` | number | Size of media file in bytes |
|
||||
| ↳ `thumb` | object | Secondary thumbnail details \(duplicate of thumbnail\) |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `file_name` | string | Document file name |
|
||||
| ↳ `mime_type` | string | Document MIME type |
|
||||
| ↳ `duration` | number | Duration of media in seconds |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `thumbnail` | object | Document thumbnail information |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `file_id` | string | Document file ID |
|
||||
| ↳ `file_unique_id` | string | Unique document file identifier |
|
||||
| ↳ `file_size` | number | Size of document file in bytes |
|
||||
| ↳ `thumb` | object | Duplicate thumbnail info \(used for compatibility\) |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `document` | object | Document file details if the message contains a document |
|
||||
| ↳ `file_name` | string | Document file name |
|
||||
| ↳ `mime_type` | string | Document MIME type |
|
||||
@@ -212,15 +252,17 @@ Send videos to Telegram channels or users through the Telegram Bot API.
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `file_id` | string | Document file ID |
|
||||
| ↳ `file_unique_id` | string | Unique document file identifier |
|
||||
| ↳ `file_size` | number | Size of document file in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `thumb` | object | Duplicate thumbnail info \(used for compatibility\) |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `file_id` | string | Document file ID |
|
||||
| ↳ `file_unique_id` | string | Unique document file identifier |
|
||||
| ↳ `file_size` | number | Size of document file in bytes |
|
||||
|
||||
### `telegram_send_audio`
|
||||
|
||||
@@ -247,11 +289,16 @@ Send audio files to Telegram channels or users through the Telegram Bot API.
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Sender |
|
||||
| ↳ `username` | string | Sender |
|
||||
| ↳ `id` | number | Chat ID |
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Chat first name \(if private chat\) |
|
||||
| ↳ `username` | string | Chat username \(for private or channels\) |
|
||||
| ↳ `chat` | object | Information about the chat where the message was sent |
|
||||
| ↳ `id` | number | Chat ID |
|
||||
| ↳ `first_name` | string | Chat first name \(if private chat\) |
|
||||
| ↳ `username` | string | Chat username \(for private or channels\) |
|
||||
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
|
||||
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
|
||||
| ↳ `date` | number | Unix timestamp when the message was sent |
|
||||
| ↳ `text` | string | Text content of the sent message \(if applicable\) |
|
||||
| ↳ `audio` | object | Audio file details |
|
||||
@@ -263,6 +310,14 @@ Send audio files to Telegram channels or users through the Telegram Bot API.
|
||||
| ↳ `file_id` | string | Unique file identifier for this audio |
|
||||
| ↳ `file_unique_id` | string | Unique identifier across different bots for this file |
|
||||
| ↳ `file_size` | number | Size of the audio file in bytes |
|
||||
| ↳ `duration` | number | Duration of the audio in seconds |
|
||||
| ↳ `performer` | string | Performer of the audio |
|
||||
| ↳ `title` | string | Title of the audio |
|
||||
| ↳ `file_name` | string | Original filename of the audio |
|
||||
| ↳ `mime_type` | string | MIME type of the audio file |
|
||||
| ↳ `file_id` | string | Unique file identifier for this audio |
|
||||
| ↳ `file_unique_id` | string | Unique identifier across different bots for this file |
|
||||
| ↳ `file_size` | number | Size of the audio file in bytes |
|
||||
|
||||
### `telegram_send_animation`
|
||||
|
||||
@@ -289,26 +344,25 @@ Send animations (GIFs) to Telegram channels or users through the Telegram Bot AP
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Sender |
|
||||
| ↳ `username` | string | Sender |
|
||||
| ↳ `id` | number | Chat ID |
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Chat first name \(if private chat\) |
|
||||
| ↳ `username` | string | Chat username \(for private or channels\) |
|
||||
| ↳ `chat` | object | Information about the chat where message was sent |
|
||||
| ↳ `id` | number | Chat ID |
|
||||
| ↳ `first_name` | string | Chat first name \(if private chat\) |
|
||||
| ↳ `username` | string | Chat username \(for private or channels\) |
|
||||
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
|
||||
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
|
||||
| ↳ `date` | number | Unix timestamp when the message was sent |
|
||||
| ↳ `text` | string | Text content of the sent message \(if applicable\) |
|
||||
| ↳ `format` | object | Media format information \(for videos, GIFs, etc.\) |
|
||||
| ↳ `file_name` | string | Media file name |
|
||||
| ↳ `mime_type` | string | Media MIME type |
|
||||
| ↳ `duration` | number | Duration of media in seconds |
|
||||
| ↳ `width` | number | Media width in pixels |
|
||||
| ↳ `height` | number | Media height in pixels |
|
||||
| ↳ `thumbnail` | object | Thumbnail image details |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `thumb` | object | Secondary thumbnail details \(duplicate of thumbnail\) |
|
||||
| ↳ `thumbnail` | object | Thumbnail image details |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
@@ -317,6 +371,32 @@ Send animations (GIFs) to Telegram channels or users through the Telegram Bot AP
|
||||
| ↳ `file_id` | string | Media file ID |
|
||||
| ↳ `file_unique_id` | string | Unique media file identifier |
|
||||
| ↳ `file_size` | number | Size of media file in bytes |
|
||||
| ↳ `thumb` | object | Secondary thumbnail details \(duplicate of thumbnail\) |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `file_name` | string | Document file name |
|
||||
| ↳ `mime_type` | string | Document MIME type |
|
||||
| ↳ `duration` | number | Duration of media in seconds |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `thumbnail` | object | Document thumbnail information |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `file_id` | string | Document file ID |
|
||||
| ↳ `file_unique_id` | string | Unique document file identifier |
|
||||
| ↳ `file_size` | number | Size of document file in bytes |
|
||||
| ↳ `thumb` | object | Duplicate thumbnail info \(used for compatibility\) |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `document` | object | Document file details if the message contains a document |
|
||||
| ↳ `file_name` | string | Document file name |
|
||||
| ↳ `mime_type` | string | Document MIME type |
|
||||
@@ -326,15 +406,17 @@ Send animations (GIFs) to Telegram channels or users through the Telegram Bot AP
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `file_id` | string | Document file ID |
|
||||
| ↳ `file_unique_id` | string | Unique document file identifier |
|
||||
| ↳ `file_size` | number | Size of document file in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `thumb` | object | Duplicate thumbnail info \(used for compatibility\) |
|
||||
| ↳ `file_id` | string | Thumbnail file ID |
|
||||
| ↳ `file_unique_id` | string | Unique thumbnail file identifier |
|
||||
| ↳ `file_size` | number | Thumbnail file size in bytes |
|
||||
| ↳ `width` | number | Thumbnail width in pixels |
|
||||
| ↳ `height` | number | Thumbnail height in pixels |
|
||||
| ↳ `file_id` | string | Document file ID |
|
||||
| ↳ `file_unique_id` | string | Unique document file identifier |
|
||||
| ↳ `file_size` | number | Size of document file in bytes |
|
||||
|
||||
### `telegram_send_document`
|
||||
|
||||
@@ -361,11 +443,16 @@ Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the T
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Sender |
|
||||
| ↳ `username` | string | Sender |
|
||||
| ↳ `id` | number | Chat ID |
|
||||
| ↳ `is_bot` | boolean | Whether the chat is a bot or not |
|
||||
| ↳ `first_name` | string | Chat first name \(if private chat\) |
|
||||
| ↳ `username` | string | Chat username \(for private or channels\) |
|
||||
| ↳ `chat` | object | Information about the chat where message was sent |
|
||||
| ↳ `id` | number | Chat ID |
|
||||
| ↳ `first_name` | string | Chat first name \(if private chat\) |
|
||||
| ↳ `username` | string | Chat username \(for private or channels\) |
|
||||
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
|
||||
| ↳ `type` | string | Type of chat \(private, group, supergroup, or channel\) |
|
||||
| ↳ `date` | number | Unix timestamp when the message was sent |
|
||||
| ↳ `document` | object | Document file details |
|
||||
| ↳ `file_name` | string | Document file name |
|
||||
@@ -373,5 +460,10 @@ Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the T
|
||||
| ↳ `file_id` | string | Document file ID |
|
||||
| ↳ `file_unique_id` | string | Unique document file identifier |
|
||||
| ↳ `file_size` | number | Size of document file in bytes |
|
||||
| ↳ `file_name` | string | Document file name |
|
||||
| ↳ `mime_type` | string | Document MIME type |
|
||||
| ↳ `file_id` | string | Document file ID |
|
||||
| ↳ `file_unique_id` | string | Unique document file identifier |
|
||||
| ↳ `file_size` | number | Size of document file in bytes |
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ Parse documents using AWS Textract OCR and document analysis
|
||||
| `blocks` | array | Array of Block objects containing detected text, tables, forms, and other elements |
|
||||
| ↳ `BlockType` | string | Type of block \(PAGE, LINE, WORD, TABLE, CELL, KEY_VALUE_SET, etc.\) |
|
||||
| ↳ `Id` | string | Unique identifier for the block |
|
||||
| ↳ `Text` | string | The text content \(for LINE and WORD blocks\) |
|
||||
| ↳ `Text` | string | Query text |
|
||||
| ↳ `TextType` | string | Type of text \(PRINTED or HANDWRITING\) |
|
||||
| ↳ `Confidence` | number | Confidence score \(0-100\) |
|
||||
| ↳ `Page` | number | Page number |
|
||||
@@ -73,12 +73,34 @@ Parse documents using AWS Textract OCR and document analysis
|
||||
| ↳ `Left` | number | Left position as ratio of document width |
|
||||
| ↳ `Top` | number | Top position as ratio of document height |
|
||||
| ↳ `Width` | number | Width as ratio of document width |
|
||||
| ↳ `Height` | number | Height as ratio of document height |
|
||||
| ↳ `Left` | number | Left position as ratio of document width |
|
||||
| ↳ `Top` | number | Top position as ratio of document height |
|
||||
| ↳ `Width` | number | Width as ratio of document width |
|
||||
| ↳ `Polygon` | array | Polygon coordinates |
|
||||
| ↳ `X` | number | X coordinate |
|
||||
| ↳ `Y` | number | Y coordinate |
|
||||
| ↳ `X` | number | X coordinate |
|
||||
| ↳ `Y` | number | Y coordinate |
|
||||
| ↳ `BoundingBox` | object | Height as ratio of document height |
|
||||
| ↳ `Height` | number | Height as ratio of document height |
|
||||
| ↳ `Left` | number | Left position as ratio of document width |
|
||||
| ↳ `Top` | number | Top position as ratio of document height |
|
||||
| ↳ `Width` | number | Width as ratio of document width |
|
||||
| ↳ `Height` | number | Height as ratio of document height |
|
||||
| ↳ `Left` | number | Left position as ratio of document width |
|
||||
| ↳ `Top` | number | Top position as ratio of document height |
|
||||
| ↳ `Width` | number | Width as ratio of document width |
|
||||
| ↳ `Polygon` | array | Polygon coordinates |
|
||||
| ↳ `X` | number | X coordinate |
|
||||
| ↳ `Y` | number | Y coordinate |
|
||||
| ↳ `X` | number | X coordinate |
|
||||
| ↳ `Y` | number | Y coordinate |
|
||||
| ↳ `Relationships` | array | Relationships to other blocks |
|
||||
| ↳ `Type` | string | Relationship type \(CHILD, VALUE, ANSWER, etc.\) |
|
||||
| ↳ `Ids` | array | IDs of related blocks |
|
||||
| ↳ `Type` | string | Relationship type \(CHILD, VALUE, ANSWER, etc.\) |
|
||||
| ↳ `Ids` | array | IDs of related blocks |
|
||||
| ↳ `EntityTypes` | array | Entity types for KEY_VALUE_SET \(KEY or VALUE\) |
|
||||
| ↳ `SelectionStatus` | string | For checkboxes: SELECTED or NOT_SELECTED |
|
||||
| ↳ `RowIndex` | number | Row index for table cells |
|
||||
@@ -89,6 +111,8 @@ Parse documents using AWS Textract OCR and document analysis
|
||||
| ↳ `Text` | string | Query text |
|
||||
| ↳ `Alias` | string | Query alias |
|
||||
| ↳ `Pages` | array | Pages to search |
|
||||
| ↳ `Alias` | string | Query alias |
|
||||
| ↳ `Pages` | array | Pages to search |
|
||||
| `documentMetadata` | object | Metadata about the analyzed document |
|
||||
| ↳ `pages` | number | Number of pages in the document |
|
||||
| `modelVersion` | string | Version of the Textract model used for processing |
|
||||
|
||||
@@ -109,6 +109,12 @@ Retrieve insights and analytics for Typeform forms
|
||||
| ↳ `responses_count` | number | Number of responses from this platform |
|
||||
| ↳ `total_visits` | number | Total visits from this platform |
|
||||
| ↳ `unique_visits` | number | Unique visits from this platform |
|
||||
| ↳ `average_time` | number | Overall average completion time |
|
||||
| ↳ `completion_rate` | number | Overall completion rate |
|
||||
| ↳ `platform` | string | Platform name \(e.g., desktop, mobile\) |
|
||||
| ↳ `responses_count` | number | Total number of responses |
|
||||
| ↳ `total_visits` | number | Total number of visits |
|
||||
| ↳ `unique_visits` | number | Total number of unique visits |
|
||||
| ↳ `summary` | object | Overall average completion time |
|
||||
| ↳ `average_time` | number | Overall average completion time |
|
||||
| ↳ `completion_rate` | number | Overall completion rate |
|
||||
|
||||
@@ -56,6 +56,9 @@ Read content from a Wealthbox note
|
||||
| ↳ `itemId` | string | ID of the note |
|
||||
| ↳ `noteId` | string | ID of the note |
|
||||
| ↳ `itemType` | string | Type of item \(note\) |
|
||||
| ↳ `itemId` | string | ID of the note |
|
||||
| ↳ `noteId` | string | ID of the note |
|
||||
| ↳ `itemType` | string | Type of item \(note\) |
|
||||
|
||||
### `wealthbox_write_note`
|
||||
|
||||
@@ -80,6 +83,9 @@ Create or update a Wealthbox note
|
||||
| ↳ `itemId` | string | ID of the created/updated note |
|
||||
| ↳ `noteId` | string | ID of the created/updated note |
|
||||
| ↳ `itemType` | string | Type of item \(note\) |
|
||||
| ↳ `itemId` | string | ID of the created/updated note |
|
||||
| ↳ `noteId` | string | ID of the created/updated note |
|
||||
| ↳ `itemType` | string | Type of item \(note\) |
|
||||
|
||||
### `wealthbox_read_contact`
|
||||
|
||||
@@ -103,6 +109,9 @@ Read content from a Wealthbox contact
|
||||
| ↳ `itemId` | string | ID of the contact |
|
||||
| ↳ `contactId` | string | ID of the contact |
|
||||
| ↳ `itemType` | string | Type of item \(contact\) |
|
||||
| ↳ `itemId` | string | ID of the contact |
|
||||
| ↳ `contactId` | string | ID of the contact |
|
||||
| ↳ `itemType` | string | Type of item \(contact\) |
|
||||
|
||||
### `wealthbox_write_contact`
|
||||
|
||||
@@ -129,6 +138,9 @@ Create a new Wealthbox contact
|
||||
| ↳ `itemId` | string | ID of the created/updated contact |
|
||||
| ↳ `contactId` | string | ID of the created/updated contact |
|
||||
| ↳ `itemType` | string | Type of item \(contact\) |
|
||||
| ↳ `itemId` | string | ID of the created/updated contact |
|
||||
| ↳ `contactId` | string | ID of the created/updated contact |
|
||||
| ↳ `itemType` | string | Type of item \(contact\) |
|
||||
|
||||
### `wealthbox_read_task`
|
||||
|
||||
@@ -152,6 +164,9 @@ Read content from a Wealthbox task
|
||||
| ↳ `itemId` | string | ID of the task |
|
||||
| ↳ `taskId` | string | ID of the task |
|
||||
| ↳ `itemType` | string | Type of item \(task\) |
|
||||
| ↳ `itemId` | string | ID of the task |
|
||||
| ↳ `taskId` | string | ID of the task |
|
||||
| ↳ `itemType` | string | Type of item \(task\) |
|
||||
|
||||
### `wealthbox_write_task`
|
||||
|
||||
@@ -178,5 +193,8 @@ Create or update a Wealthbox task
|
||||
| ↳ `itemId` | string | ID of the created/updated task |
|
||||
| ↳ `taskId` | string | ID of the created/updated task |
|
||||
| ↳ `itemType` | string | Type of item \(task\) |
|
||||
| ↳ `itemId` | string | ID of the created/updated task |
|
||||
| ↳ `taskId` | string | ID of the created/updated task |
|
||||
| ↳ `itemType` | string | Type of item \(task\) |
|
||||
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ Post new tweets, reply to tweets, or create polls on X (Twitter)
|
||||
| ↳ `attachments` | object | Media or poll attachments |
|
||||
| ↳ `mediaKeys` | array | Media attachment keys |
|
||||
| ↳ `pollId` | string | Poll ID if poll attached |
|
||||
| ↳ `mediaKeys` | array | Media attachment keys |
|
||||
| ↳ `pollId` | string | Poll ID if poll attached |
|
||||
|
||||
### `x_read`
|
||||
|
||||
@@ -137,5 +139,8 @@ Get user profile information
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
| ↳ `followersCount` | number | Number of followers |
|
||||
| ↳ `followingCount` | number | Number of users following |
|
||||
| ↳ `tweetCount` | number | Total number of tweets |
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
--panel-width: 320px; /* PANEL_WIDTH.DEFAULT */
|
||||
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
|
||||
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
|
||||
--terminal-height: 206px; /* TERMINAL_HEIGHT.DEFAULT */
|
||||
--terminal-height: 155px; /* TERMINAL_HEIGHT.DEFAULT */
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
|
||||
@@ -35,7 +35,8 @@ const AutoLayoutRequestSchema = z.object({
|
||||
})
|
||||
.optional()
|
||||
.default({}),
|
||||
gridSize: z.number().min(0).max(50).optional(),
|
||||
// Optional: if provided, use these blocks instead of loading from DB
|
||||
// This allows using blocks with live measurements from the UI
|
||||
blocks: z.record(z.any()).optional(),
|
||||
edges: z.array(z.any()).optional(),
|
||||
loops: z.record(z.any()).optional(),
|
||||
@@ -52,6 +53,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const { id: workflowId } = await params
|
||||
|
||||
try {
|
||||
// Get the session
|
||||
const session = await getSession()
|
||||
if (!session?.user?.id) {
|
||||
logger.warn(`[${requestId}] Unauthorized autolayout attempt for workflow ${workflowId}`)
|
||||
@@ -60,6 +62,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
const userId = session.user.id
|
||||
|
||||
// Parse request body
|
||||
const body = await request.json()
|
||||
const layoutOptions = AutoLayoutRequestSchema.parse(body)
|
||||
|
||||
@@ -67,6 +70,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
userId,
|
||||
})
|
||||
|
||||
// Fetch the workflow to check ownership/access
|
||||
const accessContext = await getWorkflowAccessContext(workflowId, userId)
|
||||
const workflowData = accessContext?.workflow
|
||||
|
||||
@@ -75,6 +79,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if user has permission to update this workflow
|
||||
const canUpdate =
|
||||
accessContext?.isOwner ||
|
||||
(workflowData.workspaceId
|
||||
@@ -89,6 +94,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Use provided blocks/edges if available (with live measurements from UI),
|
||||
// otherwise load from database
|
||||
let currentWorkflowData: NormalizedWorkflowData | null
|
||||
|
||||
if (layoutOptions.blocks && layoutOptions.edges) {
|
||||
@@ -118,7 +125,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
y: layoutOptions.padding?.y ?? DEFAULT_LAYOUT_PADDING.y,
|
||||
},
|
||||
alignment: layoutOptions.alignment,
|
||||
gridSize: layoutOptions.gridSize,
|
||||
}
|
||||
|
||||
const layoutResult = applyAutoLayout(
|
||||
|
||||
108
apps/sim/app/api/yaml/autolayout/route.ts
Normal file
108
apps/sim/app/api/yaml/autolayout/route.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { applyAutoLayout } from '@/lib/workflows/autolayout'
|
||||
import {
|
||||
DEFAULT_HORIZONTAL_SPACING,
|
||||
DEFAULT_LAYOUT_PADDING,
|
||||
DEFAULT_VERTICAL_SPACING,
|
||||
} from '@/lib/workflows/autolayout/constants'
|
||||
|
||||
const logger = createLogger('YamlAutoLayoutAPI')
|
||||
|
||||
const AutoLayoutRequestSchema = z.object({
|
||||
workflowState: z.object({
|
||||
blocks: z.record(z.any()),
|
||||
edges: z.array(z.any()),
|
||||
loops: z.record(z.any()).optional().default({}),
|
||||
parallels: z.record(z.any()).optional().default({}),
|
||||
}),
|
||||
options: z
|
||||
.object({
|
||||
spacing: z
|
||||
.object({
|
||||
horizontal: z.number().optional(),
|
||||
vertical: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
alignment: z.enum(['start', 'center', 'end']).optional(),
|
||||
padding: z
|
||||
.object({
|
||||
x: z.number().optional(),
|
||||
y: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const { workflowState, options } = AutoLayoutRequestSchema.parse(body)
|
||||
|
||||
logger.info(`[${requestId}] Applying auto layout`, {
|
||||
blockCount: Object.keys(workflowState.blocks).length,
|
||||
edgeCount: workflowState.edges.length,
|
||||
})
|
||||
|
||||
const autoLayoutOptions = {
|
||||
horizontalSpacing: options?.spacing?.horizontal ?? DEFAULT_HORIZONTAL_SPACING,
|
||||
verticalSpacing: options?.spacing?.vertical ?? DEFAULT_VERTICAL_SPACING,
|
||||
padding: {
|
||||
x: options?.padding?.x ?? DEFAULT_LAYOUT_PADDING.x,
|
||||
y: options?.padding?.y ?? DEFAULT_LAYOUT_PADDING.y,
|
||||
},
|
||||
alignment: options?.alignment ?? 'center',
|
||||
}
|
||||
|
||||
const layoutResult = applyAutoLayout(
|
||||
workflowState.blocks,
|
||||
workflowState.edges,
|
||||
autoLayoutOptions
|
||||
)
|
||||
|
||||
if (!layoutResult.success || !layoutResult.blocks) {
|
||||
logger.error(`[${requestId}] Auto layout failed:`, {
|
||||
error: layoutResult.error,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
errors: [layoutResult.error || 'Unknown auto layout error'],
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Auto layout completed successfully:`, {
|
||||
success: true,
|
||||
blockCount: Object.keys(layoutResult.blocks).length,
|
||||
})
|
||||
|
||||
const transformedResponse = {
|
||||
success: true,
|
||||
workflowState: {
|
||||
blocks: layoutResult.blocks,
|
||||
edges: workflowState.edges,
|
||||
loops: workflowState.loops || {},
|
||||
parallels: workflowState.parallels || {},
|
||||
},
|
||||
}
|
||||
|
||||
return NextResponse.json(transformedResponse)
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Auto layout failed:`, error)
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
errors: [error instanceof Error ? error.message : 'Unknown auto layout error'],
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Code,
|
||||
Input,
|
||||
Label,
|
||||
Table,
|
||||
@@ -776,6 +777,15 @@ export default function ResumeExecutionPage({
|
||||
refreshSelectedDetail,
|
||||
])
|
||||
|
||||
const pauseResponsePreview = useMemo(() => {
|
||||
if (!selectedDetail?.pausePoint.response?.data) return '{}'
|
||||
try {
|
||||
return JSON.stringify(selectedDetail.pausePoint.response.data, null, 2)
|
||||
} catch {
|
||||
return String(selectedDetail.pausePoint.response.data)
|
||||
}
|
||||
}, [selectedDetail])
|
||||
|
||||
const isFormComplete = useMemo(() => {
|
||||
if (!isHumanMode || !hasInputFormat) return true
|
||||
return inputFormatFields.every((field) => {
|
||||
@@ -1145,12 +1155,10 @@ export default function ResumeExecutionPage({
|
||||
borderBottom: '1px solid var(--border)',
|
||||
}}
|
||||
>
|
||||
<Label>Display Data</Label>
|
||||
<Label>Pause Data</Label>
|
||||
</div>
|
||||
<div style={{ padding: '16px' }}>
|
||||
<p style={{ fontSize: '13px', color: 'var(--text-muted)' }}>
|
||||
No display data configured
|
||||
</p>
|
||||
<Code.Viewer code={pauseResponsePreview} language='json' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -573,19 +573,7 @@ const TraceSpanNode = memo(function TraceSpanNode({
|
||||
return children.sort((a, b) => parseTime(a.startTime) - parseTime(b.startTime))
|
||||
}, [span, spanId, spanStartTime])
|
||||
|
||||
// Hide empty model timing segments for agents without tool calls
|
||||
const filteredChildren = useMemo(() => {
|
||||
const isAgent = span.type?.toLowerCase() === 'agent'
|
||||
const hasToolCalls =
|
||||
(span.toolCalls?.length ?? 0) > 0 || allChildren.some((c) => c.type?.toLowerCase() === 'tool')
|
||||
|
||||
if (isAgent && !hasToolCalls) {
|
||||
return allChildren.filter((c) => c.type?.toLowerCase() !== 'model')
|
||||
}
|
||||
return allChildren
|
||||
}, [allChildren, span.type, span.toolCalls])
|
||||
|
||||
const hasChildren = filteredChildren.length > 0
|
||||
const hasChildren = allChildren.length > 0
|
||||
const isExpanded = isRootWorkflow || expandedNodes.has(spanId)
|
||||
const isToggleable = !isRootWorkflow
|
||||
|
||||
@@ -697,7 +685,7 @@ const TraceSpanNode = memo(function TraceSpanNode({
|
||||
{/* Nested Children */}
|
||||
{hasChildren && (
|
||||
<div className='flex min-w-0 flex-col gap-[2px] border-[var(--border)] border-l pl-[10px]'>
|
||||
{filteredChildren.map((child, index) => (
|
||||
{allChildren.map((child, index) => (
|
||||
<div key={child.id || `${spanId}-child-${index}`} className='pl-[6px]'>
|
||||
<TraceSpanNode
|
||||
span={child}
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import clsx from 'clsx'
|
||||
import { RepeatIcon, SplitIcon } from 'lucide-react'
|
||||
import { ChevronDown, RepeatIcon, SplitIcon } from 'lucide-react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { ChevronDown } from '@/components/emcn'
|
||||
import {
|
||||
FieldItem,
|
||||
type SchemaField,
|
||||
@@ -116,8 +115,9 @@ function ConnectionItem({
|
||||
{hasFields && (
|
||||
<ChevronDown
|
||||
className={clsx(
|
||||
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
|
||||
!isExpanded && '-rotate-90'
|
||||
'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-100',
|
||||
'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]',
|
||||
isExpanded && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1183,6 +1183,19 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks, true)
|
||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
}
|
||||
} else if (sourceBlock.type === 'approval') {
|
||||
const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
|
||||
|
||||
const isSelfReference = activeSourceBlockId === blockId
|
||||
|
||||
if (dynamicOutputs.length > 0) {
|
||||
const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
|
||||
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
|
||||
} else {
|
||||
const outputPaths = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
|
||||
const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
|
||||
}
|
||||
} else if (sourceBlock.type === 'human_in_the_loop') {
|
||||
const dynamicOutputs = getBlockOutputPaths(sourceBlock.type, mergedSubBlocks)
|
||||
|
||||
@@ -1387,8 +1400,13 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
if (!accessibleBlock) continue
|
||||
|
||||
// Skip the current block - blocks cannot reference their own outputs
|
||||
// Exception: human_in_the_loop blocks can reference their own outputs (url, resumeEndpoint)
|
||||
if (accessibleBlockId === blockId && accessibleBlock.type !== 'human_in_the_loop') continue
|
||||
// Exception: approval and human_in_the_loop blocks can reference their own outputs
|
||||
if (
|
||||
accessibleBlockId === blockId &&
|
||||
accessibleBlock.type !== 'approval' &&
|
||||
accessibleBlock.type !== 'human_in_the_loop'
|
||||
)
|
||||
continue
|
||||
|
||||
const blockConfig = getBlock(accessibleBlock.type)
|
||||
|
||||
@@ -1502,6 +1520,19 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks, true)
|
||||
blockTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
}
|
||||
} else if (accessibleBlock.type === 'approval') {
|
||||
const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
|
||||
|
||||
const isSelfReference = accessibleBlockId === blockId
|
||||
|
||||
if (dynamicOutputs.length > 0) {
|
||||
const allTags = dynamicOutputs.map((path) => `${normalizedBlockName}.${path}`)
|
||||
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
|
||||
} else {
|
||||
const outputPaths = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
|
||||
const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
|
||||
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
|
||||
}
|
||||
} else if (accessibleBlock.type === 'human_in_the_loop') {
|
||||
const dynamicOutputs = getBlockOutputPaths(accessibleBlock.type, mergedSubBlocks)
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { memo } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { Filter } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
PopoverScrollArea,
|
||||
PopoverSection,
|
||||
PopoverTrigger,
|
||||
} from '@/components/emcn'
|
||||
import type {
|
||||
BlockInfo,
|
||||
TerminalFilters,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
|
||||
import { getBlockIcon } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/utils'
|
||||
|
||||
/**
|
||||
* Props for the FilterPopover component
|
||||
*/
|
||||
export interface FilterPopoverProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
filters: TerminalFilters
|
||||
toggleStatus: (status: 'error' | 'info') => void
|
||||
toggleBlock: (blockId: string) => void
|
||||
uniqueBlocks: BlockInfo[]
|
||||
hasActiveFilters: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter popover component used in terminal header and output panel
|
||||
*/
|
||||
export const FilterPopover = memo(function FilterPopover({
|
||||
open,
|
||||
onOpenChange,
|
||||
filters,
|
||||
toggleStatus,
|
||||
toggleBlock,
|
||||
uniqueBlocks,
|
||||
hasActiveFilters,
|
||||
}: FilterPopoverProps) {
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChange} size='sm'>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='!p-1.5 -m-1.5'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label='Filters'
|
||||
>
|
||||
<Filter
|
||||
className={clsx('h-3 w-3', hasActiveFilters && 'text-[var(--brand-secondary)]')}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side='top'
|
||||
align='end'
|
||||
sideOffset={4}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
minWidth={160}
|
||||
maxWidth={220}
|
||||
maxHeight={300}
|
||||
>
|
||||
<PopoverSection>Status</PopoverSection>
|
||||
<PopoverItem
|
||||
active={filters.statuses.has('error')}
|
||||
showCheck={filters.statuses.has('error')}
|
||||
onClick={() => toggleStatus('error')}
|
||||
>
|
||||
<div
|
||||
className='h-[6px] w-[6px] rounded-[2px]'
|
||||
style={{ backgroundColor: 'var(--text-error)' }}
|
||||
/>
|
||||
<span className='flex-1'>Error</span>
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
active={filters.statuses.has('info')}
|
||||
showCheck={filters.statuses.has('info')}
|
||||
onClick={() => toggleStatus('info')}
|
||||
>
|
||||
<div
|
||||
className='h-[6px] w-[6px] rounded-[2px]'
|
||||
style={{ backgroundColor: 'var(--terminal-status-info-color)' }}
|
||||
/>
|
||||
<span className='flex-1'>Info</span>
|
||||
</PopoverItem>
|
||||
|
||||
{uniqueBlocks.length > 0 && (
|
||||
<>
|
||||
<PopoverDivider className='my-[4px]' />
|
||||
<PopoverSection className='!mt-0'>Blocks</PopoverSection>
|
||||
<PopoverScrollArea className='max-h-[100px]'>
|
||||
{uniqueBlocks.map((block) => {
|
||||
const BlockIcon = getBlockIcon(block.blockType)
|
||||
const isSelected = filters.blockIds.has(block.blockId)
|
||||
|
||||
return (
|
||||
<PopoverItem
|
||||
key={block.blockId}
|
||||
active={isSelected}
|
||||
showCheck={isSelected}
|
||||
onClick={() => toggleBlock(block.blockId)}
|
||||
>
|
||||
{BlockIcon && <BlockIcon className='h-3 w-3' />}
|
||||
<span className='flex-1'>{block.blockName}</span>
|
||||
</PopoverItem>
|
||||
)
|
||||
})}
|
||||
</PopoverScrollArea>
|
||||
</>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
export { FilterPopover, type FilterPopoverProps } from './filter-popover'
|
||||
@@ -1,5 +1,2 @@
|
||||
export { FilterPopover, type FilterPopoverProps } from './filter-popover'
|
||||
export { LogRowContextMenu, type LogRowContextMenuProps } from './log-row-context-menu'
|
||||
export { OutputPanel, type OutputPanelProps } from './output-panel'
|
||||
export { RunningBadge, StatusDisplay, type StatusDisplayProps } from './status-display'
|
||||
export { ToggleButton, type ToggleButtonProps } from './toggle-button'
|
||||
export { LogRowContextMenu } from './log-row-context-menu'
|
||||
export { OutputContextMenu } from './output-context-menu'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { memo, type RefObject } from 'react'
|
||||
import type { RefObject } from 'react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
@@ -8,13 +8,20 @@ import {
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
} from '@/components/emcn'
|
||||
import type {
|
||||
ContextMenuPosition,
|
||||
TerminalFilters,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
|
||||
import type { ConsoleEntry } from '@/stores/terminal'
|
||||
|
||||
export interface LogRowContextMenuProps {
|
||||
interface ContextMenuPosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface TerminalFilters {
|
||||
blockIds: Set<string>
|
||||
statuses: Set<'error' | 'info'>
|
||||
runIds: Set<string>
|
||||
}
|
||||
|
||||
interface LogRowContextMenuProps {
|
||||
isOpen: boolean
|
||||
position: ContextMenuPosition
|
||||
menuRef: RefObject<HTMLDivElement | null>
|
||||
@@ -23,16 +30,19 @@ export interface LogRowContextMenuProps {
|
||||
filters: TerminalFilters
|
||||
onFilterByBlock: (blockId: string) => void
|
||||
onFilterByStatus: (status: 'error' | 'info') => void
|
||||
onFilterByRunId: (runId: string) => void
|
||||
onCopyRunId: (runId: string) => void
|
||||
onClearFilters: () => void
|
||||
onClearConsole: () => void
|
||||
onFixInCopilot: (entry: ConsoleEntry) => void
|
||||
hasActiveFilters: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu for terminal log rows (left side).
|
||||
* Displays filtering options based on the selected row's properties.
|
||||
*/
|
||||
export const LogRowContextMenu = memo(function LogRowContextMenu({
|
||||
export function LogRowContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
menuRef,
|
||||
@@ -41,15 +51,19 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
|
||||
filters,
|
||||
onFilterByBlock,
|
||||
onFilterByStatus,
|
||||
onFilterByRunId,
|
||||
onCopyRunId,
|
||||
onClearFilters,
|
||||
onClearConsole,
|
||||
onFixInCopilot,
|
||||
hasActiveFilters,
|
||||
}: LogRowContextMenuProps) {
|
||||
const hasRunId = entry?.executionId != null
|
||||
|
||||
const isBlockFiltered = entry ? filters.blockIds.has(entry.blockId) : false
|
||||
const entryStatus = entry?.success ? 'info' : 'error'
|
||||
const isStatusFiltered = entry ? filters.statuses.has(entryStatus) : false
|
||||
const isRunIdFiltered = entry?.executionId ? filters.runIds.has(entry.executionId) : false
|
||||
|
||||
return (
|
||||
<Popover
|
||||
@@ -120,11 +134,34 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
|
||||
>
|
||||
Filter by Status
|
||||
</PopoverItem>
|
||||
{hasRunId && (
|
||||
<PopoverItem
|
||||
showCheck={isRunIdFiltered}
|
||||
onClick={() => {
|
||||
onFilterByRunId(entry.executionId!)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Filter by Run ID
|
||||
</PopoverItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Clear filters */}
|
||||
{hasActiveFilters && (
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onClearFilters()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Clear All Filters
|
||||
</PopoverItem>
|
||||
)}
|
||||
|
||||
{/* Destructive action */}
|
||||
{entry && <PopoverDivider />}
|
||||
{(entry || hasActiveFilters) && <PopoverDivider />}
|
||||
<PopoverItem
|
||||
onClick={() => {
|
||||
onClearConsole()
|
||||
@@ -136,4 +173,4 @@ export const LogRowContextMenu = memo(function LogRowContextMenu({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { LogRowContextMenu, type LogRowContextMenuProps } from './log-row-context-menu'
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { memo, type RefObject } from 'react'
|
||||
import type { RefObject } from 'react'
|
||||
import {
|
||||
Popover,
|
||||
PopoverAnchor,
|
||||
@@ -8,9 +8,13 @@ import {
|
||||
PopoverDivider,
|
||||
PopoverItem,
|
||||
} from '@/components/emcn'
|
||||
import type { ContextMenuPosition } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
|
||||
|
||||
export interface OutputContextMenuProps {
|
||||
interface ContextMenuPosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface OutputContextMenuProps {
|
||||
isOpen: boolean
|
||||
position: ContextMenuPosition
|
||||
menuRef: RefObject<HTMLDivElement | null>
|
||||
@@ -18,8 +22,6 @@ export interface OutputContextMenuProps {
|
||||
onCopySelection: () => void
|
||||
onCopyAll: () => void
|
||||
onSearch: () => void
|
||||
structuredView: boolean
|
||||
onToggleStructuredView: () => void
|
||||
wrapText: boolean
|
||||
onToggleWrap: () => void
|
||||
openOnRun: boolean
|
||||
@@ -32,7 +34,7 @@ export interface OutputContextMenuProps {
|
||||
* Context menu for terminal output panel (right side).
|
||||
* Displays copy, search, and display options for the code viewer.
|
||||
*/
|
||||
export const OutputContextMenu = memo(function OutputContextMenu({
|
||||
export function OutputContextMenu({
|
||||
isOpen,
|
||||
position,
|
||||
menuRef,
|
||||
@@ -40,8 +42,6 @@ export const OutputContextMenu = memo(function OutputContextMenu({
|
||||
onCopySelection,
|
||||
onCopyAll,
|
||||
onSearch,
|
||||
structuredView,
|
||||
onToggleStructuredView,
|
||||
wrapText,
|
||||
onToggleWrap,
|
||||
openOnRun,
|
||||
@@ -96,9 +96,6 @@ export const OutputContextMenu = memo(function OutputContextMenu({
|
||||
|
||||
{/* Display settings - toggles don't close menu */}
|
||||
<PopoverDivider />
|
||||
<PopoverItem showCheck={structuredView} onClick={onToggleStructuredView}>
|
||||
Structured View
|
||||
</PopoverItem>
|
||||
<PopoverItem showCheck={wrapText} onClick={onToggleWrap}>
|
||||
Wrap Text
|
||||
</PopoverItem>
|
||||
@@ -119,4 +116,4 @@ export const OutputContextMenu = memo(function OutputContextMenu({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -1,913 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import {
|
||||
createContext,
|
||||
memo,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { List, type RowComponentProps, useListRef } from 'react-window'
|
||||
import { Badge, ChevronDown } from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
|
||||
type ValueType = 'null' | 'undefined' | 'array' | 'string' | 'number' | 'boolean' | 'object'
|
||||
type BadgeVariant = 'green' | 'blue' | 'orange' | 'purple' | 'gray' | 'red'
|
||||
|
||||
interface NodeEntry {
|
||||
key: string
|
||||
value: unknown
|
||||
path: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Search context for structured output tree.
|
||||
*/
|
||||
interface SearchContextValue {
|
||||
query: string
|
||||
pathToMatchIndices: Map<string, number[]>
|
||||
}
|
||||
|
||||
const SearchContext = createContext<SearchContextValue | null>(null)
|
||||
|
||||
/**
|
||||
* Configuration for virtualized rendering.
|
||||
*/
|
||||
const CONFIG = {
|
||||
ROW_HEIGHT: 22,
|
||||
INDENT_PER_LEVEL: 12,
|
||||
BASE_PADDING: 20,
|
||||
MAX_SEARCH_DEPTH: 100,
|
||||
OVERSCAN_COUNT: 10,
|
||||
VIRTUALIZATION_THRESHOLD: 200,
|
||||
} as const
|
||||
|
||||
const BADGE_VARIANTS: Record<ValueType, BadgeVariant> = {
|
||||
string: 'green',
|
||||
number: 'blue',
|
||||
boolean: 'orange',
|
||||
array: 'purple',
|
||||
null: 'gray',
|
||||
undefined: 'gray',
|
||||
object: 'gray',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Styling constants matching the original non-virtualized implementation.
|
||||
*/
|
||||
const STYLES = {
|
||||
row: 'group flex min-h-[22px] cursor-pointer items-center gap-[6px] rounded-[8px] px-[6px] -mx-[6px] hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-5)]',
|
||||
chevron:
|
||||
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
|
||||
keyName:
|
||||
'font-medium text-[13px] text-[var(--text-primary)] group-hover:text-[var(--text-primary)]',
|
||||
badge: 'rounded-[4px] px-[4px] py-[0px] text-[11px]',
|
||||
summary: 'text-[12px] text-[var(--text-tertiary)]',
|
||||
indent:
|
||||
'mt-[2px] ml-[3px] flex min-w-0 flex-col gap-[2px] border-[var(--border)] border-l pl-[9px]',
|
||||
value: 'min-w-0 py-[2px] text-[13px] text-[var(--text-primary)]',
|
||||
emptyValue: 'py-[2px] text-[13px] text-[var(--text-tertiary)]',
|
||||
matchHighlight: 'bg-yellow-200/60 dark:bg-yellow-500/40',
|
||||
currentMatchHighlight: 'bg-orange-400',
|
||||
} as const
|
||||
|
||||
const EMPTY_MATCH_INDICES: number[] = []
|
||||
|
||||
function getTypeLabel(value: unknown): ValueType {
|
||||
if (value === null) return 'null'
|
||||
if (value === undefined) return 'undefined'
|
||||
if (Array.isArray(value)) return 'array'
|
||||
return typeof value as ValueType
|
||||
}
|
||||
|
||||
function formatPrimitive(value: unknown): string {
|
||||
if (value === null) return 'null'
|
||||
if (value === undefined) return 'undefined'
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function isPrimitive(value: unknown): value is null | undefined | string | number | boolean {
|
||||
return value === null || value === undefined || typeof value !== 'object'
|
||||
}
|
||||
|
||||
function isEmpty(value: unknown): boolean {
|
||||
if (Array.isArray(value)) return value.length === 0
|
||||
if (typeof value === 'object' && value !== null) return Object.keys(value).length === 0
|
||||
return false
|
||||
}
|
||||
|
||||
function extractErrorMessage(data: unknown): string {
|
||||
if (typeof data === 'string') return data
|
||||
if (data instanceof Error) return data.message
|
||||
if (typeof data === 'object' && data !== null && 'message' in data) {
|
||||
return String((data as { message: unknown }).message)
|
||||
}
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
function buildEntries(value: unknown, basePath: string): NodeEntry[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item, i) => ({ key: String(i), value: item, path: `${basePath}[${i}]` }))
|
||||
}
|
||||
return Object.entries(value as Record<string, unknown>).map(([k, v]) => ({
|
||||
key: k,
|
||||
value: v,
|
||||
path: `${basePath}.${k}`,
|
||||
}))
|
||||
}
|
||||
|
||||
function getCollapsedSummary(value: unknown): string | null {
|
||||
if (Array.isArray(value)) {
|
||||
const len = value.length
|
||||
return `${len} item${len !== 1 ? 's' : ''}`
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
const count = Object.keys(value).length
|
||||
return `${count} key${count !== 1 ? 's' : ''}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function computeInitialPaths(data: unknown, isError: boolean): Set<string> {
|
||||
if (isError) return new Set(['root.error'])
|
||||
if (!data || typeof data !== 'object') return new Set()
|
||||
const entries = Array.isArray(data)
|
||||
? data.map((_, i) => `root[${i}]`)
|
||||
: Object.keys(data).map((k) => `root.${k}`)
|
||||
return new Set(entries)
|
||||
}
|
||||
|
||||
function getAncestorPaths(path: string): string[] {
|
||||
const ancestors: string[] = []
|
||||
let current = path
|
||||
|
||||
while (current.includes('.') || current.includes('[')) {
|
||||
const splitPoint = Math.max(current.lastIndexOf('.'), current.lastIndexOf('['))
|
||||
if (splitPoint <= 0) break
|
||||
current = current.slice(0, splitPoint)
|
||||
if (current !== 'root') ancestors.push(current)
|
||||
}
|
||||
|
||||
return ancestors
|
||||
}
|
||||
|
||||
function findTextMatches(text: string, query: string): Array<[number, number]> {
|
||||
if (!query) return []
|
||||
|
||||
const matches: Array<[number, number]> = []
|
||||
const lowerText = text.toLowerCase()
|
||||
const lowerQuery = query.toLowerCase()
|
||||
let pos = 0
|
||||
|
||||
while (pos < lowerText.length) {
|
||||
const idx = lowerText.indexOf(lowerQuery, pos)
|
||||
if (idx === -1) break
|
||||
matches.push([idx, idx + query.length])
|
||||
pos = idx + 1
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
function addPrimitiveMatches(value: unknown, path: string, query: string, matches: string[]): void {
|
||||
const text = formatPrimitive(value)
|
||||
const count = findTextMatches(text, query).length
|
||||
for (let i = 0; i < count; i++) {
|
||||
matches.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
function collectAllMatchPaths(data: unknown, query: string, basePath: string, depth = 0): string[] {
|
||||
if (!query || depth > CONFIG.MAX_SEARCH_DEPTH) return []
|
||||
|
||||
const matches: string[] = []
|
||||
|
||||
if (isPrimitive(data)) {
|
||||
addPrimitiveMatches(data, `${basePath}.value`, query, matches)
|
||||
return matches
|
||||
}
|
||||
|
||||
for (const entry of buildEntries(data, basePath)) {
|
||||
if (isPrimitive(entry.value)) {
|
||||
addPrimitiveMatches(entry.value, entry.path, query, matches)
|
||||
} else {
|
||||
matches.push(...collectAllMatchPaths(entry.value, query, entry.path, depth + 1))
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
function buildPathToIndicesMap(matchPaths: string[]): Map<string, number[]> {
|
||||
const map = new Map<string, number[]>()
|
||||
matchPaths.forEach((path, globalIndex) => {
|
||||
const existing = map.get(path)
|
||||
if (existing) {
|
||||
existing.push(globalIndex)
|
||||
} else {
|
||||
map.set(path, [globalIndex])
|
||||
}
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders text with search highlights using segments.
|
||||
*/
|
||||
function renderHighlightedSegments(
|
||||
text: string,
|
||||
query: string,
|
||||
matchIndices: number[],
|
||||
currentMatchIndex: number,
|
||||
path: string
|
||||
): React.ReactNode {
|
||||
if (!query || matchIndices.length === 0) return text
|
||||
|
||||
const textMatches = findTextMatches(text, query)
|
||||
if (textMatches.length === 0) return text
|
||||
|
||||
const segments: React.ReactNode[] = []
|
||||
let lastEnd = 0
|
||||
|
||||
textMatches.forEach(([start, end], i) => {
|
||||
const globalIndex = matchIndices[i]
|
||||
const isCurrent = globalIndex === currentMatchIndex
|
||||
|
||||
if (start > lastEnd) {
|
||||
segments.push(<span key={`t-${path}-${start}`}>{text.slice(lastEnd, start)}</span>)
|
||||
}
|
||||
|
||||
segments.push(
|
||||
<mark
|
||||
key={`m-${path}-${start}`}
|
||||
data-search-match
|
||||
data-match-index={globalIndex}
|
||||
className={cn(
|
||||
'rounded-sm',
|
||||
isCurrent ? STYLES.currentMatchHighlight : STYLES.matchHighlight
|
||||
)}
|
||||
>
|
||||
{text.slice(start, end)}
|
||||
</mark>
|
||||
)
|
||||
lastEnd = end
|
||||
})
|
||||
|
||||
if (lastEnd < text.length) {
|
||||
segments.push(<span key={`t-${path}-${lastEnd}`}>{text.slice(lastEnd)}</span>)
|
||||
}
|
||||
|
||||
return <>{segments}</>
|
||||
}
|
||||
|
||||
interface HighlightedTextProps {
|
||||
text: string
|
||||
matchIndices: number[]
|
||||
path: string
|
||||
currentMatchIndex: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders text with search highlights for non-virtualized mode.
|
||||
* Accepts currentMatchIndex as prop to ensure re-render when it changes.
|
||||
*/
|
||||
const HighlightedText = memo(function HighlightedText({
|
||||
text,
|
||||
matchIndices,
|
||||
path,
|
||||
currentMatchIndex,
|
||||
}: HighlightedTextProps) {
|
||||
const searchContext = useContext(SearchContext)
|
||||
|
||||
if (!searchContext || matchIndices.length === 0) return <>{text}</>
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderHighlightedSegments(text, searchContext.query, matchIndices, currentMatchIndex, path)}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
interface StructuredNodeProps {
|
||||
name: string
|
||||
value: unknown
|
||||
path: string
|
||||
expandedPaths: Set<string>
|
||||
onToggle: (path: string) => void
|
||||
wrapText: boolean
|
||||
currentMatchIndex: number
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive node component for non-virtualized rendering.
|
||||
* Preserves exact original styling with border-left tree lines.
|
||||
*/
|
||||
const StructuredNode = memo(function StructuredNode({
|
||||
name,
|
||||
value,
|
||||
path,
|
||||
expandedPaths,
|
||||
onToggle,
|
||||
wrapText,
|
||||
currentMatchIndex,
|
||||
isError = false,
|
||||
}: StructuredNodeProps) {
|
||||
const searchContext = useContext(SearchContext)
|
||||
const type = getTypeLabel(value)
|
||||
const isPrimitiveValue = isPrimitive(value)
|
||||
const isEmptyValue = !isPrimitiveValue && isEmpty(value)
|
||||
const isExpanded = expandedPaths.has(path)
|
||||
|
||||
const handleToggle = useCallback(() => onToggle(path), [onToggle, path])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
handleToggle()
|
||||
}
|
||||
},
|
||||
[handleToggle]
|
||||
)
|
||||
|
||||
const childEntries = useMemo(
|
||||
() => (isPrimitiveValue || isEmptyValue ? [] : buildEntries(value, path)),
|
||||
[value, isPrimitiveValue, isEmptyValue, path]
|
||||
)
|
||||
|
||||
const collapsedSummary = useMemo(
|
||||
() => (isPrimitiveValue ? null : getCollapsedSummary(value)),
|
||||
[value, isPrimitiveValue]
|
||||
)
|
||||
|
||||
const badgeVariant = isError ? 'red' : BADGE_VARIANTS[type]
|
||||
const valueText = isPrimitiveValue ? formatPrimitive(value) : ''
|
||||
const matchIndices = searchContext?.pathToMatchIndices.get(path) ?? EMPTY_MATCH_INDICES
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col'>
|
||||
<div
|
||||
className={STYLES.row}
|
||||
onClick={handleToggle}
|
||||
onKeyDown={handleKeyDown}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
<span className={cn(STYLES.keyName, isError && 'text-[var(--text-error)]')}>{name}</span>
|
||||
<Badge variant={badgeVariant} className={STYLES.badge}>
|
||||
{type}
|
||||
</Badge>
|
||||
{!isExpanded && collapsedSummary && (
|
||||
<span className={STYLES.summary}>{collapsedSummary}</span>
|
||||
)}
|
||||
<ChevronDown className={cn(STYLES.chevron, !isExpanded && '-rotate-90')} />
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className={STYLES.indent}>
|
||||
{isPrimitiveValue ? (
|
||||
<div
|
||||
className={cn(
|
||||
STYLES.value,
|
||||
wrapText ? '[word-break:break-word]' : 'whitespace-nowrap'
|
||||
)}
|
||||
>
|
||||
<HighlightedText
|
||||
text={valueText}
|
||||
matchIndices={matchIndices}
|
||||
path={path}
|
||||
currentMatchIndex={currentMatchIndex}
|
||||
/>
|
||||
</div>
|
||||
) : isEmptyValue ? (
|
||||
<div className={STYLES.emptyValue}>{Array.isArray(value) ? '[]' : '{}'}</div>
|
||||
) : (
|
||||
childEntries.map((entry) => (
|
||||
<StructuredNode
|
||||
key={entry.path}
|
||||
name={entry.key}
|
||||
value={entry.value}
|
||||
path={entry.path}
|
||||
expandedPaths={expandedPaths}
|
||||
onToggle={onToggle}
|
||||
wrapText={wrapText}
|
||||
currentMatchIndex={currentMatchIndex}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Flattened row for virtualization.
|
||||
*/
|
||||
interface FlatRow {
|
||||
path: string
|
||||
key: string
|
||||
value: unknown
|
||||
depth: number
|
||||
type: 'header' | 'value' | 'empty'
|
||||
valueType: ValueType
|
||||
isExpanded: boolean
|
||||
isError: boolean
|
||||
collapsedSummary: string | null
|
||||
displayText: string
|
||||
matchIndices: number[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens the tree into rows for virtualization.
|
||||
*/
|
||||
function flattenTree(
|
||||
data: unknown,
|
||||
expandedPaths: Set<string>,
|
||||
pathToMatchIndices: Map<string, number[]>,
|
||||
isError: boolean
|
||||
): FlatRow[] {
|
||||
const rows: FlatRow[] = []
|
||||
|
||||
if (isError) {
|
||||
const errorText = extractErrorMessage(data)
|
||||
const isExpanded = expandedPaths.has('root.error')
|
||||
|
||||
rows.push({
|
||||
path: 'root.error',
|
||||
key: 'error',
|
||||
value: errorText,
|
||||
depth: 0,
|
||||
type: 'header',
|
||||
valueType: 'string',
|
||||
isExpanded,
|
||||
isError: true,
|
||||
collapsedSummary: null,
|
||||
displayText: '',
|
||||
matchIndices: [],
|
||||
})
|
||||
|
||||
if (isExpanded) {
|
||||
rows.push({
|
||||
path: 'root.error.value',
|
||||
key: '',
|
||||
value: errorText,
|
||||
depth: 1,
|
||||
type: 'value',
|
||||
valueType: 'string',
|
||||
isExpanded: false,
|
||||
isError: true,
|
||||
collapsedSummary: null,
|
||||
displayText: errorText,
|
||||
matchIndices: pathToMatchIndices.get('root.error') ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
function processNode(key: string, value: unknown, path: string, depth: number): void {
|
||||
const valueType = getTypeLabel(value)
|
||||
const isPrimitiveValue = isPrimitive(value)
|
||||
const isEmptyValue = !isPrimitiveValue && isEmpty(value)
|
||||
const isExpanded = expandedPaths.has(path)
|
||||
const collapsedSummary = isPrimitiveValue ? null : getCollapsedSummary(value)
|
||||
|
||||
rows.push({
|
||||
path,
|
||||
key,
|
||||
value,
|
||||
depth,
|
||||
type: 'header',
|
||||
valueType,
|
||||
isExpanded,
|
||||
isError: false,
|
||||
collapsedSummary,
|
||||
displayText: '',
|
||||
matchIndices: [],
|
||||
})
|
||||
|
||||
if (isExpanded) {
|
||||
if (isPrimitiveValue) {
|
||||
rows.push({
|
||||
path: `${path}.value`,
|
||||
key: '',
|
||||
value,
|
||||
depth: depth + 1,
|
||||
type: 'value',
|
||||
valueType,
|
||||
isExpanded: false,
|
||||
isError: false,
|
||||
collapsedSummary: null,
|
||||
displayText: formatPrimitive(value),
|
||||
matchIndices: pathToMatchIndices.get(path) ?? [],
|
||||
})
|
||||
} else if (isEmptyValue) {
|
||||
rows.push({
|
||||
path: `${path}.empty`,
|
||||
key: '',
|
||||
value,
|
||||
depth: depth + 1,
|
||||
type: 'empty',
|
||||
valueType,
|
||||
isExpanded: false,
|
||||
isError: false,
|
||||
collapsedSummary: null,
|
||||
displayText: Array.isArray(value) ? '[]' : '{}',
|
||||
matchIndices: [],
|
||||
})
|
||||
} else {
|
||||
for (const entry of buildEntries(value, path)) {
|
||||
processNode(entry.key, entry.value, entry.path, depth + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPrimitive(data)) {
|
||||
processNode('value', data, 'root.value', 0)
|
||||
} else if (data && typeof data === 'object') {
|
||||
for (const entry of buildEntries(data, 'root')) {
|
||||
processNode(entry.key, entry.value, entry.path, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts total visible rows for determining virtualization threshold.
|
||||
*/
|
||||
function countVisibleRows(data: unknown, expandedPaths: Set<string>, isError: boolean): number {
|
||||
if (isError) return expandedPaths.has('root.error') ? 2 : 1
|
||||
|
||||
let count = 0
|
||||
|
||||
function countNode(value: unknown, path: string): void {
|
||||
count++
|
||||
if (!expandedPaths.has(path)) return
|
||||
|
||||
if (isPrimitive(value) || isEmpty(value)) {
|
||||
count++
|
||||
} else {
|
||||
for (const entry of buildEntries(value, path)) {
|
||||
countNode(entry.value, entry.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPrimitive(data)) {
|
||||
countNode(data, 'root.value')
|
||||
} else if (data && typeof data === 'object') {
|
||||
for (const entry of buildEntries(data, 'root')) {
|
||||
countNode(entry.value, entry.path)
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
interface VirtualizedRowProps {
|
||||
rows: FlatRow[]
|
||||
onToggle: (path: string) => void
|
||||
wrapText: boolean
|
||||
searchQuery: string
|
||||
currentMatchIndex: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtualized row component for large data sets.
|
||||
*/
|
||||
function VirtualizedRow({ index, style, ...props }: RowComponentProps<VirtualizedRowProps>) {
|
||||
const { rows, onToggle, wrapText, searchQuery, currentMatchIndex } = props
|
||||
const row = rows[index]
|
||||
const paddingLeft = CONFIG.BASE_PADDING + row.depth * CONFIG.INDENT_PER_LEVEL
|
||||
|
||||
if (row.type === 'header') {
|
||||
const badgeVariant = row.isError ? 'red' : BADGE_VARIANTS[row.valueType]
|
||||
|
||||
return (
|
||||
<div style={{ ...style, paddingLeft }} data-row-index={index}>
|
||||
<div
|
||||
className={STYLES.row}
|
||||
onClick={() => onToggle(row.path)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onToggle(row.path)
|
||||
}
|
||||
}}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
aria-expanded={row.isExpanded}
|
||||
>
|
||||
<span className={cn(STYLES.keyName, row.isError && 'text-[var(--text-error)]')}>
|
||||
{row.key}
|
||||
</span>
|
||||
<Badge variant={badgeVariant} className={STYLES.badge}>
|
||||
{row.valueType}
|
||||
</Badge>
|
||||
{!row.isExpanded && row.collapsedSummary && (
|
||||
<span className={STYLES.summary}>{row.collapsedSummary}</span>
|
||||
)}
|
||||
<ChevronDown className={cn(STYLES.chevron, !row.isExpanded && '-rotate-90')} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (row.type === 'empty') {
|
||||
return (
|
||||
<div style={{ ...style, paddingLeft }} data-row-index={index}>
|
||||
<div className={STYLES.emptyValue}>{row.displayText}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ ...style, paddingLeft }} data-row-index={index}>
|
||||
<div
|
||||
className={cn(
|
||||
STYLES.value,
|
||||
row.isError && 'text-[var(--text-error)]',
|
||||
wrapText ? '[word-break:break-word]' : 'whitespace-nowrap'
|
||||
)}
|
||||
>
|
||||
{renderHighlightedSegments(
|
||||
row.displayText,
|
||||
searchQuery,
|
||||
row.matchIndices,
|
||||
currentMatchIndex,
|
||||
row.path
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface StructuredOutputProps {
|
||||
data: unknown
|
||||
wrapText?: boolean
|
||||
isError?: boolean
|
||||
isRunning?: boolean
|
||||
className?: string
|
||||
searchQuery?: string
|
||||
currentMatchIndex?: number
|
||||
onMatchCountChange?: (count: number) => void
|
||||
contentRef?: React.RefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders structured data as nested collapsible blocks.
|
||||
* Uses virtualization for large data sets (>200 visible rows) while
|
||||
* preserving exact original styling for smaller data sets.
|
||||
*/
|
||||
export const StructuredOutput = memo(function StructuredOutput({
|
||||
data,
|
||||
wrapText = true,
|
||||
isError = false,
|
||||
isRunning = false,
|
||||
className,
|
||||
searchQuery,
|
||||
currentMatchIndex = 0,
|
||||
onMatchCountChange,
|
||||
contentRef,
|
||||
}: StructuredOutputProps) {
|
||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(() =>
|
||||
computeInitialPaths(data, isError)
|
||||
)
|
||||
const prevDataRef = useRef(data)
|
||||
const prevIsErrorRef = useRef(isError)
|
||||
const internalRef = useRef<HTMLDivElement>(null)
|
||||
const listRef = useListRef(null)
|
||||
const [containerHeight, setContainerHeight] = useState(400)
|
||||
|
||||
const setContainerRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
;(internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
||||
if (contentRef) {
|
||||
;(contentRef as React.MutableRefObject<HTMLDivElement | null>).current = node
|
||||
}
|
||||
},
|
||||
[contentRef]
|
||||
)
|
||||
|
||||
// Measure container height
|
||||
useEffect(() => {
|
||||
const container = internalRef.current?.parentElement
|
||||
if (!container) return
|
||||
|
||||
const updateHeight = () => setContainerHeight(container.clientHeight)
|
||||
updateHeight()
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateHeight)
|
||||
resizeObserver.observe(container)
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [])
|
||||
|
||||
// Reset expanded paths when data changes
|
||||
useEffect(() => {
|
||||
if (prevDataRef.current !== data || prevIsErrorRef.current !== isError) {
|
||||
prevDataRef.current = data
|
||||
prevIsErrorRef.current = isError
|
||||
setExpandedPaths(computeInitialPaths(data, isError))
|
||||
}
|
||||
}, [data, isError])
|
||||
|
||||
const allMatchPaths = useMemo(() => {
|
||||
if (!searchQuery) return []
|
||||
if (isError) {
|
||||
const errorText = extractErrorMessage(data)
|
||||
const count = findTextMatches(errorText, searchQuery).length
|
||||
return Array(count).fill('root.error') as string[]
|
||||
}
|
||||
return collectAllMatchPaths(data, searchQuery, 'root')
|
||||
}, [data, searchQuery, isError])
|
||||
|
||||
useEffect(() => {
|
||||
onMatchCountChange?.(allMatchPaths.length)
|
||||
}, [allMatchPaths.length, onMatchCountChange])
|
||||
|
||||
const pathToMatchIndices = useMemo(() => buildPathToIndicesMap(allMatchPaths), [allMatchPaths])
|
||||
|
||||
// Auto-expand to current match
|
||||
useEffect(() => {
|
||||
if (
|
||||
allMatchPaths.length === 0 ||
|
||||
currentMatchIndex < 0 ||
|
||||
currentMatchIndex >= allMatchPaths.length
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentPath = allMatchPaths[currentMatchIndex]
|
||||
const pathsToExpand = [currentPath, ...getAncestorPaths(currentPath)]
|
||||
|
||||
setExpandedPaths((prev) => {
|
||||
if (pathsToExpand.every((p) => prev.has(p))) return prev
|
||||
const next = new Set(prev)
|
||||
pathsToExpand.forEach((p) => next.add(p))
|
||||
return next
|
||||
})
|
||||
}, [currentMatchIndex, allMatchPaths])
|
||||
|
||||
const handleToggle = useCallback((path: string) => {
|
||||
setExpandedPaths((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(path)) {
|
||||
next.delete(path)
|
||||
} else {
|
||||
next.add(path)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const rootEntries = useMemo<NodeEntry[]>(() => {
|
||||
if (isPrimitive(data)) return [{ key: 'value', value: data, path: 'root.value' }]
|
||||
return buildEntries(data, 'root')
|
||||
}, [data])
|
||||
|
||||
const searchContextValue = useMemo<SearchContextValue | null>(() => {
|
||||
if (!searchQuery) return null
|
||||
return { query: searchQuery, pathToMatchIndices }
|
||||
}, [searchQuery, pathToMatchIndices])
|
||||
|
||||
const visibleRowCount = useMemo(
|
||||
() => countVisibleRows(data, expandedPaths, isError),
|
||||
[data, expandedPaths, isError]
|
||||
)
|
||||
const useVirtualization = visibleRowCount > CONFIG.VIRTUALIZATION_THRESHOLD
|
||||
|
||||
const flatRows = useMemo(() => {
|
||||
if (!useVirtualization) return []
|
||||
return flattenTree(data, expandedPaths, pathToMatchIndices, isError)
|
||||
}, [data, expandedPaths, pathToMatchIndices, isError, useVirtualization])
|
||||
|
||||
// Scroll to match (virtualized)
|
||||
useEffect(() => {
|
||||
if (!useVirtualization || allMatchPaths.length === 0 || !listRef.current) return
|
||||
|
||||
const currentPath = allMatchPaths[currentMatchIndex]
|
||||
const targetPath = currentPath.endsWith('.value') ? currentPath : `${currentPath}.value`
|
||||
const rowIndex = flatRows.findIndex((r) => r.path === targetPath || r.path === currentPath)
|
||||
|
||||
if (rowIndex !== -1) {
|
||||
listRef.current.scrollToRow({ index: rowIndex, align: 'center' })
|
||||
}
|
||||
}, [currentMatchIndex, allMatchPaths, flatRows, listRef, useVirtualization])
|
||||
|
||||
// Scroll to match (non-virtualized)
|
||||
useEffect(() => {
|
||||
if (useVirtualization || allMatchPaths.length === 0) return
|
||||
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
const match = internalRef.current?.querySelector(
|
||||
`[data-match-index="${currentMatchIndex}"]`
|
||||
) as HTMLElement | null
|
||||
match?.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
})
|
||||
|
||||
return () => cancelAnimationFrame(rafId)
|
||||
}, [currentMatchIndex, allMatchPaths.length, expandedPaths, useVirtualization])
|
||||
|
||||
const containerClass = cn('flex flex-col pl-[20px]', wrapText && 'overflow-x-hidden', className)
|
||||
const virtualizedContainerClass = cn('relative', wrapText && 'overflow-x-hidden', className)
|
||||
const listClass = wrapText ? 'overflow-x-hidden' : 'overflow-x-auto'
|
||||
|
||||
// Running state
|
||||
if (isRunning && data === undefined) {
|
||||
return (
|
||||
<div ref={setContainerRef} className={containerClass}>
|
||||
<div className={STYLES.row}>
|
||||
<span className={STYLES.keyName}>running</span>
|
||||
<Badge variant='green' className={STYLES.badge}>
|
||||
Running
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if (rootEntries.length === 0 && !isError) {
|
||||
return (
|
||||
<div ref={setContainerRef} className={containerClass}>
|
||||
<span className={STYLES.emptyValue}>null</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Virtualized rendering
|
||||
if (useVirtualization) {
|
||||
return (
|
||||
<div
|
||||
ref={setContainerRef}
|
||||
className={virtualizedContainerClass}
|
||||
style={{ height: containerHeight }}
|
||||
>
|
||||
<List
|
||||
listRef={listRef}
|
||||
defaultHeight={containerHeight}
|
||||
rowCount={flatRows.length}
|
||||
rowHeight={CONFIG.ROW_HEIGHT}
|
||||
rowComponent={VirtualizedRow}
|
||||
rowProps={{
|
||||
rows: flatRows,
|
||||
onToggle: handleToggle,
|
||||
wrapText,
|
||||
searchQuery: searchQuery ?? '',
|
||||
currentMatchIndex,
|
||||
}}
|
||||
overscanCount={CONFIG.OVERSCAN_COUNT}
|
||||
className={listClass}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Non-virtualized rendering (preserves exact original styling)
|
||||
if (isError) {
|
||||
return (
|
||||
<SearchContext.Provider value={searchContextValue}>
|
||||
<div ref={setContainerRef} className={containerClass}>
|
||||
<StructuredNode
|
||||
name='error'
|
||||
value={extractErrorMessage(data)}
|
||||
path='root.error'
|
||||
expandedPaths={expandedPaths}
|
||||
onToggle={handleToggle}
|
||||
wrapText={wrapText}
|
||||
currentMatchIndex={currentMatchIndex}
|
||||
isError
|
||||
/>
|
||||
</div>
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchContext.Provider value={searchContextValue}>
|
||||
<div ref={setContainerRef} className={containerClass}>
|
||||
{rootEntries.map((entry) => (
|
||||
<StructuredNode
|
||||
key={entry.path}
|
||||
name={entry.key}
|
||||
value={entry.value}
|
||||
path={entry.path}
|
||||
expandedPaths={expandedPaths}
|
||||
onToggle={handleToggle}
|
||||
wrapText={wrapText}
|
||||
currentMatchIndex={currentMatchIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SearchContext.Provider>
|
||||
)
|
||||
})
|
||||
@@ -1,4 +0,0 @@
|
||||
export { OutputContextMenu, type OutputContextMenuProps } from './components/output-context-menu'
|
||||
export { StructuredOutput, type StructuredOutputProps } from './components/structured-output'
|
||||
export type { OutputPanelProps } from './output-panel'
|
||||
export { OutputPanel } from './output-panel'
|
||||
@@ -1,643 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowDownToLine,
|
||||
ArrowUp,
|
||||
Check,
|
||||
Clipboard,
|
||||
Database,
|
||||
MoreHorizontal,
|
||||
Palette,
|
||||
Pause,
|
||||
Search,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Button,
|
||||
Code,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverItem,
|
||||
PopoverTrigger,
|
||||
Tooltip,
|
||||
} from '@/components/emcn'
|
||||
import { FilterPopover } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/filter-popover'
|
||||
import { OutputContextMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/components/output-context-menu'
|
||||
import { StructuredOutput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/output-panel/components/structured-output'
|
||||
import { ToggleButton } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/components/toggle-button'
|
||||
import type {
|
||||
BlockInfo,
|
||||
TerminalFilters,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
|
||||
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
|
||||
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
|
||||
import type { ConsoleEntry } from '@/stores/terminal'
|
||||
import { useTerminalStore } from '@/stores/terminal'
|
||||
|
||||
interface OutputCodeContentProps {
|
||||
code: string
|
||||
language: 'javascript' | 'json'
|
||||
wrapText: boolean
|
||||
searchQuery: string | undefined
|
||||
currentMatchIndex: number
|
||||
onMatchCountChange: (count: number) => void
|
||||
contentRef: React.RefObject<HTMLDivElement | null>
|
||||
}
|
||||
|
||||
const OutputCodeContent = React.memo(function OutputCodeContent({
|
||||
code,
|
||||
language,
|
||||
wrapText,
|
||||
searchQuery,
|
||||
currentMatchIndex,
|
||||
onMatchCountChange,
|
||||
contentRef,
|
||||
}: OutputCodeContentProps) {
|
||||
return (
|
||||
<Code.Viewer
|
||||
code={code}
|
||||
showGutter
|
||||
language={language}
|
||||
className='m-0 min-h-full rounded-none border-0 bg-[var(--surface-1)] dark:bg-[var(--surface-1)]'
|
||||
paddingLeft={8}
|
||||
gutterStyle={{ backgroundColor: 'transparent' }}
|
||||
wrapText={wrapText}
|
||||
searchQuery={searchQuery}
|
||||
currentMatchIndex={currentMatchIndex}
|
||||
onMatchCountChange={onMatchCountChange}
|
||||
contentRef={contentRef}
|
||||
virtualized
|
||||
showCollapseColumn={language === 'json'}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Props for the OutputPanel component
|
||||
* Store-backed settings (wrapText, openOnRun, structuredView, outputPanelWidth)
|
||||
* are accessed directly from useTerminalStore to reduce prop drilling.
|
||||
*/
|
||||
export interface OutputPanelProps {
|
||||
selectedEntry: ConsoleEntry
|
||||
handleOutputPanelResizeMouseDown: (e: React.MouseEvent) => void
|
||||
handleHeaderClick: () => void
|
||||
isExpanded: boolean
|
||||
expandToLastHeight: () => void
|
||||
showInput: boolean
|
||||
setShowInput: (show: boolean) => void
|
||||
hasInputData: boolean
|
||||
isPlaygroundEnabled: boolean
|
||||
shouldShowTrainingButton: boolean
|
||||
isTraining: boolean
|
||||
handleTrainingClick: (e: React.MouseEvent) => void
|
||||
showCopySuccess: boolean
|
||||
handleCopy: () => void
|
||||
filteredEntries: ConsoleEntry[]
|
||||
handleExportConsole: (e: React.MouseEvent) => void
|
||||
hasActiveFilters: boolean
|
||||
handleClearConsole: (e: React.MouseEvent) => void
|
||||
shouldShowCodeDisplay: boolean
|
||||
outputDataStringified: string
|
||||
outputData: unknown
|
||||
handleClearConsoleFromMenu: () => void
|
||||
filters: TerminalFilters
|
||||
toggleBlock: (blockId: string) => void
|
||||
toggleStatus: (status: 'error' | 'info') => void
|
||||
uniqueBlocks: BlockInfo[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Output panel component that manages its own search state.
|
||||
* Accesses store-backed settings directly to reduce prop drilling.
|
||||
*/
|
||||
export const OutputPanel = React.memo(function OutputPanel({
|
||||
selectedEntry,
|
||||
handleOutputPanelResizeMouseDown,
|
||||
handleHeaderClick,
|
||||
isExpanded,
|
||||
expandToLastHeight,
|
||||
showInput,
|
||||
setShowInput,
|
||||
hasInputData,
|
||||
isPlaygroundEnabled,
|
||||
shouldShowTrainingButton,
|
||||
isTraining,
|
||||
handleTrainingClick,
|
||||
showCopySuccess,
|
||||
handleCopy,
|
||||
filteredEntries,
|
||||
handleExportConsole,
|
||||
hasActiveFilters,
|
||||
handleClearConsole,
|
||||
shouldShowCodeDisplay,
|
||||
outputDataStringified,
|
||||
outputData,
|
||||
handleClearConsoleFromMenu,
|
||||
filters,
|
||||
toggleBlock,
|
||||
toggleStatus,
|
||||
uniqueBlocks,
|
||||
}: OutputPanelProps) {
|
||||
// Access store-backed settings directly to reduce prop drilling
|
||||
const outputPanelWidth = useTerminalStore((state) => state.outputPanelWidth)
|
||||
const wrapText = useTerminalStore((state) => state.wrapText)
|
||||
const setWrapText = useTerminalStore((state) => state.setWrapText)
|
||||
const openOnRun = useTerminalStore((state) => state.openOnRun)
|
||||
const setOpenOnRun = useTerminalStore((state) => state.setOpenOnRun)
|
||||
const structuredView = useTerminalStore((state) => state.structuredView)
|
||||
const setStructuredView = useTerminalStore((state) => state.setStructuredView)
|
||||
|
||||
const outputContentRef = useRef<HTMLDivElement>(null)
|
||||
const [filtersOpen, setFiltersOpen] = useState(false)
|
||||
const [outputOptionsOpen, setOutputOptionsOpen] = useState(false)
|
||||
const {
|
||||
isSearchActive: isOutputSearchActive,
|
||||
searchQuery: outputSearchQuery,
|
||||
setSearchQuery: setOutputSearchQuery,
|
||||
matchCount,
|
||||
currentMatchIndex,
|
||||
activateSearch: activateOutputSearch,
|
||||
closeSearch: closeOutputSearch,
|
||||
goToNextMatch,
|
||||
goToPreviousMatch,
|
||||
handleMatchCountChange,
|
||||
searchInputRef: outputSearchInputRef,
|
||||
} = useCodeViewerFeatures({
|
||||
contentRef: outputContentRef,
|
||||
externalWrapText: wrapText,
|
||||
onWrapTextChange: setWrapText,
|
||||
})
|
||||
|
||||
// Context menu state for output panel
|
||||
const [hasSelection, setHasSelection] = useState(false)
|
||||
const [storedSelectionText, setStoredSelectionText] = useState('')
|
||||
const {
|
||||
isOpen: isOutputMenuOpen,
|
||||
position: outputMenuPosition,
|
||||
menuRef: outputMenuRef,
|
||||
handleContextMenu: handleOutputContextMenu,
|
||||
closeMenu: closeOutputMenu,
|
||||
} = useContextMenu()
|
||||
|
||||
const handleOutputPanelContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
const selection = window.getSelection()
|
||||
const selectionText = selection?.toString() || ''
|
||||
setStoredSelectionText(selectionText)
|
||||
setHasSelection(selectionText.length > 0)
|
||||
handleOutputContextMenu(e)
|
||||
},
|
||||
[handleOutputContextMenu]
|
||||
)
|
||||
|
||||
const handleCopySelection = useCallback(() => {
|
||||
if (storedSelectionText) {
|
||||
navigator.clipboard.writeText(storedSelectionText)
|
||||
}
|
||||
}, [storedSelectionText])
|
||||
|
||||
// Memoized callbacks to avoid inline arrow functions
|
||||
const handleToggleStructuredView = useCallback(() => {
|
||||
setStructuredView(!structuredView)
|
||||
}, [structuredView, setStructuredView])
|
||||
|
||||
const handleToggleWrapText = useCallback(() => {
|
||||
setWrapText(!wrapText)
|
||||
}, [wrapText, setWrapText])
|
||||
|
||||
const handleToggleOpenOnRun = useCallback(() => {
|
||||
setOpenOnRun(!openOnRun)
|
||||
}, [openOnRun, setOpenOnRun])
|
||||
|
||||
const handleCopyClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
handleCopy()
|
||||
},
|
||||
[handleCopy]
|
||||
)
|
||||
|
||||
const handleSearchClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
activateOutputSearch()
|
||||
},
|
||||
[activateOutputSearch]
|
||||
)
|
||||
|
||||
const handleCloseSearchClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
closeOutputSearch()
|
||||
},
|
||||
[closeOutputSearch]
|
||||
)
|
||||
|
||||
const handleOutputButtonClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!isExpanded) {
|
||||
expandToLastHeight()
|
||||
}
|
||||
if (showInput) setShowInput(false)
|
||||
},
|
||||
[isExpanded, expandToLastHeight, showInput, setShowInput]
|
||||
)
|
||||
|
||||
const handleInputButtonClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!isExpanded) {
|
||||
expandToLastHeight()
|
||||
}
|
||||
setShowInput(true)
|
||||
},
|
||||
[isExpanded, expandToLastHeight, setShowInput]
|
||||
)
|
||||
|
||||
const handleToggleButtonClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
handleHeaderClick()
|
||||
},
|
||||
[handleHeaderClick]
|
||||
)
|
||||
|
||||
/**
|
||||
* Track text selection state for context menu.
|
||||
* Skip updates when the context menu is open to prevent the selection
|
||||
* state from changing mid-click (which would disable the copy button).
|
||||
*/
|
||||
useEffect(() => {
|
||||
const handleSelectionChange = () => {
|
||||
if (isOutputMenuOpen) return
|
||||
|
||||
const selection = window.getSelection()
|
||||
setHasSelection(Boolean(selection && selection.toString().length > 0))
|
||||
}
|
||||
|
||||
document.addEventListener('selectionchange', handleSelectionChange)
|
||||
return () => document.removeEventListener('selectionchange', handleSelectionChange)
|
||||
}, [isOutputMenuOpen])
|
||||
|
||||
// Memoize the search query for structured output to avoid re-renders
|
||||
const structuredSearchQuery = useMemo(
|
||||
() => (isOutputSearchActive ? outputSearchQuery : undefined),
|
||||
[isOutputSearchActive, outputSearchQuery]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className='absolute top-0 right-0 bottom-0 flex flex-col border-[var(--border)] border-l bg-[var(--surface-1)]'
|
||||
style={{ width: `${outputPanelWidth}px` }}
|
||||
>
|
||||
{/* Horizontal Resize Handle */}
|
||||
<div
|
||||
className='-ml-[4px] absolute top-0 bottom-0 left-0 z-20 w-[8px] cursor-ew-resize'
|
||||
onMouseDown={handleOutputPanelResizeMouseDown}
|
||||
role='separator'
|
||||
aria-label='Resize output panel'
|
||||
aria-orientation='vertical'
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div
|
||||
className='group flex h-[30px] flex-shrink-0 cursor-pointer items-center justify-between bg-[var(--surface-1)] pr-[16px] pl-[10px]'
|
||||
onClick={handleHeaderClick}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className={clsx(
|
||||
'px-[8px] py-[6px] text-[12px]',
|
||||
!showInput ? '!text-[var(--text-primary)]' : '!text-[var(--text-tertiary)]'
|
||||
)}
|
||||
onClick={handleOutputButtonClick}
|
||||
aria-label='Show output'
|
||||
>
|
||||
Output
|
||||
</Button>
|
||||
{hasInputData && (
|
||||
<Button
|
||||
variant='ghost'
|
||||
className={clsx(
|
||||
'px-[8px] py-[6px] text-[12px]',
|
||||
showInput ? '!text-[var(--text-primary)]' : '!text-[var(--text-tertiary)]'
|
||||
)}
|
||||
onClick={handleInputButtonClick}
|
||||
aria-label='Show input'
|
||||
>
|
||||
Input
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center gap-[8px]'>
|
||||
{/* Unified filter popover */}
|
||||
{filteredEntries.length > 0 && (
|
||||
<FilterPopover
|
||||
open={filtersOpen}
|
||||
onOpenChange={setFiltersOpen}
|
||||
filters={filters}
|
||||
toggleStatus={toggleStatus}
|
||||
toggleBlock={toggleBlock}
|
||||
uniqueBlocks={uniqueBlocks}
|
||||
hasActiveFilters={hasActiveFilters}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isOutputSearchActive ? (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleCloseSearchClick}
|
||||
aria-label='Close search'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<X className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Close search</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
) : (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleSearchClick}
|
||||
aria-label='Search in output'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Search className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Search</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{isPlaygroundEnabled && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Link href='/playground'>
|
||||
<Button
|
||||
variant='ghost'
|
||||
aria-label='Component Playground'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Palette className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
</Link>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Component Playground</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{shouldShowTrainingButton && (
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleTrainingClick}
|
||||
aria-label={isTraining ? 'Stop training' : 'Train Copilot'}
|
||||
className={clsx(
|
||||
'!p-1.5 -m-1.5',
|
||||
isTraining && 'text-orange-600 dark:text-orange-400'
|
||||
)}
|
||||
>
|
||||
{isTraining ? (
|
||||
<Pause className='h-[12px] w-[12px]' />
|
||||
) : (
|
||||
<Database className='h-[12px] w-[12px]' />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>{isTraining ? 'Stop Training' : 'Train Copilot'}</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleCopyClick}
|
||||
aria-label='Copy output'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
{showCopySuccess ? (
|
||||
<Check className='h-[12px] w-[12px]' />
|
||||
) : (
|
||||
<Clipboard className='h-[12px] w-[12px]' />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>{showCopySuccess ? 'Copied' : 'Copy output'}</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
{filteredEntries.length > 0 && (
|
||||
<>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleExportConsole}
|
||||
aria-label='Download console CSV'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<ArrowDownToLine className='h-3 w-3' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<span>Download CSV</span>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={handleClearConsole}
|
||||
aria-label='Clear console'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<Trash2 className='h-3 w-3' />
|
||||
</Button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content>
|
||||
<Tooltip.Shortcut keys='⌘D'>Clear console</Tooltip.Shortcut>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
</>
|
||||
)}
|
||||
<Popover open={outputOptionsOpen} onOpenChange={setOutputOptionsOpen} size='sm'>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
aria-label='Terminal options'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<MoreHorizontal className='h-3.5 w-3.5' />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side='bottom'
|
||||
align='end'
|
||||
sideOffset={4}
|
||||
collisionPadding={0}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{ minWidth: '140px', maxWidth: '160px' }}
|
||||
className='gap-[2px]'
|
||||
>
|
||||
<PopoverItem
|
||||
active={structuredView}
|
||||
showCheck={structuredView}
|
||||
onClick={handleToggleStructuredView}
|
||||
>
|
||||
<span>Structured view</span>
|
||||
</PopoverItem>
|
||||
<PopoverItem active={wrapText} showCheck={wrapText} onClick={handleToggleWrapText}>
|
||||
<span>Wrap text</span>
|
||||
</PopoverItem>
|
||||
<PopoverItem
|
||||
active={openOnRun}
|
||||
showCheck={openOnRun}
|
||||
onClick={handleToggleOpenOnRun}
|
||||
>
|
||||
<span>Open on run</span>
|
||||
</PopoverItem>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<ToggleButton isExpanded={isExpanded} onClick={handleToggleButtonClick} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search Overlay */}
|
||||
{isOutputSearchActive && (
|
||||
<div
|
||||
className='absolute top-[30px] right-[8px] z-30 flex h-[34px] items-center gap-[6px] rounded-b-[4px] border border-[var(--border)] border-t-0 bg-[var(--surface-1)] px-[6px] shadow-sm'
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-toolbar-root
|
||||
data-search-active='true'
|
||||
>
|
||||
<Input
|
||||
ref={outputSearchInputRef}
|
||||
type='text'
|
||||
value={outputSearchQuery}
|
||||
onChange={(e) => setOutputSearchQuery(e.target.value)}
|
||||
placeholder='Search...'
|
||||
className='mr-[2px] h-[23px] w-[94px] text-[12px]'
|
||||
/>
|
||||
<span
|
||||
className={clsx(
|
||||
'w-[58px] font-medium text-[11px]',
|
||||
matchCount > 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]'
|
||||
)}
|
||||
>
|
||||
{matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : 'No results'}
|
||||
</span>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={goToPreviousMatch}
|
||||
aria-label='Previous match'
|
||||
className='!p-1.5 -m-1.5'
|
||||
disabled={matchCount === 0}
|
||||
>
|
||||
<ArrowUp className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={goToNextMatch}
|
||||
aria-label='Next match'
|
||||
className='!p-1.5 -m-1.5'
|
||||
disabled={matchCount === 0}
|
||||
>
|
||||
<ArrowDown className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={closeOutputSearch}
|
||||
aria-label='Close search'
|
||||
className='!p-1.5 -m-1.5'
|
||||
>
|
||||
<X className='h-[12px] w-[12px]' />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
className={clsx('flex-1 overflow-y-auto', !wrapText && 'overflow-x-auto')}
|
||||
onContextMenu={handleOutputPanelContextMenu}
|
||||
>
|
||||
{shouldShowCodeDisplay ? (
|
||||
<OutputCodeContent
|
||||
code={selectedEntry.input.code}
|
||||
language={(selectedEntry.input.language as 'javascript' | 'json') || 'javascript'}
|
||||
wrapText={wrapText}
|
||||
searchQuery={structuredSearchQuery}
|
||||
currentMatchIndex={currentMatchIndex}
|
||||
onMatchCountChange={handleMatchCountChange}
|
||||
contentRef={outputContentRef}
|
||||
/>
|
||||
) : structuredView ? (
|
||||
<StructuredOutput
|
||||
data={outputData}
|
||||
wrapText={wrapText}
|
||||
isError={!showInput && Boolean(selectedEntry.error)}
|
||||
isRunning={!showInput && Boolean(selectedEntry.isRunning)}
|
||||
className='min-h-full'
|
||||
searchQuery={structuredSearchQuery}
|
||||
currentMatchIndex={currentMatchIndex}
|
||||
onMatchCountChange={handleMatchCountChange}
|
||||
contentRef={outputContentRef}
|
||||
/>
|
||||
) : (
|
||||
<OutputCodeContent
|
||||
code={outputDataStringified}
|
||||
language='json'
|
||||
wrapText={wrapText}
|
||||
searchQuery={structuredSearchQuery}
|
||||
currentMatchIndex={currentMatchIndex}
|
||||
onMatchCountChange={handleMatchCountChange}
|
||||
contentRef={outputContentRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Output Panel Context Menu */}
|
||||
<OutputContextMenu
|
||||
isOpen={isOutputMenuOpen}
|
||||
position={outputMenuPosition}
|
||||
menuRef={outputMenuRef}
|
||||
onClose={closeOutputMenu}
|
||||
onCopySelection={handleCopySelection}
|
||||
onCopyAll={handleCopy}
|
||||
onSearch={activateOutputSearch}
|
||||
structuredView={structuredView}
|
||||
onToggleStructuredView={handleToggleStructuredView}
|
||||
wrapText={wrapText}
|
||||
onToggleWrap={handleToggleWrapText}
|
||||
openOnRun={openOnRun}
|
||||
onToggleOpenOnRun={handleToggleOpenOnRun}
|
||||
onClearConsole={handleClearConsoleFromMenu}
|
||||
hasSelection={hasSelection}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
export { RunningBadge, StatusDisplay, type StatusDisplayProps } from './status-display'
|
||||
@@ -1,43 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { memo } from 'react'
|
||||
import { Badge } from '@/components/emcn'
|
||||
import { BADGE_STYLE } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
|
||||
|
||||
/**
|
||||
* Running badge component - displays a consistent "Running" indicator
|
||||
*/
|
||||
export const RunningBadge = memo(function RunningBadge() {
|
||||
return (
|
||||
<Badge variant='green' className={BADGE_STYLE}>
|
||||
Running
|
||||
</Badge>
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Props for StatusDisplay component
|
||||
*/
|
||||
export interface StatusDisplayProps {
|
||||
isRunning: boolean
|
||||
isCanceled: boolean
|
||||
formattedDuration: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Reusable status display for terminal rows.
|
||||
* Shows Running badge, 'canceled' text, or formatted duration.
|
||||
*/
|
||||
export const StatusDisplay = memo(function StatusDisplay({
|
||||
isRunning,
|
||||
isCanceled,
|
||||
formattedDuration,
|
||||
}: StatusDisplayProps) {
|
||||
if (isRunning) {
|
||||
return <RunningBadge />
|
||||
}
|
||||
if (isCanceled) {
|
||||
return <>canceled</>
|
||||
}
|
||||
return <>{formattedDuration}</>
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
export { ToggleButton, type ToggleButtonProps } from './toggle-button'
|
||||
@@ -1,33 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import { memo } from 'react'
|
||||
import clsx from 'clsx'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { Button } from '@/components/emcn'
|
||||
|
||||
export interface ToggleButtonProps {
|
||||
isExpanded: boolean
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle button component for terminal expand/collapse
|
||||
*/
|
||||
export const ToggleButton = memo(function ToggleButton({ isExpanded, onClick }: ToggleButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='!p-1.5 -m-1.5'
|
||||
onClick={onClick}
|
||||
aria-label='Toggle terminal'
|
||||
>
|
||||
<ChevronDown
|
||||
className={clsx(
|
||||
'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-100',
|
||||
!isExpanded && 'rotate-180'
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
})
|
||||
@@ -1,4 +1,3 @@
|
||||
export type { SortConfig, SortDirection, SortField, TerminalFilters } from '../types'
|
||||
export { useOutputPanelResize } from './use-output-panel-resize'
|
||||
export { useTerminalFilters } from './use-terminal-filters'
|
||||
export { useTerminalResize } from './use-terminal-resize'
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { OUTPUT_PANEL_WIDTH, TERMINAL_BLOCK_COLUMN_WIDTH } from '@/stores/constants'
|
||||
import { OUTPUT_PANEL_WIDTH } from '@/stores/constants'
|
||||
import { useTerminalStore } from '@/stores/terminal'
|
||||
|
||||
const BLOCK_COLUMN_WIDTH = 240
|
||||
|
||||
export function useOutputPanelResize() {
|
||||
const setOutputPanelWidth = useTerminalStore((state) => state.setOutputPanelWidth)
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
@@ -23,7 +25,7 @@ export function useOutputPanelResize() {
|
||||
|
||||
const newWidth = window.innerWidth - e.clientX - panelWidth
|
||||
const terminalWidth = window.innerWidth - sidebarWidth - panelWidth
|
||||
const maxWidth = terminalWidth - TERMINAL_BLOCK_COLUMN_WIDTH
|
||||
const maxWidth = terminalWidth - BLOCK_COLUMN_WIDTH
|
||||
const clampedWidth = Math.max(OUTPUT_PANEL_WIDTH.MIN, Math.min(newWidth, maxWidth))
|
||||
|
||||
setOutputPanelWidth(clampedWidth)
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import type {
|
||||
SortConfig,
|
||||
TerminalFilters,
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/types'
|
||||
import type { ConsoleEntry } from '@/stores/terminal'
|
||||
|
||||
/**
|
||||
* Sort configuration
|
||||
*/
|
||||
export type SortField = 'timestamp'
|
||||
export type SortDirection = 'asc' | 'desc'
|
||||
|
||||
export interface SortConfig {
|
||||
field: SortField
|
||||
direction: SortDirection
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter configuration state
|
||||
*/
|
||||
export interface TerminalFilters {
|
||||
blockIds: Set<string>
|
||||
statuses: Set<'error' | 'info'>
|
||||
runIds: Set<string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook to manage terminal filters and sorting.
|
||||
* Provides filter state, sort state, and filtering/sorting logic for console entries.
|
||||
@@ -15,6 +31,7 @@ export function useTerminalFilters() {
|
||||
const [filters, setFilters] = useState<TerminalFilters>({
|
||||
blockIds: new Set(),
|
||||
statuses: new Set(),
|
||||
runIds: new Set(),
|
||||
})
|
||||
|
||||
const [sortConfig, setSortConfig] = useState<SortConfig>({
|
||||
@@ -52,6 +69,21 @@ export function useTerminalFilters() {
|
||||
})
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Toggles a run ID filter
|
||||
*/
|
||||
const toggleRunId = useCallback((runId: string) => {
|
||||
setFilters((prev) => {
|
||||
const newRunIds = new Set(prev.runIds)
|
||||
if (newRunIds.has(runId)) {
|
||||
newRunIds.delete(runId)
|
||||
} else {
|
||||
newRunIds.add(runId)
|
||||
}
|
||||
return { ...prev, runIds: newRunIds }
|
||||
})
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Toggles sort direction between ascending and descending
|
||||
*/
|
||||
@@ -69,6 +101,7 @@ export function useTerminalFilters() {
|
||||
setFilters({
|
||||
blockIds: new Set(),
|
||||
statuses: new Set(),
|
||||
runIds: new Set(),
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -76,7 +109,7 @@ export function useTerminalFilters() {
|
||||
* Checks if any filters are active
|
||||
*/
|
||||
const hasActiveFilters = useMemo(() => {
|
||||
return filters.blockIds.size > 0 || filters.statuses.size > 0
|
||||
return filters.blockIds.size > 0 || filters.statuses.size > 0 || filters.runIds.size > 0
|
||||
}, [filters])
|
||||
|
||||
/**
|
||||
@@ -101,6 +134,14 @@ export function useTerminalFilters() {
|
||||
if (!hasStatus) return false
|
||||
}
|
||||
|
||||
// Run ID filter
|
||||
if (
|
||||
filters.runIds.size > 0 &&
|
||||
(!entry.executionId || !filters.runIds.has(entry.executionId))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -123,6 +164,7 @@ export function useTerminalFilters() {
|
||||
sortConfig,
|
||||
toggleBlock,
|
||||
toggleStatus,
|
||||
toggleRunId,
|
||||
toggleSort,
|
||||
clearFilters,
|
||||
hasActiveFilters,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Terminal filter configuration state
|
||||
*/
|
||||
export interface TerminalFilters {
|
||||
blockIds: Set<string>
|
||||
statuses: Set<'error' | 'info'>
|
||||
}
|
||||
|
||||
/**
|
||||
* Context menu position for positioning floating menus
|
||||
*/
|
||||
export interface ContextMenuPosition {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort field options for terminal entries
|
||||
*/
|
||||
export type SortField = 'timestamp'
|
||||
|
||||
/**
|
||||
* Sort direction options
|
||||
*/
|
||||
export type SortDirection = 'asc' | 'desc'
|
||||
|
||||
/**
|
||||
* Sort configuration for terminal entries
|
||||
*/
|
||||
export interface SortConfig {
|
||||
field: SortField
|
||||
direction: SortDirection
|
||||
}
|
||||
|
||||
/**
|
||||
* Status type for console entries
|
||||
*/
|
||||
export type EntryStatus = 'error' | 'info'
|
||||
|
||||
/**
|
||||
* Block information for filters
|
||||
*/
|
||||
export interface BlockInfo {
|
||||
blockId: string
|
||||
blockName: string
|
||||
blockType: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Common row styling classes for terminal components
|
||||
*/
|
||||
export const ROW_STYLES = {
|
||||
base: 'group flex cursor-pointer items-center justify-between gap-[8px] rounded-[8px] px-[6px]',
|
||||
selected: 'bg-[var(--surface-6)] dark:bg-[var(--surface-5)]',
|
||||
hover: 'hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-5)]',
|
||||
nested:
|
||||
'mt-[2px] ml-[3px] flex min-w-0 flex-col gap-[2px] border-[var(--border)] border-l pl-[9px]',
|
||||
iconButton: '!p-1.5 -m-1.5',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Common badge styling for status badges
|
||||
*/
|
||||
export const BADGE_STYLE = 'rounded-[4px] px-[4px] py-[0px] text-[11px]'
|
||||
@@ -1,452 +0,0 @@
|
||||
import type React from 'react'
|
||||
import { RepeatIcon, SplitIcon } from 'lucide-react'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { TERMINAL_BLOCK_COLUMN_WIDTH } from '@/stores/constants'
|
||||
import type { ConsoleEntry } from '@/stores/terminal'
|
||||
|
||||
/**
|
||||
* Subflow colors matching the subflow tool configs
|
||||
*/
|
||||
const SUBFLOW_COLORS = {
|
||||
loop: '#2FB3FF',
|
||||
parallel: '#FEE12B',
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Retrieves the icon component for a given block type
|
||||
*/
|
||||
export function getBlockIcon(
|
||||
blockType: string
|
||||
): React.ComponentType<{ className?: string }> | null {
|
||||
const blockConfig = getBlock(blockType)
|
||||
|
||||
if (blockConfig?.icon) {
|
||||
return blockConfig.icon
|
||||
}
|
||||
|
||||
if (blockType === 'loop') {
|
||||
return RepeatIcon
|
||||
}
|
||||
|
||||
if (blockType === 'parallel') {
|
||||
return SplitIcon
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the background color for a block type
|
||||
*/
|
||||
export function getBlockColor(blockType: string): string {
|
||||
const blockConfig = getBlock(blockType)
|
||||
if (blockConfig?.bgColor) {
|
||||
return blockConfig.bgColor
|
||||
}
|
||||
// Use proper subflow colors matching the toolbar configs
|
||||
if (blockType === 'loop') {
|
||||
return SUBFLOW_COLORS.loop
|
||||
}
|
||||
if (blockType === 'parallel') {
|
||||
return SUBFLOW_COLORS.parallel
|
||||
}
|
||||
return '#6b7280'
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats duration from milliseconds to readable format
|
||||
*/
|
||||
export function formatDuration(ms?: number): string {
|
||||
if (ms === undefined || ms === null) return '-'
|
||||
if (ms < 1000) return `${ms}ms`
|
||||
return `${(ms / 1000).toFixed(2)}s`
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a keyboard event originated from a text-editable element
|
||||
*/
|
||||
export function isEventFromEditableElement(e: KeyboardEvent): boolean {
|
||||
const target = e.target as HTMLElement | null
|
||||
if (!target) return false
|
||||
|
||||
const isEditable = (el: HTMLElement | null): boolean => {
|
||||
if (!el) return false
|
||||
if (el instanceof HTMLInputElement) return true
|
||||
if (el instanceof HTMLTextAreaElement) return true
|
||||
if ((el as HTMLElement).isContentEditable) return true
|
||||
const role = el.getAttribute('role')
|
||||
if (role === 'textbox' || role === 'combobox') return true
|
||||
return false
|
||||
}
|
||||
|
||||
let el: HTMLElement | null = target
|
||||
while (el) {
|
||||
if (isEditable(el)) return true
|
||||
el = el.parentElement
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block type is a subflow (loop or parallel)
|
||||
*/
|
||||
export function isSubflowBlockType(blockType: string): boolean {
|
||||
const lower = blockType?.toLowerCase() || ''
|
||||
return lower === 'loop' || lower === 'parallel'
|
||||
}
|
||||
|
||||
/**
|
||||
* Node type for the tree structure
|
||||
*/
|
||||
export type EntryNodeType = 'block' | 'subflow' | 'iteration'
|
||||
|
||||
/**
|
||||
* Entry node for tree structure - represents a block, subflow, or iteration
|
||||
*/
|
||||
export interface EntryNode {
|
||||
/** The console entry (for blocks) or synthetic entry (for subflows/iterations) */
|
||||
entry: ConsoleEntry
|
||||
/** Child nodes */
|
||||
children: EntryNode[]
|
||||
/** Node type */
|
||||
nodeType: EntryNodeType
|
||||
/** Iteration info for iteration nodes */
|
||||
iterationInfo?: {
|
||||
current: number
|
||||
total?: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution group interface for grouping entries by execution
|
||||
*/
|
||||
export interface ExecutionGroup {
|
||||
executionId: string
|
||||
startTime: string
|
||||
endTime: string
|
||||
startTimeMs: number
|
||||
endTimeMs: number
|
||||
duration: number
|
||||
status: 'success' | 'error'
|
||||
/** Flat list of entries (legacy, kept for filters) */
|
||||
entries: ConsoleEntry[]
|
||||
/** Tree structure of entry nodes for nested display */
|
||||
entryTree: EntryNode[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Iteration group for grouping blocks within the same iteration
|
||||
*/
|
||||
interface IterationGroup {
|
||||
iterationType: string
|
||||
iterationCurrent: number
|
||||
iterationTotal?: number
|
||||
blocks: ConsoleEntry[]
|
||||
startTimeMs: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a tree structure from flat entries.
|
||||
* Groups iteration entries by (iterationType, iterationCurrent), showing all blocks
|
||||
* that executed within each iteration.
|
||||
* Sorts by start time to ensure chronological order.
|
||||
*/
|
||||
function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
|
||||
// Separate regular blocks from iteration entries
|
||||
const regularBlocks: ConsoleEntry[] = []
|
||||
const iterationEntries: ConsoleEntry[] = []
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.iterationType && entry.iterationCurrent !== undefined) {
|
||||
iterationEntries.push(entry)
|
||||
} else {
|
||||
regularBlocks.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Group iteration entries by (iterationType, iterationCurrent)
|
||||
const iterationGroupsMap = new Map<string, IterationGroup>()
|
||||
for (const entry of iterationEntries) {
|
||||
const key = `${entry.iterationType}-${entry.iterationCurrent}`
|
||||
let group = iterationGroupsMap.get(key)
|
||||
const entryStartMs = new Date(entry.startedAt || entry.timestamp).getTime()
|
||||
|
||||
if (!group) {
|
||||
group = {
|
||||
iterationType: entry.iterationType!,
|
||||
iterationCurrent: entry.iterationCurrent!,
|
||||
iterationTotal: entry.iterationTotal,
|
||||
blocks: [],
|
||||
startTimeMs: entryStartMs,
|
||||
}
|
||||
iterationGroupsMap.set(key, group)
|
||||
} else {
|
||||
// Update start time to earliest
|
||||
if (entryStartMs < group.startTimeMs) {
|
||||
group.startTimeMs = entryStartMs
|
||||
}
|
||||
// Update total if available
|
||||
if (entry.iterationTotal !== undefined) {
|
||||
group.iterationTotal = entry.iterationTotal
|
||||
}
|
||||
}
|
||||
group.blocks.push(entry)
|
||||
}
|
||||
|
||||
// Sort blocks within each iteration by start time ascending (oldest first, top-down)
|
||||
for (const group of iterationGroupsMap.values()) {
|
||||
group.blocks.sort((a, b) => {
|
||||
const aStart = new Date(a.startedAt || a.timestamp).getTime()
|
||||
const bStart = new Date(b.startedAt || b.timestamp).getTime()
|
||||
return aStart - bStart
|
||||
})
|
||||
}
|
||||
|
||||
// Group iterations by iterationType to create subflow parents
|
||||
const subflowGroups = new Map<string, IterationGroup[]>()
|
||||
for (const group of iterationGroupsMap.values()) {
|
||||
const type = group.iterationType
|
||||
let groups = subflowGroups.get(type)
|
||||
if (!groups) {
|
||||
groups = []
|
||||
subflowGroups.set(type, groups)
|
||||
}
|
||||
groups.push(group)
|
||||
}
|
||||
|
||||
// Sort iterations within each subflow by iteration number
|
||||
for (const groups of subflowGroups.values()) {
|
||||
groups.sort((a, b) => a.iterationCurrent - b.iterationCurrent)
|
||||
}
|
||||
|
||||
// Build subflow nodes with iteration children
|
||||
const subflowNodes: EntryNode[] = []
|
||||
for (const [iterationType, iterationGroups] of subflowGroups.entries()) {
|
||||
// Calculate subflow timing from all its iterations
|
||||
const firstIteration = iterationGroups[0]
|
||||
const allBlocks = iterationGroups.flatMap((g) => g.blocks)
|
||||
const subflowStartMs = Math.min(
|
||||
...allBlocks.map((b) => new Date(b.startedAt || b.timestamp).getTime())
|
||||
)
|
||||
const subflowEndMs = Math.max(
|
||||
...allBlocks.map((b) => new Date(b.endedAt || b.timestamp).getTime())
|
||||
)
|
||||
const totalDuration = allBlocks.reduce((sum, b) => sum + (b.durationMs || 0), 0)
|
||||
|
||||
// Create synthetic subflow parent entry
|
||||
const syntheticSubflow: ConsoleEntry = {
|
||||
id: `subflow-${iterationType}-${firstIteration.blocks[0]?.executionId || 'unknown'}`,
|
||||
timestamp: new Date(subflowStartMs).toISOString(),
|
||||
workflowId: firstIteration.blocks[0]?.workflowId || '',
|
||||
blockId: `${iterationType}-container`,
|
||||
blockName: iterationType.charAt(0).toUpperCase() + iterationType.slice(1),
|
||||
blockType: iterationType,
|
||||
executionId: firstIteration.blocks[0]?.executionId,
|
||||
startedAt: new Date(subflowStartMs).toISOString(),
|
||||
endedAt: new Date(subflowEndMs).toISOString(),
|
||||
durationMs: totalDuration,
|
||||
success: !allBlocks.some((b) => b.error),
|
||||
}
|
||||
|
||||
// Build iteration child nodes
|
||||
const iterationNodes: EntryNode[] = iterationGroups.map((iterGroup) => {
|
||||
// Create synthetic iteration entry
|
||||
const iterBlocks = iterGroup.blocks
|
||||
const iterStartMs = Math.min(
|
||||
...iterBlocks.map((b) => new Date(b.startedAt || b.timestamp).getTime())
|
||||
)
|
||||
const iterEndMs = Math.max(
|
||||
...iterBlocks.map((b) => new Date(b.endedAt || b.timestamp).getTime())
|
||||
)
|
||||
const iterDuration = iterBlocks.reduce((sum, b) => sum + (b.durationMs || 0), 0)
|
||||
|
||||
const syntheticIteration: ConsoleEntry = {
|
||||
id: `iteration-${iterationType}-${iterGroup.iterationCurrent}-${iterBlocks[0]?.executionId || 'unknown'}`,
|
||||
timestamp: new Date(iterStartMs).toISOString(),
|
||||
workflowId: iterBlocks[0]?.workflowId || '',
|
||||
blockId: `iteration-${iterGroup.iterationCurrent}`,
|
||||
blockName: `Iteration ${iterGroup.iterationCurrent}${iterGroup.iterationTotal !== undefined ? ` / ${iterGroup.iterationTotal}` : ''}`,
|
||||
blockType: iterationType,
|
||||
executionId: iterBlocks[0]?.executionId,
|
||||
startedAt: new Date(iterStartMs).toISOString(),
|
||||
endedAt: new Date(iterEndMs).toISOString(),
|
||||
durationMs: iterDuration,
|
||||
success: !iterBlocks.some((b) => b.error),
|
||||
iterationCurrent: iterGroup.iterationCurrent,
|
||||
iterationTotal: iterGroup.iterationTotal,
|
||||
iterationType: iterationType as 'loop' | 'parallel',
|
||||
}
|
||||
|
||||
// Block nodes within this iteration
|
||||
const blockNodes: EntryNode[] = iterBlocks.map((block) => ({
|
||||
entry: block,
|
||||
children: [],
|
||||
nodeType: 'block' as const,
|
||||
}))
|
||||
|
||||
return {
|
||||
entry: syntheticIteration,
|
||||
children: blockNodes,
|
||||
nodeType: 'iteration' as const,
|
||||
iterationInfo: {
|
||||
current: iterGroup.iterationCurrent,
|
||||
total: iterGroup.iterationTotal,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
subflowNodes.push({
|
||||
entry: syntheticSubflow,
|
||||
children: iterationNodes,
|
||||
nodeType: 'subflow' as const,
|
||||
})
|
||||
}
|
||||
|
||||
// Build nodes for regular blocks
|
||||
const regularNodes: EntryNode[] = regularBlocks.map((entry) => ({
|
||||
entry,
|
||||
children: [],
|
||||
nodeType: 'block' as const,
|
||||
}))
|
||||
|
||||
// Combine all nodes and sort by start time ascending (oldest first, top-down)
|
||||
const allNodes = [...subflowNodes, ...regularNodes]
|
||||
allNodes.sort((a, b) => {
|
||||
const aStart = new Date(a.entry.startedAt || a.entry.timestamp).getTime()
|
||||
const bStart = new Date(b.entry.startedAt || b.entry.timestamp).getTime()
|
||||
return aStart - bStart
|
||||
})
|
||||
|
||||
return allNodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups console entries by execution ID and builds a tree structure.
|
||||
* Pre-computes timestamps for efficient sorting.
|
||||
*/
|
||||
export function groupEntriesByExecution(entries: ConsoleEntry[]): ExecutionGroup[] {
|
||||
const groups = new Map<
|
||||
string,
|
||||
{ meta: Omit<ExecutionGroup, 'entryTree'>; entries: ConsoleEntry[] }
|
||||
>()
|
||||
|
||||
for (const entry of entries) {
|
||||
const execId = entry.executionId || entry.id
|
||||
|
||||
const entryStartTime = entry.startedAt || entry.timestamp
|
||||
const entryEndTime = entry.endedAt || entry.timestamp
|
||||
const entryStartMs = new Date(entryStartTime).getTime()
|
||||
const entryEndMs = new Date(entryEndTime).getTime()
|
||||
|
||||
let group = groups.get(execId)
|
||||
|
||||
if (!group) {
|
||||
group = {
|
||||
meta: {
|
||||
executionId: execId,
|
||||
startTime: entryStartTime,
|
||||
endTime: entryEndTime,
|
||||
startTimeMs: entryStartMs,
|
||||
endTimeMs: entryEndMs,
|
||||
duration: 0,
|
||||
status: 'success',
|
||||
entries: [],
|
||||
},
|
||||
entries: [],
|
||||
}
|
||||
groups.set(execId, group)
|
||||
} else {
|
||||
// Update timing bounds
|
||||
if (entryStartMs < group.meta.startTimeMs) {
|
||||
group.meta.startTime = entryStartTime
|
||||
group.meta.startTimeMs = entryStartMs
|
||||
}
|
||||
if (entryEndMs > group.meta.endTimeMs) {
|
||||
group.meta.endTime = entryEndTime
|
||||
group.meta.endTimeMs = entryEndMs
|
||||
}
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (entry.error) {
|
||||
group.meta.status = 'error'
|
||||
}
|
||||
|
||||
group.entries.push(entry)
|
||||
}
|
||||
|
||||
// Build tree structure for each group
|
||||
const result: ExecutionGroup[] = []
|
||||
for (const group of groups.values()) {
|
||||
group.meta.duration = group.meta.endTimeMs - group.meta.startTimeMs
|
||||
group.meta.entries = group.entries
|
||||
result.push({
|
||||
...group.meta,
|
||||
entryTree: buildEntryTree(group.entries),
|
||||
})
|
||||
}
|
||||
|
||||
// Sort by start time descending (newest first)
|
||||
result.sort((a, b) => b.startTimeMs - a.startTimeMs)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens entry tree into display order for keyboard navigation
|
||||
*/
|
||||
export function flattenEntryTree(nodes: EntryNode[]): ConsoleEntry[] {
|
||||
const result: ConsoleEntry[] = []
|
||||
for (const node of nodes) {
|
||||
result.push(node.entry)
|
||||
if (node.children.length > 0) {
|
||||
result.push(...flattenEntryTree(node.children))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Block entry with parent tracking for navigation
|
||||
*/
|
||||
export interface NavigableBlockEntry {
|
||||
entry: ConsoleEntry
|
||||
executionId: string
|
||||
/** IDs of parent nodes (subflows, iterations) that contain this block */
|
||||
parentNodeIds: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens entry tree to only include actual block entries (not subflows/iterations).
|
||||
* Also tracks parent node IDs for auto-expanding when navigating.
|
||||
*/
|
||||
export function flattenBlockEntriesOnly(
|
||||
nodes: EntryNode[],
|
||||
executionId: string,
|
||||
parentIds: string[] = []
|
||||
): NavigableBlockEntry[] {
|
||||
const result: NavigableBlockEntry[] = []
|
||||
for (const node of nodes) {
|
||||
if (node.nodeType === 'block') {
|
||||
result.push({
|
||||
entry: node.entry,
|
||||
executionId,
|
||||
parentNodeIds: parentIds,
|
||||
})
|
||||
}
|
||||
if (node.children.length > 0) {
|
||||
const newParentIds = node.nodeType !== 'block' ? [...parentIds, node.entry.id] : parentIds
|
||||
result.push(...flattenBlockEntriesOnly(node.children, executionId, newParentIds))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminal height configuration constants
|
||||
*/
|
||||
export const TERMINAL_CONFIG = {
|
||||
NEAR_MIN_THRESHOLD: 40,
|
||||
BLOCK_COLUMN_WIDTH_PX: TERMINAL_BLOCK_COLUMN_WIDTH,
|
||||
HEADER_TEXT_CLASS: 'font-medium text-[var(--text-tertiary)] text-[12px]',
|
||||
} as const
|
||||
@@ -3,7 +3,6 @@ import { createLogger } from '@sim/logger'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import type { AutoLayoutOptions } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils'
|
||||
import { applyAutoLayoutAndUpdateStore as applyAutoLayoutStandalone } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils/auto-layout-utils'
|
||||
import { useSnapToGridSize } from '@/hooks/queries/general-settings'
|
||||
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
|
||||
|
||||
export type { AutoLayoutOptions }
|
||||
@@ -14,28 +13,21 @@ const logger = createLogger('useAutoLayout')
|
||||
* Hook providing auto-layout functionality for workflows.
|
||||
* Binds workflowId context and provides memoized callback for React components.
|
||||
* Includes automatic fitView animation after successful layout.
|
||||
* Automatically uses the user's snap-to-grid setting for grid-aligned layout.
|
||||
*
|
||||
* Note: This hook requires a ReactFlowProvider ancestor.
|
||||
*/
|
||||
export function useAutoLayout(workflowId: string | null) {
|
||||
const reactFlowInstance = useReactFlow()
|
||||
const { fitViewToBounds } = useCanvasViewport(reactFlowInstance)
|
||||
const snapToGridSize = useSnapToGridSize()
|
||||
|
||||
const applyAutoLayoutAndUpdateStore = useCallback(
|
||||
async (options: AutoLayoutOptions = {}) => {
|
||||
if (!workflowId) {
|
||||
return { success: false, error: 'No workflow ID provided' }
|
||||
}
|
||||
// Include gridSize from user's snap-to-grid setting
|
||||
const optionsWithGrid: AutoLayoutOptions = {
|
||||
...options,
|
||||
gridSize: options.gridSize ?? (snapToGridSize > 0 ? snapToGridSize : undefined),
|
||||
}
|
||||
return applyAutoLayoutStandalone(workflowId, optionsWithGrid)
|
||||
return applyAutoLayoutStandalone(workflowId, options)
|
||||
},
|
||||
[workflowId, snapToGridSize]
|
||||
[workflowId]
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -81,8 +81,7 @@ export function useWorkflowExecution() {
|
||||
const queryClient = useQueryClient()
|
||||
const currentWorkflow = useCurrentWorkflow()
|
||||
const { activeWorkflowId, workflows } = useWorkflowRegistry()
|
||||
const { toggleConsole, addConsole, updateConsole, cancelRunningEntries } =
|
||||
useTerminalConsoleStore()
|
||||
const { toggleConsole, addConsole } = useTerminalConsoleStore()
|
||||
const { getAllVariables } = useEnvironmentStore()
|
||||
const { getVariablesByWorkflowId, variables } = useVariablesStore()
|
||||
const {
|
||||
@@ -868,8 +867,6 @@ export function useWorkflowExecution() {
|
||||
if (activeWorkflowId) {
|
||||
logger.info('Using server-side executor')
|
||||
|
||||
const executionId = uuidv4()
|
||||
|
||||
let executionResult: ExecutionResult = {
|
||||
success: false,
|
||||
output: {},
|
||||
@@ -913,27 +910,6 @@ export function useWorkflowExecution() {
|
||||
incomingEdges.forEach((edge) => {
|
||||
setEdgeRunStatus(edge.id, 'success')
|
||||
})
|
||||
|
||||
// Add entry to terminal immediately with isRunning=true
|
||||
const startedAt = new Date().toISOString()
|
||||
addConsole({
|
||||
input: {},
|
||||
output: undefined,
|
||||
success: undefined,
|
||||
durationMs: undefined,
|
||||
startedAt,
|
||||
endedAt: undefined,
|
||||
workflowId: activeWorkflowId,
|
||||
blockId: data.blockId,
|
||||
executionId,
|
||||
blockName: data.blockName || 'Unknown Block',
|
||||
blockType: data.blockType || 'unknown',
|
||||
isRunning: true,
|
||||
// Pass through iteration context for subflow grouping
|
||||
iterationCurrent: data.iterationCurrent,
|
||||
iterationTotal: data.iterationTotal,
|
||||
iterationType: data.iterationType,
|
||||
})
|
||||
},
|
||||
|
||||
onBlockCompleted: (data) => {
|
||||
@@ -964,23 +940,24 @@ export function useWorkflowExecution() {
|
||||
endedAt,
|
||||
})
|
||||
|
||||
// Update existing console entry (created in onBlockStarted) with completion data
|
||||
updateConsole(
|
||||
data.blockId,
|
||||
{
|
||||
input: data.input || {},
|
||||
replaceOutput: data.output,
|
||||
success: true,
|
||||
durationMs: data.durationMs,
|
||||
endedAt,
|
||||
isRunning: false,
|
||||
// Pass through iteration context for subflow grouping
|
||||
iterationCurrent: data.iterationCurrent,
|
||||
iterationTotal: data.iterationTotal,
|
||||
iterationType: data.iterationType,
|
||||
},
|
||||
executionId
|
||||
)
|
||||
// Add to console
|
||||
addConsole({
|
||||
input: data.input || {},
|
||||
output: data.output,
|
||||
success: true,
|
||||
durationMs: data.durationMs,
|
||||
startedAt,
|
||||
endedAt,
|
||||
workflowId: activeWorkflowId,
|
||||
blockId: data.blockId,
|
||||
executionId: executionId || uuidv4(),
|
||||
blockName: data.blockName || 'Unknown Block',
|
||||
blockType: data.blockType || 'unknown',
|
||||
// Pass through iteration context for console pills
|
||||
iterationCurrent: data.iterationCurrent,
|
||||
iterationTotal: data.iterationTotal,
|
||||
iterationType: data.iterationType,
|
||||
})
|
||||
|
||||
// Call onBlockComplete callback if provided
|
||||
if (onBlockComplete) {
|
||||
@@ -1015,24 +992,25 @@ export function useWorkflowExecution() {
|
||||
endedAt,
|
||||
})
|
||||
|
||||
// Update existing console entry (created in onBlockStarted) with error data
|
||||
updateConsole(
|
||||
data.blockId,
|
||||
{
|
||||
input: data.input || {},
|
||||
replaceOutput: {},
|
||||
success: false,
|
||||
error: data.error,
|
||||
durationMs: data.durationMs,
|
||||
endedAt,
|
||||
isRunning: false,
|
||||
// Pass through iteration context for subflow grouping
|
||||
iterationCurrent: data.iterationCurrent,
|
||||
iterationTotal: data.iterationTotal,
|
||||
iterationType: data.iterationType,
|
||||
},
|
||||
executionId
|
||||
)
|
||||
// Add error to console
|
||||
addConsole({
|
||||
input: data.input || {},
|
||||
output: {},
|
||||
success: false,
|
||||
error: data.error,
|
||||
durationMs: data.durationMs,
|
||||
startedAt,
|
||||
endedAt,
|
||||
workflowId: activeWorkflowId,
|
||||
blockId: data.blockId,
|
||||
executionId: executionId || uuidv4(),
|
||||
blockName: data.blockName,
|
||||
blockType: data.blockType,
|
||||
// Pass through iteration context for console pills
|
||||
iterationCurrent: data.iterationCurrent,
|
||||
iterationTotal: data.iterationTotal,
|
||||
iterationType: data.iterationType,
|
||||
})
|
||||
},
|
||||
|
||||
onStreamChunk: (data) => {
|
||||
@@ -1111,7 +1089,7 @@ export function useWorkflowExecution() {
|
||||
endedAt: new Date().toISOString(),
|
||||
workflowId: activeWorkflowId,
|
||||
blockId: 'validation',
|
||||
executionId,
|
||||
executionId: executionId || uuidv4(),
|
||||
blockName: 'Workflow Validation',
|
||||
blockType: 'validation',
|
||||
})
|
||||
@@ -1380,11 +1358,6 @@ export function useWorkflowExecution() {
|
||||
// Mark current chat execution as superseded so its cleanup won't affect new executions
|
||||
currentChatExecutionIdRef.current = null
|
||||
|
||||
// Mark all running entries as canceled in the terminal
|
||||
if (activeWorkflowId) {
|
||||
cancelRunningEntries(activeWorkflowId)
|
||||
}
|
||||
|
||||
// Reset execution state - this triggers chat stream cleanup via useEffect in chat.tsx
|
||||
setIsExecuting(false)
|
||||
setIsDebugging(false)
|
||||
@@ -1401,8 +1374,6 @@ export function useWorkflowExecution() {
|
||||
setIsExecuting,
|
||||
setIsDebugging,
|
||||
setActiveBlocks,
|
||||
activeWorkflowId,
|
||||
cancelRunningEntries,
|
||||
])
|
||||
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface AutoLayoutOptions {
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
gridSize?: number
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +62,6 @@ export async function applyAutoLayoutAndUpdateStore(
|
||||
x: options.padding?.x ?? DEFAULT_LAYOUT_PADDING.x,
|
||||
y: options.padding?.y ?? DEFAULT_LAYOUT_PADDING.y,
|
||||
},
|
||||
gridSize: options.gridSize,
|
||||
}
|
||||
|
||||
// Call the autolayout API route
|
||||
|
||||
@@ -99,19 +99,33 @@ const logger = createLogger('Workflow')
|
||||
const DEFAULT_PASTE_OFFSET = { x: 50, y: 50 }
|
||||
|
||||
/**
|
||||
* Calculates the offset to paste blocks at viewport center
|
||||
* Calculates the offset to paste blocks at viewport center, or simple offset for nested blocks
|
||||
*/
|
||||
function calculatePasteOffset(
|
||||
clipboard: {
|
||||
blocks: Record<string, { position: { x: number; y: number }; type: string; height?: number }>
|
||||
blocks: Record<
|
||||
string,
|
||||
{
|
||||
position: { x: number; y: number }
|
||||
type: string
|
||||
height?: number
|
||||
data?: { parentId?: string }
|
||||
}
|
||||
>
|
||||
} | null,
|
||||
viewportCenter: { x: number; y: number }
|
||||
viewportCenter: { x: number; y: number },
|
||||
existingBlocks: Record<string, { id: string }> = {}
|
||||
): { x: number; y: number } {
|
||||
if (!clipboard) return DEFAULT_PASTE_OFFSET
|
||||
|
||||
const clipboardBlocks = Object.values(clipboard.blocks)
|
||||
if (clipboardBlocks.length === 0) return DEFAULT_PASTE_OFFSET
|
||||
|
||||
const allBlocksNested = clipboardBlocks.every(
|
||||
(b) => b.data?.parentId && existingBlocks[b.data.parentId]
|
||||
)
|
||||
if (allBlocksNested) return DEFAULT_PASTE_OFFSET
|
||||
|
||||
const minX = Math.min(...clipboardBlocks.map((b) => b.position.x))
|
||||
const maxX = Math.max(
|
||||
...clipboardBlocks.map((b) => {
|
||||
@@ -307,9 +321,7 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
const isAutoConnectEnabled = useAutoConnect()
|
||||
const autoConnectRef = useRef(isAutoConnectEnabled)
|
||||
useEffect(() => {
|
||||
autoConnectRef.current = isAutoConnectEnabled
|
||||
}, [isAutoConnectEnabled])
|
||||
autoConnectRef.current = isAutoConnectEnabled
|
||||
|
||||
// Panel open states for context menu
|
||||
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
|
||||
@@ -448,11 +460,16 @@ const WorkflowContent = React.memo(() => {
|
||||
)
|
||||
|
||||
/** Re-applies diff markers when blocks change after socket rehydration. */
|
||||
const blocksRef = useRef(blocks)
|
||||
const diffBlocksRef = useRef(blocks)
|
||||
useEffect(() => {
|
||||
if (!isWorkflowReady) return
|
||||
if (hasActiveDiff && isDiffReady && blocks !== blocksRef.current) {
|
||||
blocksRef.current = blocks
|
||||
|
||||
const blocksChanged = blocks !== diffBlocksRef.current
|
||||
if (!blocksChanged) return
|
||||
|
||||
diffBlocksRef.current = blocks
|
||||
|
||||
if (hasActiveDiff && isDiffReady) {
|
||||
setTimeout(() => reapplyDiffMarkers(), 0)
|
||||
}
|
||||
}, [blocks, hasActiveDiff, isDiffReady, reapplyDiffMarkers, isWorkflowReady])
|
||||
@@ -515,8 +532,7 @@ const WorkflowContent = React.memo(() => {
|
||||
})
|
||||
}, [edges, isShowingDiff, isDiffReady, diffAnalysis, blocks])
|
||||
|
||||
const { userPermissions, workspacePermissions, permissionsError } =
|
||||
useWorkspacePermissionsContext()
|
||||
const { userPermissions } = useWorkspacePermissionsContext()
|
||||
|
||||
/** Returns read-only permissions when viewing snapshot, otherwise user permissions. */
|
||||
const effectivePermissions = useMemo(() => {
|
||||
@@ -752,25 +768,6 @@ const WorkflowContent = React.memo(() => {
|
||||
[isErrorConnectionDrag]
|
||||
)
|
||||
|
||||
/** Logs permission loading results for debugging. */
|
||||
useEffect(() => {
|
||||
if (permissionsError) {
|
||||
logger.error('Failed to load workspace permissions', {
|
||||
workspaceId,
|
||||
error: permissionsError,
|
||||
})
|
||||
} else if (workspacePermissions) {
|
||||
logger.info('Workspace permissions loaded in workflow', {
|
||||
workspaceId,
|
||||
userCount: workspacePermissions.total,
|
||||
permissions: workspacePermissions.users.map((u) => ({
|
||||
email: u.email,
|
||||
permissions: u.permissionType,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}, [workspacePermissions, permissionsError, workspaceId])
|
||||
|
||||
const updateNodeParent = useCallback(
|
||||
(nodeId: string, newParentId: string | null, affectedEdges: any[] = []) => {
|
||||
const node = getNodes().find((n: any) => n.id === nodeId)
|
||||
@@ -1042,7 +1039,7 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
executePasteOperation(
|
||||
'paste',
|
||||
calculatePasteOffset(clipboard, getViewportCenter()),
|
||||
calculatePasteOffset(clipboard, getViewportCenter(), blocks),
|
||||
targetContainer,
|
||||
flowPosition // Pass the click position so blocks are centered at where user right-clicked
|
||||
)
|
||||
@@ -1054,6 +1051,7 @@ const WorkflowContent = React.memo(() => {
|
||||
screenToFlowPosition,
|
||||
contextMenuPosition,
|
||||
isPointInLoopNode,
|
||||
blocks,
|
||||
])
|
||||
|
||||
const handleContextDuplicate = useCallback(() => {
|
||||
@@ -1164,7 +1162,10 @@ const WorkflowContent = React.memo(() => {
|
||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
|
||||
if (effectivePermissions.canEdit && hasClipboard()) {
|
||||
event.preventDefault()
|
||||
executePasteOperation('paste', calculatePasteOffset(clipboard, getViewportCenter()))
|
||||
executePasteOperation(
|
||||
'paste',
|
||||
calculatePasteOffset(clipboard, getViewportCenter(), blocks)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1186,6 +1187,7 @@ const WorkflowContent = React.memo(() => {
|
||||
clipboard,
|
||||
getViewportCenter,
|
||||
executePasteOperation,
|
||||
blocks,
|
||||
])
|
||||
|
||||
/**
|
||||
@@ -2160,6 +2162,8 @@ const WorkflowContent = React.memo(() => {
|
||||
// Local state for nodes - allows smooth drag without store updates on every frame
|
||||
const [displayNodes, setDisplayNodes] = useState<Node[]>([])
|
||||
|
||||
// Sync derivedNodes to displayNodes while preserving selection state
|
||||
// This effect handles both normal sync and pending selection from paste/duplicate
|
||||
useEffect(() => {
|
||||
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
|
||||
if (pendingSelection && pendingSelection.length > 0) {
|
||||
@@ -2189,7 +2193,6 @@ const WorkflowContent = React.memo(() => {
|
||||
}, [derivedNodes, blocks, pendingSelection, clearPendingSelection])
|
||||
|
||||
// Phase 2: When displayNodes updates, check if pending zoom blocks are ready
|
||||
// (Phase 1 is located earlier in the file where pendingZoomBlockIdsRef is defined)
|
||||
useEffect(() => {
|
||||
const pendingBlockIds = pendingZoomBlockIdsRef.current
|
||||
if (!pendingBlockIds || pendingBlockIds.size === 0) {
|
||||
@@ -2380,40 +2383,6 @@ const WorkflowContent = React.memo(() => {
|
||||
resizeLoopNodesWrapper()
|
||||
}, [derivedNodes, resizeLoopNodesWrapper, isWorkflowReady])
|
||||
|
||||
/** Cleans up orphaned nodes with invalid parent references after deletion. */
|
||||
useEffect(() => {
|
||||
if (!isWorkflowReady) return
|
||||
|
||||
// Create a mapping of node IDs to check for missing parent references
|
||||
const nodeIds = new Set(Object.keys(blocks))
|
||||
|
||||
// Check for nodes with invalid parent references and collect updates
|
||||
const orphanedUpdates: Array<{
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
parentId: string
|
||||
}> = []
|
||||
Object.entries(blocks).forEach(([id, block]) => {
|
||||
const parentId = block.data?.parentId
|
||||
|
||||
// If block has a parent reference but parent no longer exists
|
||||
if (parentId && !nodeIds.has(parentId)) {
|
||||
logger.warn('Found orphaned node with invalid parent reference', {
|
||||
nodeId: id,
|
||||
missingParentId: parentId,
|
||||
})
|
||||
|
||||
const absolutePosition = getNodeAbsolutePosition(id)
|
||||
orphanedUpdates.push({ id, position: absolutePosition, parentId: '' })
|
||||
}
|
||||
})
|
||||
|
||||
// Batch update all orphaned nodes at once
|
||||
if (orphanedUpdates.length > 0) {
|
||||
batchUpdateBlocksWithParent(orphanedUpdates)
|
||||
}
|
||||
}, [blocks, batchUpdateBlocksWithParent, getNodeAbsolutePosition, isWorkflowReady])
|
||||
|
||||
/** Handles edge removal changes. */
|
||||
const onEdgesChange = useCallback(
|
||||
(changes: any) => {
|
||||
|
||||
@@ -1141,17 +1141,15 @@ function PreviewEditorContent({
|
||||
<div className='relative flex h-full w-80 flex-col overflow-hidden border-[var(--border)] border-l bg-[var(--surface-1)]'>
|
||||
{/* Header - styled like editor */}
|
||||
<div className='mx-[-1px] flex flex-shrink-0 items-center gap-[8px] rounded-b-[4px] border-[var(--border)] border-x border-b bg-[var(--surface-4)] px-[12px] py-[6px]'>
|
||||
{block.type !== 'note' && (
|
||||
<div
|
||||
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
style={{ backgroundColor: blockConfig.bgColor }}
|
||||
>
|
||||
<IconComponent
|
||||
icon={blockConfig.icon}
|
||||
className='h-[12px] w-[12px] text-[var(--white)]'
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||
style={{ backgroundColor: blockConfig.bgColor }}
|
||||
>
|
||||
<IconComponent
|
||||
icon={blockConfig.icon}
|
||||
className='h-[12px] w-[12px] text-[var(--white)]'
|
||||
/>
|
||||
</div>
|
||||
<span className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
|
||||
{block.name || blockConfig.name}
|
||||
</span>
|
||||
|
||||
@@ -411,9 +411,8 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
|
||||
|
||||
const IconComponent = blockConfig.icon
|
||||
const isStarterOrTrigger = blockConfig.category === 'triggers' || type === 'starter' || isTrigger
|
||||
const isNoteBlock = type === 'note'
|
||||
|
||||
const shouldShowDefaultHandles = !isStarterOrTrigger && !isNoteBlock
|
||||
const shouldShowDefaultHandles = !isStarterOrTrigger
|
||||
const hasSubBlocks = visibleSubBlocks.length > 0
|
||||
const hasContentBelowHeader =
|
||||
type === 'condition'
|
||||
@@ -575,8 +574,8 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Source and error handles for non-condition/router/note blocks */}
|
||||
{type !== 'condition' && type !== 'router_v2' && type !== 'response' && !isNoteBlock && (
|
||||
{/* Source and error handles for non-condition/router blocks */}
|
||||
{type !== 'condition' && type !== 'router_v2' && type !== 'response' && (
|
||||
<>
|
||||
<Handle
|
||||
type='source'
|
||||
|
||||
@@ -406,11 +406,9 @@ export function PreviewWorkflow({
|
||||
}
|
||||
}
|
||||
|
||||
const nodeType = block.type === 'note' ? 'noteBlock' : 'workflowBlock'
|
||||
|
||||
nodeArray.push({
|
||||
id: blockId,
|
||||
type: nodeType,
|
||||
type: 'workflowBlock',
|
||||
position: absolutePosition,
|
||||
draggable: false,
|
||||
zIndex: block.data?.parentId ? 10 : undefined,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* Search utility functions for tiered matching algorithm
|
||||
* Provides predictable search results prioritizing exact matches over fuzzy matches
|
||||
*/
|
||||
|
||||
export interface SearchableItem {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
type: string
|
||||
aliases?: string[]
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export interface SearchResult<T extends SearchableItem> {
|
||||
item: T
|
||||
score: number
|
||||
matchType: 'exact' | 'prefix' | 'alias' | 'word-boundary' | 'substring' | 'description'
|
||||
}
|
||||
|
||||
const SCORE_EXACT_MATCH = 10000
|
||||
const SCORE_PREFIX_MATCH = 5000
|
||||
const SCORE_ALIAS_MATCH = 3000
|
||||
const SCORE_WORD_BOUNDARY = 1000
|
||||
const SCORE_SUBSTRING_MATCH = 100
|
||||
const DESCRIPTION_WEIGHT = 0.3
|
||||
|
||||
/**
|
||||
* Calculate match score for a single field
|
||||
* Returns 0 if no match found
|
||||
*/
|
||||
function calculateFieldScore(
|
||||
query: string,
|
||||
field: string
|
||||
): {
|
||||
score: number
|
||||
matchType: 'exact' | 'prefix' | 'word-boundary' | 'substring' | null
|
||||
} {
|
||||
const normalizedQuery = query.toLowerCase().trim()
|
||||
const normalizedField = field.toLowerCase().trim()
|
||||
|
||||
if (!normalizedQuery || !normalizedField) {
|
||||
return { score: 0, matchType: null }
|
||||
}
|
||||
|
||||
// Tier 1: Exact match
|
||||
if (normalizedField === normalizedQuery) {
|
||||
return { score: SCORE_EXACT_MATCH, matchType: 'exact' }
|
||||
}
|
||||
|
||||
// Tier 2: Prefix match (starts with query)
|
||||
if (normalizedField.startsWith(normalizedQuery)) {
|
||||
return { score: SCORE_PREFIX_MATCH, matchType: 'prefix' }
|
||||
}
|
||||
|
||||
// Tier 3: Word boundary match (query matches start of a word)
|
||||
const words = normalizedField.split(/[\s-_/]+/)
|
||||
const hasWordBoundaryMatch = words.some((word) => word.startsWith(normalizedQuery))
|
||||
if (hasWordBoundaryMatch) {
|
||||
return { score: SCORE_WORD_BOUNDARY, matchType: 'word-boundary' }
|
||||
}
|
||||
|
||||
// Tier 4: Substring match (query appears anywhere)
|
||||
if (normalizedField.includes(normalizedQuery)) {
|
||||
return { score: SCORE_SUBSTRING_MATCH, matchType: 'substring' }
|
||||
}
|
||||
|
||||
// No match
|
||||
return { score: 0, matchType: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if query matches any alias in the item's aliases array
|
||||
* Returns the alias score if a match is found, 0 otherwise
|
||||
*/
|
||||
function calculateAliasScore(
|
||||
query: string,
|
||||
aliases?: string[]
|
||||
): { score: number; matchType: 'alias' | null } {
|
||||
if (!aliases || aliases.length === 0) {
|
||||
return { score: 0, matchType: null }
|
||||
}
|
||||
|
||||
const normalizedQuery = query.toLowerCase().trim()
|
||||
|
||||
for (const alias of aliases) {
|
||||
const normalizedAlias = alias.toLowerCase().trim()
|
||||
|
||||
if (normalizedAlias === normalizedQuery) {
|
||||
return { score: SCORE_ALIAS_MATCH, matchType: 'alias' }
|
||||
}
|
||||
|
||||
if (normalizedAlias.startsWith(normalizedQuery)) {
|
||||
return { score: SCORE_ALIAS_MATCH * 0.8, matchType: 'alias' }
|
||||
}
|
||||
|
||||
if (normalizedQuery.includes(normalizedAlias) || normalizedAlias.includes(normalizedQuery)) {
|
||||
return { score: SCORE_ALIAS_MATCH * 0.6, matchType: 'alias' }
|
||||
}
|
||||
}
|
||||
|
||||
return { score: 0, matchType: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate multi-word match score
|
||||
* Each word in the query must appear somewhere in the field
|
||||
* Returns a score based on how well the words match
|
||||
*/
|
||||
function calculateMultiWordScore(
|
||||
queryWords: string[],
|
||||
field: string
|
||||
): { score: number; matchType: 'word-boundary' | 'substring' | null } {
|
||||
const normalizedField = field.toLowerCase().trim()
|
||||
const fieldWords = normalizedField.split(/[\s\-_/:]+/)
|
||||
|
||||
let allWordsMatch = true
|
||||
let totalScore = 0
|
||||
let hasWordBoundary = false
|
||||
|
||||
for (const queryWord of queryWords) {
|
||||
const wordBoundaryMatch = fieldWords.some((fw) => fw.startsWith(queryWord))
|
||||
const substringMatch = normalizedField.includes(queryWord)
|
||||
|
||||
if (wordBoundaryMatch) {
|
||||
totalScore += SCORE_WORD_BOUNDARY
|
||||
hasWordBoundary = true
|
||||
} else if (substringMatch) {
|
||||
totalScore += SCORE_SUBSTRING_MATCH
|
||||
} else {
|
||||
allWordsMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!allWordsMatch) {
|
||||
return { score: 0, matchType: null }
|
||||
}
|
||||
|
||||
return {
|
||||
score: totalScore / queryWords.length,
|
||||
matchType: hasWordBoundary ? 'word-boundary' : 'substring',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search items using tiered matching algorithm
|
||||
* Returns items sorted by relevance (highest score first)
|
||||
*/
|
||||
export function searchItems<T extends SearchableItem>(
|
||||
query: string,
|
||||
items: T[]
|
||||
): SearchResult<T>[] {
|
||||
const normalizedQuery = query.trim()
|
||||
|
||||
if (!normalizedQuery) {
|
||||
return []
|
||||
}
|
||||
|
||||
const results: SearchResult<T>[] = []
|
||||
const queryWords = normalizedQuery.toLowerCase().split(/\s+/).filter(Boolean)
|
||||
const isMultiWord = queryWords.length > 1
|
||||
|
||||
for (const item of items) {
|
||||
const nameMatch = calculateFieldScore(normalizedQuery, item.name)
|
||||
|
||||
const descMatch = item.description
|
||||
? calculateFieldScore(normalizedQuery, item.description)
|
||||
: { score: 0, matchType: null }
|
||||
|
||||
const aliasMatch = calculateAliasScore(normalizedQuery, item.aliases)
|
||||
|
||||
let nameScore = nameMatch.score
|
||||
let descScore = descMatch.score * DESCRIPTION_WEIGHT
|
||||
const aliasScore = aliasMatch.score
|
||||
|
||||
let bestMatchType = nameMatch.matchType
|
||||
|
||||
// For multi-word queries, also try matching each word independently and take the better score
|
||||
if (isMultiWord) {
|
||||
const multiWordNameMatch = calculateMultiWordScore(queryWords, item.name)
|
||||
if (multiWordNameMatch.score > nameScore) {
|
||||
nameScore = multiWordNameMatch.score
|
||||
bestMatchType = multiWordNameMatch.matchType
|
||||
}
|
||||
|
||||
if (item.description) {
|
||||
const multiWordDescMatch = calculateMultiWordScore(queryWords, item.description)
|
||||
const multiWordDescScore = multiWordDescMatch.score * DESCRIPTION_WEIGHT
|
||||
if (multiWordDescScore > descScore) {
|
||||
descScore = multiWordDescScore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bestScore = Math.max(nameScore, descScore, aliasScore)
|
||||
|
||||
if (bestScore > 0) {
|
||||
let matchType: SearchResult<T>['matchType'] = 'substring'
|
||||
if (nameScore >= descScore && nameScore >= aliasScore) {
|
||||
matchType = bestMatchType || 'substring'
|
||||
} else if (aliasScore >= descScore) {
|
||||
matchType = 'alias'
|
||||
} else {
|
||||
matchType = 'description'
|
||||
}
|
||||
|
||||
results.push({
|
||||
item,
|
||||
score: bestScore,
|
||||
matchType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
results.sort((a, b) => b.score - a.score)
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable match type label
|
||||
*/
|
||||
export function getMatchTypeLabel(matchType: SearchResult<any>['matchType']): string {
|
||||
switch (matchType) {
|
||||
case 'exact':
|
||||
return 'Exact match'
|
||||
case 'prefix':
|
||||
return 'Starts with'
|
||||
case 'alias':
|
||||
return 'Similar to'
|
||||
case 'word-boundary':
|
||||
return 'Word match'
|
||||
case 'substring':
|
||||
return 'Contains'
|
||||
case 'description':
|
||||
return 'In description'
|
||||
default:
|
||||
return 'Match'
|
||||
}
|
||||
}
|
||||
@@ -176,7 +176,7 @@ function FormattedInput({
|
||||
onChange,
|
||||
onScroll,
|
||||
}: FormattedInputProps) {
|
||||
const handleScroll = (e: { currentTarget: HTMLInputElement }) => {
|
||||
const handleScroll = (e: React.UIEvent<HTMLInputElement>) => {
|
||||
onScroll(e.currentTarget.scrollLeft)
|
||||
}
|
||||
|
||||
|
||||
@@ -73,12 +73,7 @@ export const Sidebar = memo(function Sidebar() {
|
||||
|
||||
const { data: sessionData, isPending: sessionLoading } = useSession()
|
||||
const { canEdit } = useUserPermissionsContext()
|
||||
const { config: permissionConfig, filterBlocks } = usePermissionConfig()
|
||||
const initializeSearchData = useSearchModalStore((state) => state.initializeData)
|
||||
|
||||
useEffect(() => {
|
||||
initializeSearchData(filterBlocks)
|
||||
}, [initializeSearchData, filterBlocks])
|
||||
const { config: permissionConfig } = usePermissionConfig()
|
||||
|
||||
/**
|
||||
* Sidebar state from store with hydration tracking to prevent SSR mismatch.
|
||||
|
||||
@@ -769,13 +769,7 @@ Example 3 (Array Input):
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Generated response content' },
|
||||
model: { type: 'string', description: 'Model used for generation' },
|
||||
tokens: { type: 'json', description: 'Token usage statistics' },
|
||||
toolCalls: { type: 'json', description: 'Tool calls made' },
|
||||
providerTiming: {
|
||||
type: 'json',
|
||||
description: 'Provider timing information',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
cost: { type: 'number', description: 'Cost of the API call', hiddenFromDisplay: true },
|
||||
tokens: { type: 'any', description: 'Token usage statistics' },
|
||||
toolCalls: { type: 'any', description: 'Tool calls made' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
import { ClerkIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import type { ClerkResponse } from '@/tools/clerk/types'
|
||||
|
||||
export const ClerkBlock: BlockConfig<ClerkResponse> = {
|
||||
type: 'clerk',
|
||||
name: 'Clerk',
|
||||
description: 'Manage users, organizations, and sessions in Clerk',
|
||||
longDescription:
|
||||
'Integrate Clerk authentication and user management into your workflow. Create, update, delete, and list users. Manage organizations and their memberships. Monitor and control user sessions.',
|
||||
docsLink: 'https://docs.sim.ai/tools/clerk',
|
||||
category: 'tools',
|
||||
bgColor: '#131316',
|
||||
icon: ClerkIcon,
|
||||
|
||||
subBlocks: [
|
||||
{
|
||||
id: 'operation',
|
||||
title: 'Operation',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'List Users', id: 'clerk_list_users' },
|
||||
{ label: 'Get User', id: 'clerk_get_user' },
|
||||
{ label: 'Create User', id: 'clerk_create_user' },
|
||||
{ label: 'Update User', id: 'clerk_update_user' },
|
||||
{ label: 'Delete User', id: 'clerk_delete_user' },
|
||||
{ label: 'List Organizations', id: 'clerk_list_organizations' },
|
||||
{ label: 'Get Organization', id: 'clerk_get_organization' },
|
||||
{ label: 'Create Organization', id: 'clerk_create_organization' },
|
||||
{ label: 'List Sessions', id: 'clerk_list_sessions' },
|
||||
{ label: 'Get Session', id: 'clerk_get_session' },
|
||||
{ label: 'Revoke Session', id: 'clerk_revoke_session' },
|
||||
],
|
||||
value: () => 'clerk_list_users',
|
||||
},
|
||||
{
|
||||
id: 'secretKey',
|
||||
title: 'Secret Key',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'sk_live_... or sk_test_...',
|
||||
required: true,
|
||||
},
|
||||
// List Users params
|
||||
{
|
||||
id: 'query',
|
||||
title: 'Search Query',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search by email, phone, username, or name',
|
||||
condition: { field: 'operation', value: 'clerk_list_users' },
|
||||
},
|
||||
{
|
||||
id: 'emailAddressFilter',
|
||||
title: 'Email Filter',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by email (comma-separated)',
|
||||
condition: { field: 'operation', value: 'clerk_list_users' },
|
||||
},
|
||||
{
|
||||
id: 'usernameFilter',
|
||||
title: 'Username Filter',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by username (comma-separated)',
|
||||
condition: { field: 'operation', value: 'clerk_list_users' },
|
||||
},
|
||||
// Get User params
|
||||
{
|
||||
id: 'userId',
|
||||
title: 'User ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'user_...',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['clerk_get_user', 'clerk_update_user', 'clerk_delete_user'],
|
||||
},
|
||||
required: {
|
||||
field: 'operation',
|
||||
value: ['clerk_get_user', 'clerk_update_user', 'clerk_delete_user'],
|
||||
},
|
||||
},
|
||||
// Create/Update User params
|
||||
{
|
||||
id: 'emailAddress',
|
||||
title: 'Email Address',
|
||||
type: 'short-input',
|
||||
placeholder: 'user@example.com (comma-separated for multiple)',
|
||||
condition: { field: 'operation', value: 'clerk_create_user' },
|
||||
},
|
||||
{
|
||||
id: 'phoneNumber',
|
||||
title: 'Phone Number',
|
||||
type: 'short-input',
|
||||
placeholder: '+1234567890 (comma-separated for multiple)',
|
||||
condition: { field: 'operation', value: 'clerk_create_user' },
|
||||
},
|
||||
{
|
||||
id: 'username',
|
||||
title: 'Username',
|
||||
type: 'short-input',
|
||||
placeholder: 'johndoe',
|
||||
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
title: 'Password',
|
||||
type: 'short-input',
|
||||
password: true,
|
||||
placeholder: 'Minimum 8 characters',
|
||||
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
|
||||
},
|
||||
{
|
||||
id: 'firstName',
|
||||
title: 'First Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'John',
|
||||
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
|
||||
},
|
||||
{
|
||||
id: 'lastName',
|
||||
title: 'Last Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Doe',
|
||||
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
|
||||
},
|
||||
{
|
||||
id: 'externalId',
|
||||
title: 'External ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'Your system user ID',
|
||||
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
|
||||
},
|
||||
{
|
||||
id: 'publicMetadata',
|
||||
title: 'Public Metadata',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
placeholder: '{"role": "admin"}',
|
||||
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
|
||||
},
|
||||
{
|
||||
id: 'privateMetadata',
|
||||
title: 'Private Metadata',
|
||||
type: 'code',
|
||||
language: 'json',
|
||||
placeholder: '{"internalId": "123"}',
|
||||
condition: { field: 'operation', value: ['clerk_create_user', 'clerk_update_user'] },
|
||||
},
|
||||
// Organization params
|
||||
{
|
||||
id: 'orgQuery',
|
||||
title: 'Search Query',
|
||||
type: 'short-input',
|
||||
placeholder: 'Search by name, ID, or slug',
|
||||
condition: { field: 'operation', value: 'clerk_list_organizations' },
|
||||
},
|
||||
{
|
||||
id: 'includeMembersCount',
|
||||
title: 'Include Members Count',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'clerk_list_organizations' },
|
||||
},
|
||||
{
|
||||
id: 'organizationId',
|
||||
title: 'Organization ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'org_... or slug',
|
||||
condition: { field: 'operation', value: 'clerk_get_organization' },
|
||||
required: { field: 'operation', value: 'clerk_get_organization' },
|
||||
},
|
||||
{
|
||||
id: 'orgName',
|
||||
title: 'Organization Name',
|
||||
type: 'short-input',
|
||||
placeholder: 'Acme Corp',
|
||||
condition: { field: 'operation', value: 'clerk_create_organization' },
|
||||
required: { field: 'operation', value: 'clerk_create_organization' },
|
||||
},
|
||||
{
|
||||
id: 'createdBy',
|
||||
title: 'Creator User ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'user_... (will become admin)',
|
||||
condition: { field: 'operation', value: 'clerk_create_organization' },
|
||||
required: { field: 'operation', value: 'clerk_create_organization' },
|
||||
},
|
||||
{
|
||||
id: 'slug',
|
||||
title: 'Slug',
|
||||
type: 'short-input',
|
||||
placeholder: 'acme-corp',
|
||||
condition: { field: 'operation', value: 'clerk_create_organization' },
|
||||
},
|
||||
{
|
||||
id: 'maxAllowedMemberships',
|
||||
title: 'Max Members',
|
||||
type: 'short-input',
|
||||
placeholder: '0 for unlimited',
|
||||
condition: { field: 'operation', value: 'clerk_create_organization' },
|
||||
},
|
||||
// Session params
|
||||
{
|
||||
id: 'sessionUserId',
|
||||
title: 'User ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'user_...',
|
||||
condition: { field: 'operation', value: 'clerk_list_sessions' },
|
||||
},
|
||||
{
|
||||
id: 'clientId',
|
||||
title: 'Client ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'client_...',
|
||||
condition: { field: 'operation', value: 'clerk_list_sessions' },
|
||||
},
|
||||
{
|
||||
id: 'sessionStatus',
|
||||
title: 'Status',
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
{ label: 'All', id: '' },
|
||||
{ label: 'Active', id: 'active' },
|
||||
{ label: 'Ended', id: 'ended' },
|
||||
{ label: 'Expired', id: 'expired' },
|
||||
{ label: 'Revoked', id: 'revoked' },
|
||||
{ label: 'Abandoned', id: 'abandoned' },
|
||||
{ label: 'Pending', id: 'pending' },
|
||||
],
|
||||
value: () => '',
|
||||
condition: { field: 'operation', value: 'clerk_list_sessions' },
|
||||
},
|
||||
{
|
||||
id: 'sessionId',
|
||||
title: 'Session ID',
|
||||
type: 'short-input',
|
||||
placeholder: 'sess_...',
|
||||
condition: { field: 'operation', value: ['clerk_get_session', 'clerk_revoke_session'] },
|
||||
required: { field: 'operation', value: ['clerk_get_session', 'clerk_revoke_session'] },
|
||||
},
|
||||
// Pagination params (common)
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: 'Results per page (1-500, default: 10)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['clerk_list_users', 'clerk_list_organizations', 'clerk_list_sessions'],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'offset',
|
||||
title: 'Offset',
|
||||
type: 'short-input',
|
||||
placeholder: 'Skip N results for pagination',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['clerk_list_users', 'clerk_list_organizations', 'clerk_list_sessions'],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
tools: {
|
||||
access: [
|
||||
'clerk_list_users',
|
||||
'clerk_get_user',
|
||||
'clerk_create_user',
|
||||
'clerk_update_user',
|
||||
'clerk_delete_user',
|
||||
'clerk_list_organizations',
|
||||
'clerk_get_organization',
|
||||
'clerk_create_organization',
|
||||
'clerk_list_sessions',
|
||||
'clerk_get_session',
|
||||
'clerk_revoke_session',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => params.operation as string,
|
||||
params: (params) => {
|
||||
const {
|
||||
operation,
|
||||
secretKey,
|
||||
emailAddressFilter,
|
||||
usernameFilter,
|
||||
orgQuery,
|
||||
orgName,
|
||||
sessionUserId,
|
||||
sessionStatus,
|
||||
publicMetadata,
|
||||
privateMetadata,
|
||||
maxAllowedMemberships,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
const cleanParams: Record<string, unknown> = {
|
||||
secretKey,
|
||||
}
|
||||
|
||||
// Map UI fields to API params based on operation
|
||||
switch (operation) {
|
||||
case 'clerk_list_users':
|
||||
if (emailAddressFilter) cleanParams.emailAddress = emailAddressFilter
|
||||
if (usernameFilter) cleanParams.username = usernameFilter
|
||||
break
|
||||
case 'clerk_create_user':
|
||||
case 'clerk_update_user':
|
||||
if (publicMetadata) {
|
||||
cleanParams.publicMetadata =
|
||||
typeof publicMetadata === 'string' ? JSON.parse(publicMetadata) : publicMetadata
|
||||
}
|
||||
if (privateMetadata) {
|
||||
cleanParams.privateMetadata =
|
||||
typeof privateMetadata === 'string' ? JSON.parse(privateMetadata) : privateMetadata
|
||||
}
|
||||
break
|
||||
case 'clerk_list_organizations':
|
||||
if (orgQuery) cleanParams.query = orgQuery
|
||||
break
|
||||
case 'clerk_create_organization':
|
||||
if (orgName) cleanParams.name = orgName
|
||||
if (maxAllowedMemberships)
|
||||
cleanParams.maxAllowedMemberships = Number(maxAllowedMemberships)
|
||||
break
|
||||
case 'clerk_list_sessions':
|
||||
if (sessionUserId) cleanParams.userId = sessionUserId
|
||||
if (sessionStatus) cleanParams.status = sessionStatus
|
||||
break
|
||||
}
|
||||
|
||||
// Add remaining params that don't need mapping
|
||||
Object.entries(rest).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
cleanParams[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
return cleanParams
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
secretKey: { type: 'string', description: 'Clerk Secret Key' },
|
||||
userId: { type: 'string', description: 'User ID' },
|
||||
organizationId: { type: 'string', description: 'Organization ID or slug' },
|
||||
sessionId: { type: 'string', description: 'Session ID' },
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
limit: { type: 'number', description: 'Results per page' },
|
||||
offset: { type: 'number', description: 'Pagination offset' },
|
||||
},
|
||||
|
||||
outputs: {
|
||||
// List outputs (arrays stored as json for block compatibility)
|
||||
users: { type: 'json', description: 'Array of user objects' },
|
||||
organizations: { type: 'json', description: 'Array of organization objects' },
|
||||
sessions: { type: 'json', description: 'Array of session objects' },
|
||||
// Single entity fields (destructured from get/create/update operations)
|
||||
id: { type: 'string', description: 'Resource ID (user, organization, or session)' },
|
||||
name: { type: 'string', description: 'Organization name' },
|
||||
slug: { type: 'string', description: 'Organization slug' },
|
||||
username: { type: 'string', description: 'Username' },
|
||||
firstName: { type: 'string', description: 'First name' },
|
||||
lastName: { type: 'string', description: 'Last name' },
|
||||
imageUrl: { type: 'string', description: 'Profile image URL' },
|
||||
hasImage: { type: 'boolean', description: 'Whether resource has an image' },
|
||||
emailAddresses: { type: 'json', description: 'User email addresses' },
|
||||
phoneNumbers: { type: 'json', description: 'User phone numbers' },
|
||||
primaryEmailAddressId: { type: 'string', description: 'Primary email address ID' },
|
||||
primaryPhoneNumberId: { type: 'string', description: 'Primary phone number ID' },
|
||||
externalId: { type: 'string', description: 'External system ID' },
|
||||
passwordEnabled: { type: 'boolean', description: 'Whether password is enabled' },
|
||||
twoFactorEnabled: { type: 'boolean', description: 'Whether 2FA is enabled' },
|
||||
banned: { type: 'boolean', description: 'Whether user is banned' },
|
||||
locked: { type: 'boolean', description: 'Whether user is locked' },
|
||||
userId: { type: 'string', description: 'User ID (for sessions)' },
|
||||
clientId: { type: 'string', description: 'Client ID (for sessions)' },
|
||||
status: { type: 'string', description: 'Session status' },
|
||||
lastActiveAt: { type: 'number', description: 'Last activity timestamp' },
|
||||
lastSignInAt: { type: 'number', description: 'Last sign-in timestamp' },
|
||||
membersCount: { type: 'number', description: 'Number of members' },
|
||||
maxAllowedMemberships: { type: 'number', description: 'Max allowed memberships' },
|
||||
adminDeleteEnabled: { type: 'boolean', description: 'Whether admin delete is enabled' },
|
||||
createdBy: { type: 'string', description: 'Creator user ID' },
|
||||
publicMetadata: { type: 'json', description: 'Public metadata' },
|
||||
// Common outputs
|
||||
totalCount: { type: 'number', description: 'Total count for paginated results' },
|
||||
deleted: { type: 'boolean', description: 'Whether the resource was deleted' },
|
||||
object: { type: 'string', description: 'Object type' },
|
||||
createdAt: { type: 'number', description: 'Creation timestamp' },
|
||||
updatedAt: { type: 'number', description: 'Last update timestamp' },
|
||||
success: { type: 'boolean', description: 'Operation success status' },
|
||||
},
|
||||
}
|
||||
@@ -162,21 +162,5 @@ export const HumanInTheLoopBlock: BlockConfig<ResponseBlockOutput> = {
|
||||
type: 'string',
|
||||
description: 'Resume API endpoint URL for direct curl requests',
|
||||
},
|
||||
response: {
|
||||
type: 'json',
|
||||
description: 'Display data shown to the approver',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
submission: {
|
||||
type: 'json',
|
||||
description: 'Form submission data from the approver',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
resumeInput: {
|
||||
type: 'json',
|
||||
description: 'Raw input data submitted when resuming',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
submittedAt: { type: 'string', description: 'ISO timestamp when the workflow was resumed' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -247,7 +247,6 @@ export const RouterBlock: BlockConfig<RouterResponse> = {
|
||||
tokens: { type: 'json', description: 'Token usage' },
|
||||
cost: { type: 'json', description: 'Cost information' },
|
||||
selectedPath: { type: 'json', description: 'Selected routing path' },
|
||||
selectedRoute: { type: 'string', description: 'Selected route ID' },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,6 @@ export const WorkflowBlock: BlockConfig = {
|
||||
childWorkflowName: { type: 'string', description: 'Child workflow name' },
|
||||
result: { type: 'json', description: 'Workflow execution result' },
|
||||
error: { type: 'string', description: 'Error message' },
|
||||
childTraceSpans: {
|
||||
type: 'json',
|
||||
description: 'Child workflow trace spans',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
},
|
||||
hideFromToolbar: true,
|
||||
}
|
||||
|
||||
@@ -43,10 +43,5 @@ export const WorkflowInputBlock: BlockConfig = {
|
||||
childWorkflowName: { type: 'string', description: 'Child workflow name' },
|
||||
result: { type: 'json', description: 'Workflow execution result' },
|
||||
error: { type: 'string', description: 'Error message' },
|
||||
childTraceSpans: {
|
||||
type: 'json',
|
||||
description: 'Child workflow trace spans',
|
||||
hiddenFromDisplay: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { CalendlyBlock } from '@/blocks/blocks/calendly'
|
||||
import { ChatTriggerBlock } from '@/blocks/blocks/chat_trigger'
|
||||
import { CirclebackBlock } from '@/blocks/blocks/circleback'
|
||||
import { ClayBlock } from '@/blocks/blocks/clay'
|
||||
import { ClerkBlock } from '@/blocks/blocks/clerk'
|
||||
import { ConditionBlock } from '@/blocks/blocks/condition'
|
||||
import { ConfluenceBlock, ConfluenceV2Block } from '@/blocks/blocks/confluence'
|
||||
import { CursorBlock, CursorV2Block } from '@/blocks/blocks/cursor'
|
||||
@@ -169,7 +168,6 @@ export const registry: Record<string, BlockConfig> = {
|
||||
chat_trigger: ChatTriggerBlock,
|
||||
circleback: CirclebackBlock,
|
||||
clay: ClayBlock,
|
||||
clerk: ClerkBlock,
|
||||
condition: ConditionBlock,
|
||||
confluence: ConfluenceBlock,
|
||||
confluence_v2: ConfluenceV2Block,
|
||||
|
||||
@@ -157,19 +157,8 @@ export type OutputFieldDefinition =
|
||||
* Uses the same condition format as subBlocks.
|
||||
*/
|
||||
condition?: OutputCondition
|
||||
/**
|
||||
* If true, this output is hidden from display in the tag dropdown and logs,
|
||||
* but still available for resolution and execution.
|
||||
*/
|
||||
hiddenFromDisplay?: boolean
|
||||
}
|
||||
|
||||
export function isHiddenFromDisplay(def: unknown): boolean {
|
||||
return Boolean(
|
||||
def && typeof def === 'object' && 'hiddenFromDisplay' in def && def.hiddenFromDisplay
|
||||
)
|
||||
}
|
||||
|
||||
export interface ParamConfig {
|
||||
type: ParamType
|
||||
description?: string
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
.code-editor-theme .token.char,
|
||||
.code-editor-theme .token.builtin,
|
||||
.code-editor-theme .token.inserted {
|
||||
color: #b45309 !important;
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
|
||||
.code-editor-theme .token.operator,
|
||||
@@ -49,7 +49,7 @@
|
||||
.code-editor-theme .token.atrule,
|
||||
.code-editor-theme .token.attr-value,
|
||||
.code-editor-theme .token.keyword {
|
||||
color: #2f55ff !important;
|
||||
color: #2563eb !important;
|
||||
}
|
||||
|
||||
.code-editor-theme .token.function,
|
||||
@@ -119,7 +119,7 @@
|
||||
.dark .code-editor-theme .token.atrule,
|
||||
.dark .code-editor-theme .token.attr-value,
|
||||
.dark .code-editor-theme .token.keyword {
|
||||
color: #2fa1ff !important;
|
||||
color: #4db8ff !important;
|
||||
}
|
||||
|
||||
.dark .code-editor-theme .token.function,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,16 +1,5 @@
|
||||
import type { LoopType, ParallelType } from '@/lib/workflows/types'
|
||||
|
||||
/**
|
||||
* Runtime-injected keys for trigger blocks that should be hidden from logs/display.
|
||||
* These are added during execution but aren't part of the block's static output schema.
|
||||
*/
|
||||
export const TRIGGER_INTERNAL_KEYS = ['webhook', 'workflowId'] as const
|
||||
export type TriggerInternalKey = (typeof TRIGGER_INTERNAL_KEYS)[number]
|
||||
|
||||
export function isTriggerInternalKey(key: string): key is TriggerInternalKey {
|
||||
return TRIGGER_INTERNAL_KEYS.includes(key as TriggerInternalKey)
|
||||
}
|
||||
|
||||
export enum BlockType {
|
||||
PARALLEL = 'parallel',
|
||||
LOOP = 'loop',
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
DEFAULTS,
|
||||
EDGE,
|
||||
isSentinelBlockType,
|
||||
isTriggerBehavior,
|
||||
isWorkflowBlockType,
|
||||
} from '@/executor/constants'
|
||||
import type { DAGNode } from '@/executor/dag/builder'
|
||||
import { ChildWorkflowError } from '@/executor/errors/child-workflow-error'
|
||||
@@ -28,8 +30,6 @@ import type {
|
||||
} from '@/executor/types'
|
||||
import { streamingResponseFormatProcessor } from '@/executor/utils'
|
||||
import { buildBlockExecutionError, normalizeError } from '@/executor/utils/errors'
|
||||
import { isJSONString } from '@/executor/utils/json'
|
||||
import { filterOutputForLog } from '@/executor/utils/output-filter'
|
||||
import { validateBlockType } from '@/executor/utils/permission-check'
|
||||
import type { VariableResolver } from '@/executor/variables/resolver'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
@@ -87,7 +87,7 @@ export class BlockExecutor {
|
||||
resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block)
|
||||
|
||||
if (blockLog) {
|
||||
blockLog.input = this.parseJsonInputs(resolvedInputs)
|
||||
blockLog.input = resolvedInputs
|
||||
}
|
||||
} catch (error) {
|
||||
cleanupSelfReference?.()
|
||||
@@ -149,23 +149,14 @@ export class BlockExecutor {
|
||||
blockLog.endedAt = new Date().toISOString()
|
||||
blockLog.durationMs = duration
|
||||
blockLog.success = true
|
||||
blockLog.output = filterOutputForLog(block.metadata?.id || '', normalizedOutput, { block })
|
||||
blockLog.output = this.filterOutputForLog(block, normalizedOutput)
|
||||
}
|
||||
|
||||
this.state.setBlockOutput(node.id, normalizedOutput, duration)
|
||||
|
||||
if (!isSentinel) {
|
||||
const displayOutput = filterOutputForLog(block.metadata?.id || '', normalizedOutput, {
|
||||
block,
|
||||
})
|
||||
this.callOnBlockComplete(
|
||||
ctx,
|
||||
node,
|
||||
block,
|
||||
this.parseJsonInputs(resolvedInputs),
|
||||
displayOutput,
|
||||
duration
|
||||
)
|
||||
const displayOutput = this.filterOutputForDisplay(block, normalizedOutput)
|
||||
this.callOnBlockComplete(ctx, node, block, resolvedInputs, displayOutput, duration)
|
||||
}
|
||||
|
||||
return normalizedOutput
|
||||
@@ -241,8 +232,8 @@ export class BlockExecutor {
|
||||
blockLog.durationMs = duration
|
||||
blockLog.success = false
|
||||
blockLog.error = errorMessage
|
||||
blockLog.input = this.parseJsonInputs(input)
|
||||
blockLog.output = filterOutputForLog(block.metadata?.id || '', errorOutput, { block })
|
||||
blockLog.input = input
|
||||
blockLog.output = this.filterOutputForLog(block, errorOutput)
|
||||
}
|
||||
|
||||
logger.error(
|
||||
@@ -255,15 +246,8 @@ export class BlockExecutor {
|
||||
)
|
||||
|
||||
if (!isSentinel) {
|
||||
const displayOutput = filterOutputForLog(block.metadata?.id || '', errorOutput, { block })
|
||||
this.callOnBlockComplete(
|
||||
ctx,
|
||||
node,
|
||||
block,
|
||||
this.parseJsonInputs(input),
|
||||
displayOutput,
|
||||
duration
|
||||
)
|
||||
const displayOutput = this.filterOutputForDisplay(block, errorOutput)
|
||||
this.callOnBlockComplete(ctx, node, block, input, displayOutput, duration)
|
||||
}
|
||||
|
||||
const hasErrorPort = this.hasErrorPortEdge(node)
|
||||
@@ -351,34 +335,49 @@ export class BlockExecutor {
|
||||
return { result: output }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON string inputs to objects for log display only.
|
||||
* Attempts to parse any string that looks like JSON.
|
||||
* Returns a new object - does not mutate the original inputs.
|
||||
*/
|
||||
private parseJsonInputs(inputs: Record<string, any>): Record<string, any> {
|
||||
let result = inputs
|
||||
let hasChanges = false
|
||||
private filterOutputForLog(
|
||||
block: SerializedBlock,
|
||||
output: NormalizedBlockOutput
|
||||
): NormalizedBlockOutput {
|
||||
const blockType = block.metadata?.id
|
||||
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
// isJSONString is a quick heuristic (checks for { or [), not a validator.
|
||||
// Invalid JSON is safely caught below - this just avoids JSON.parse on every string.
|
||||
if (typeof value !== 'string' || !isJSONString(value)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
if (!hasChanges) {
|
||||
result = { ...inputs }
|
||||
hasChanges = true
|
||||
}
|
||||
result[key] = JSON.parse(value.trim())
|
||||
} catch {
|
||||
// Not valid JSON, keep original string
|
||||
if (blockType === BlockType.HUMAN_IN_THE_LOOP) {
|
||||
const filtered: NormalizedBlockOutput = {}
|
||||
for (const [key, value] of Object.entries(output)) {
|
||||
if (key.startsWith('_')) continue
|
||||
if (key === 'response') continue
|
||||
filtered[key] = value
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
return result
|
||||
if (isTriggerBehavior(block)) {
|
||||
const filtered: NormalizedBlockOutput = {}
|
||||
const internalKeys = ['webhook', 'workflowId']
|
||||
for (const [key, value] of Object.entries(output)) {
|
||||
if (internalKeys.includes(key)) continue
|
||||
filtered[key] = value
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
private filterOutputForDisplay(
|
||||
block: SerializedBlock,
|
||||
output: NormalizedBlockOutput
|
||||
): NormalizedBlockOutput {
|
||||
const filtered = this.filterOutputForLog(block, output)
|
||||
|
||||
if (isWorkflowBlockType(block.metadata?.id)) {
|
||||
const { childTraceSpans: _, ...displayOutput } = filtered as {
|
||||
childTraceSpans?: unknown
|
||||
} & Record<string, unknown>
|
||||
return displayOutput
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
private callOnBlockStart(ctx: ExecutionContext, node: DAGNode, block: SerializedBlock): void {
|
||||
|
||||
@@ -936,12 +936,8 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
systemPrompt: validMessages ? undefined : inputs.systemPrompt,
|
||||
context: validMessages ? undefined : stringifyJSON(messages),
|
||||
tools: formattedTools,
|
||||
temperature:
|
||||
inputs.temperature != null && inputs.temperature !== ''
|
||||
? Number(inputs.temperature)
|
||||
: undefined,
|
||||
maxTokens:
|
||||
inputs.maxTokens != null && inputs.maxTokens !== '' ? Number(inputs.maxTokens) : undefined,
|
||||
temperature: inputs.temperature,
|
||||
maxTokens: inputs.maxTokens,
|
||||
apiKey: inputs.apiKey,
|
||||
azureEndpoint: inputs.azureEndpoint,
|
||||
azureApiVersion: inputs.azureApiVersion,
|
||||
|
||||
@@ -14,8 +14,8 @@ export interface AgentInputs {
|
||||
slidingWindowSize?: string // For message-based sliding window
|
||||
slidingWindowTokens?: string // For token-based sliding window
|
||||
// LLM parameters
|
||||
temperature?: string
|
||||
maxTokens?: string
|
||||
temperature?: number
|
||||
maxTokens?: number
|
||||
apiKey?: string
|
||||
azureEndpoint?: string
|
||||
azureApiVersion?: string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { BlockType, isTriggerBehavior, isTriggerInternalKey } from '@/executor/constants'
|
||||
import { BlockType, isTriggerBehavior } from '@/executor/constants'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
|
||||
@@ -33,12 +33,7 @@ export class TriggerBlockHandler implements BlockHandler {
|
||||
const starterOutput = starterState.output
|
||||
|
||||
if (starterOutput.webhook?.data) {
|
||||
const cleanOutput: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(starterOutput)) {
|
||||
if (!isTriggerInternalKey(key)) {
|
||||
cleanOutput[key] = value
|
||||
}
|
||||
}
|
||||
const { webhook, workflowId, ...cleanOutput } = starterOutput
|
||||
return cleanOutput
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { getBlock } from '@/blocks'
|
||||
import { isHiddenFromDisplay } from '@/blocks/types'
|
||||
import { isTriggerBehavior, isTriggerInternalKey } from '@/executor/constants'
|
||||
import type { NormalizedBlockOutput } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
|
||||
/**
|
||||
* Filters block output for logging/display purposes.
|
||||
* Removes internal fields and fields marked with hiddenFromDisplay.
|
||||
*
|
||||
* @param blockType - The block type string (e.g., 'human_in_the_loop', 'workflow')
|
||||
* @param output - The raw block output to filter
|
||||
* @param options - Optional configuration
|
||||
* @param options.block - Full SerializedBlock for trigger behavior detection
|
||||
* @param options.additionalHiddenKeys - Extra keys to filter out (e.g., 'resume')
|
||||
*/
|
||||
export function filterOutputForLog(
|
||||
blockType: string,
|
||||
output: NormalizedBlockOutput,
|
||||
options?: {
|
||||
block?: SerializedBlock
|
||||
additionalHiddenKeys?: string[]
|
||||
}
|
||||
): NormalizedBlockOutput {
|
||||
const blockConfig = blockType ? getBlock(blockType) : undefined
|
||||
const filtered: NormalizedBlockOutput = {}
|
||||
const additionalHiddenKeys = options?.additionalHiddenKeys ?? []
|
||||
|
||||
for (const [key, value] of Object.entries(output)) {
|
||||
// Skip internal keys (underscore prefix)
|
||||
if (key.startsWith('_')) continue
|
||||
|
||||
if (blockConfig?.outputs && isHiddenFromDisplay(blockConfig.outputs[key])) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip runtime-injected trigger keys not in block config
|
||||
if (options?.block && isTriggerBehavior(options.block) && isTriggerInternalKey(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip additional hidden keys specified by caller
|
||||
if (additionalHiddenKeys.includes(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
filtered[key] = value
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||
import { isHiddenFromDisplay } from '@/blocks/types'
|
||||
import { escapeRegExp } from '@/executor/constants'
|
||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
||||
import type { ChatContext } from '@/stores/panel/copilot/types'
|
||||
@@ -398,11 +397,7 @@ async function processBlockMetadata(
|
||||
category: blockConfig.category,
|
||||
bgColor: blockConfig.bgColor,
|
||||
inputs: blockConfig.inputs || {},
|
||||
outputs: blockConfig.outputs
|
||||
? Object.fromEntries(
|
||||
Object.entries(blockConfig.outputs).filter(([_, def]) => !isHiddenFromDisplay(def))
|
||||
)
|
||||
: {},
|
||||
outputs: blockConfig.outputs || {},
|
||||
tools: blockConfig.tools?.access || [],
|
||||
hideFromToolbar: blockConfig.hideFromToolbar,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type GetBlockConfigResultType,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { registry as blockRegistry, getLatestBlock } from '@/blocks/registry'
|
||||
import { isHiddenFromDisplay, type SubBlockConfig } from '@/blocks/types'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
||||
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
||||
import { tools as toolsRegistry } from '@/tools/registry'
|
||||
@@ -310,7 +310,6 @@ function extractTriggerOutputs(blockConfig: any): Record<string, OutputFieldSche
|
||||
const trigger = getTrigger(triggerId)
|
||||
if (trigger.outputs) {
|
||||
for (const [key, def] of Object.entries(trigger.outputs)) {
|
||||
if (isHiddenFromDisplay(def)) continue
|
||||
outputs[key] = extractOutputField(def)
|
||||
}
|
||||
}
|
||||
@@ -343,7 +342,6 @@ function extractOutputs(
|
||||
const tool = toolsRegistry[toolId]
|
||||
if (tool?.outputs) {
|
||||
for (const [key, def] of Object.entries(tool.outputs)) {
|
||||
if (isHiddenFromDisplay(def)) continue
|
||||
outputs[key] = extractOutputField(def)
|
||||
}
|
||||
return outputs
|
||||
@@ -357,7 +355,6 @@ function extractOutputs(
|
||||
// Use block-level outputs
|
||||
if (blockConfig.outputs) {
|
||||
for (const [key, def] of Object.entries(blockConfig.outputs)) {
|
||||
if (isHiddenFromDisplay(def)) continue
|
||||
outputs[key] = extractOutputField(def)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
GetBlocksMetadataResult,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { registry as blockRegistry } from '@/blocks/registry'
|
||||
import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getUserPermissionConfig } from '@/executor/utils/permission-check'
|
||||
import { PROVIDER_DEFINITIONS } from '@/providers/models'
|
||||
import { tools as toolsRegistry } from '@/tools/registry'
|
||||
@@ -248,12 +249,6 @@ export const getBlocksMetadataServerTool: BaseServerTool<
|
||||
}
|
||||
}
|
||||
|
||||
const filteredOutputs = blockConfig.outputs
|
||||
? Object.fromEntries(
|
||||
Object.entries(blockConfig.outputs).filter(([_, def]) => !isHiddenFromDisplay(def))
|
||||
)
|
||||
: undefined
|
||||
|
||||
metadata = {
|
||||
id: blockId,
|
||||
name: blockConfig.name || blockId,
|
||||
@@ -267,7 +262,7 @@ export const getBlocksMetadataServerTool: BaseServerTool<
|
||||
triggers,
|
||||
operationInputSchema: operationParameters,
|
||||
operations,
|
||||
outputs: filteredOutputs,
|
||||
outputs: blockConfig.outputs,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const ivm = require('isolated-vm')
|
||||
const USER_CODE_START_LINE = 4
|
||||
const pendingFetches = new Map()
|
||||
let fetchIdCounter = 0
|
||||
const FETCH_TIMEOUT_MS = 300000 // 5 minutes
|
||||
const FETCH_TIMEOUT_MS = 30000
|
||||
|
||||
/**
|
||||
* Extract line and column from error stack or message
|
||||
|
||||
@@ -34,7 +34,6 @@ export function layoutContainers(
|
||||
: DEFAULT_CONTAINER_HORIZONTAL_SPACING,
|
||||
verticalSpacing: options.verticalSpacing ?? DEFAULT_VERTICAL_SPACING,
|
||||
padding: { x: CONTAINER_PADDING_X, y: CONTAINER_PADDING_Y },
|
||||
gridSize: options.gridSize,
|
||||
}
|
||||
|
||||
for (const [parentId, childIds] of children.entries()) {
|
||||
@@ -57,15 +56,18 @@ export function layoutContainers(
|
||||
continue
|
||||
}
|
||||
|
||||
// Use the shared core layout function with container options
|
||||
const { nodes, dimensions } = layoutBlocksCore(childBlocks, childEdges, {
|
||||
isContainer: true,
|
||||
layoutOptions: containerOptions,
|
||||
})
|
||||
|
||||
// Apply positions back to blocks
|
||||
for (const node of nodes.values()) {
|
||||
blocks[node.id].position = node.position
|
||||
}
|
||||
|
||||
// Update container dimensions
|
||||
const calculatedWidth = dimensions.width
|
||||
const calculatedHeight = dimensions.height
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
getBlockMetrics,
|
||||
normalizePositions,
|
||||
prepareBlockMetrics,
|
||||
snapNodesToGrid,
|
||||
} from '@/lib/workflows/autolayout/utils'
|
||||
import { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
import { EDGE } from '@/executor/constants'
|
||||
@@ -85,6 +84,7 @@ export function assignLayers(
|
||||
): Map<string, GraphNode> {
|
||||
const nodes = new Map<string, GraphNode>()
|
||||
|
||||
// Initialize nodes
|
||||
for (const [id, block] of Object.entries(blocks)) {
|
||||
nodes.set(id, {
|
||||
id,
|
||||
@@ -97,6 +97,7 @@ export function assignLayers(
|
||||
})
|
||||
}
|
||||
|
||||
// Build a map of target node -> edges coming into it (to check sourceHandle later)
|
||||
const incomingEdgesMap = new Map<string, Edge[]>()
|
||||
for (const edge of edges) {
|
||||
if (!incomingEdgesMap.has(edge.target)) {
|
||||
@@ -105,6 +106,7 @@ export function assignLayers(
|
||||
incomingEdgesMap.get(edge.target)!.push(edge)
|
||||
}
|
||||
|
||||
// Build adjacency from edges
|
||||
for (const edge of edges) {
|
||||
const sourceNode = nodes.get(edge.source)
|
||||
const targetNode = nodes.get(edge.target)
|
||||
@@ -115,6 +117,7 @@ export function assignLayers(
|
||||
}
|
||||
}
|
||||
|
||||
// Find starter nodes (no incoming edges)
|
||||
const starterNodes = Array.from(nodes.values()).filter((node) => node.incoming.size === 0)
|
||||
|
||||
if (starterNodes.length === 0 && nodes.size > 0) {
|
||||
@@ -123,6 +126,7 @@ export function assignLayers(
|
||||
logger.warn('No starter blocks found, using first block as starter', { blockId: firstNode.id })
|
||||
}
|
||||
|
||||
// Topological sort using Kahn's algorithm
|
||||
const inDegreeCount = new Map<string, number>()
|
||||
|
||||
for (const node of nodes.values()) {
|
||||
@@ -140,6 +144,8 @@ export function assignLayers(
|
||||
const node = nodes.get(nodeId)!
|
||||
processed.add(nodeId)
|
||||
|
||||
// Calculate layer based on max incoming layer + 1
|
||||
// For edges from subflow ends, add the subflow's internal depth (minus 1 to avoid double-counting)
|
||||
if (node.incoming.size > 0) {
|
||||
let maxEffectiveLayer = -1
|
||||
const incomingEdges = incomingEdgesMap.get(nodeId) || []
|
||||
@@ -147,11 +153,16 @@ export function assignLayers(
|
||||
for (const incomingId of node.incoming) {
|
||||
const incomingNode = nodes.get(incomingId)
|
||||
if (incomingNode) {
|
||||
// Find edges from this incoming node to check if it's a subflow end edge
|
||||
const edgesFromSource = incomingEdges.filter((e) => e.source === incomingId)
|
||||
let additionalDepth = 0
|
||||
|
||||
// Check if any edge from this source is a subflow end edge
|
||||
const hasSubflowEndEdge = edgesFromSource.some(isSubflowEndEdge)
|
||||
if (hasSubflowEndEdge && subflowDepths) {
|
||||
// Get the internal depth of the subflow
|
||||
// Subtract 1 because the +1 at the end of layer calculation already accounts for one layer
|
||||
// E.g., if subflow has 2 internal layers (depth=2), we add 1 extra so total offset is 2
|
||||
const depth = subflowDepths.get(incomingId) ?? 1
|
||||
additionalDepth = Math.max(0, depth - 1)
|
||||
}
|
||||
@@ -163,6 +174,7 @@ export function assignLayers(
|
||||
node.layer = maxEffectiveLayer + 1
|
||||
}
|
||||
|
||||
// Add outgoing nodes when all dependencies processed
|
||||
for (const targetId of node.outgoing) {
|
||||
const currentCount = inDegreeCount.get(targetId) || 0
|
||||
inDegreeCount.set(targetId, currentCount - 1)
|
||||
@@ -173,6 +185,7 @@ export function assignLayers(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle isolated nodes
|
||||
for (const node of nodes.values()) {
|
||||
if (!processed.has(node.id)) {
|
||||
logger.debug('Isolated node detected, assigning to layer 0', { blockId: node.id })
|
||||
@@ -211,6 +224,7 @@ function resolveVerticalOverlaps(nodes: GraphNode[], verticalSpacing: number): v
|
||||
hasOverlap = false
|
||||
iteration++
|
||||
|
||||
// Group nodes by layer for same-layer overlap resolution
|
||||
const nodesByLayer = new Map<number, GraphNode[]>()
|
||||
for (const node of nodes) {
|
||||
if (!nodesByLayer.has(node.layer)) {
|
||||
@@ -219,9 +233,11 @@ function resolveVerticalOverlaps(nodes: GraphNode[], verticalSpacing: number): v
|
||||
nodesByLayer.get(node.layer)!.push(node)
|
||||
}
|
||||
|
||||
// Process each layer independently
|
||||
for (const [layer, layerNodes] of nodesByLayer) {
|
||||
if (layerNodes.length < 2) continue
|
||||
|
||||
// Sort by Y position for consistent processing
|
||||
layerNodes.sort((a, b) => a.position.y - b.position.y)
|
||||
|
||||
for (let i = 0; i < layerNodes.length - 1; i++) {
|
||||
@@ -286,6 +302,7 @@ export function calculatePositions(
|
||||
|
||||
const layerNumbers = Array.from(layers.keys()).sort((a, b) => a - b)
|
||||
|
||||
// Calculate max width for each layer
|
||||
const layerWidths = new Map<number, number>()
|
||||
for (const layerNum of layerNumbers) {
|
||||
const nodesInLayer = layers.get(layerNum)!
|
||||
@@ -293,6 +310,7 @@ export function calculatePositions(
|
||||
layerWidths.set(layerNum, maxWidth)
|
||||
}
|
||||
|
||||
// Calculate cumulative X positions for each layer based on actual widths
|
||||
const layerXPositions = new Map<number, number>()
|
||||
let cumulativeX = padding.x
|
||||
|
||||
@@ -301,6 +319,7 @@ export function calculatePositions(
|
||||
cumulativeX += layerWidths.get(layerNum)! + horizontalSpacing
|
||||
}
|
||||
|
||||
// Build a flat map of all nodes for quick lookups
|
||||
const allNodes = new Map<string, GraphNode>()
|
||||
for (const nodesInLayer of layers.values()) {
|
||||
for (const node of nodesInLayer) {
|
||||
@@ -308,6 +327,7 @@ export function calculatePositions(
|
||||
}
|
||||
}
|
||||
|
||||
// Build incoming edges map for handle lookups
|
||||
const incomingEdgesMap = new Map<string, Edge[]>()
|
||||
for (const edge of edges) {
|
||||
if (!incomingEdgesMap.has(edge.target)) {
|
||||
@@ -316,16 +336,20 @@ export function calculatePositions(
|
||||
incomingEdgesMap.get(edge.target)!.push(edge)
|
||||
}
|
||||
|
||||
// Position nodes layer by layer, aligning with connected predecessors
|
||||
for (const layerNum of layerNumbers) {
|
||||
const nodesInLayer = layers.get(layerNum)!
|
||||
const xPosition = layerXPositions.get(layerNum)!
|
||||
|
||||
// Separate containers and non-containers
|
||||
const containersInLayer = nodesInLayer.filter(isContainerBlock)
|
||||
const nonContainersInLayer = nodesInLayer.filter((n) => !isContainerBlock(n))
|
||||
|
||||
// For the first layer (layer 0), position sequentially from padding.y
|
||||
if (layerNum === 0) {
|
||||
let yOffset = padding.y
|
||||
|
||||
// Sort containers by height for visual balance
|
||||
containersInLayer.sort((a, b) => b.metrics.height - a.metrics.height)
|
||||
|
||||
for (const node of containersInLayer) {
|
||||
@@ -337,6 +361,7 @@ export function calculatePositions(
|
||||
yOffset += CONTAINER_VERTICAL_CLEARANCE
|
||||
}
|
||||
|
||||
// Sort non-containers by outgoing connections
|
||||
nonContainersInLayer.sort((a, b) => b.outgoing.size - a.outgoing.size)
|
||||
|
||||
for (const node of nonContainersInLayer) {
|
||||
@@ -346,7 +371,9 @@ export function calculatePositions(
|
||||
continue
|
||||
}
|
||||
|
||||
// For subsequent layers, align with connected predecessors (handle-to-handle)
|
||||
for (const node of [...containersInLayer, ...nonContainersInLayer]) {
|
||||
// Find the bottommost predecessor handle Y (highest value) and align to it
|
||||
let bestSourceHandleY = -1
|
||||
let bestEdge: Edge | null = null
|
||||
const incomingEdges = incomingEdgesMap.get(node.id) || []
|
||||
@@ -354,6 +381,7 @@ export function calculatePositions(
|
||||
for (const edge of incomingEdges) {
|
||||
const predecessor = allNodes.get(edge.source)
|
||||
if (predecessor) {
|
||||
// Calculate actual source handle Y position based on block type and handle
|
||||
const sourceHandleOffset = getSourceHandleYOffset(predecessor.block, edge.sourceHandle)
|
||||
const sourceHandleY = predecessor.position.y + sourceHandleOffset
|
||||
|
||||
@@ -364,16 +392,20 @@ export function calculatePositions(
|
||||
}
|
||||
}
|
||||
|
||||
// If no predecessors found (shouldn't happen for layer > 0), use padding
|
||||
if (bestSourceHandleY < 0) {
|
||||
bestSourceHandleY = padding.y + HANDLE_POSITIONS.DEFAULT_Y_OFFSET
|
||||
}
|
||||
|
||||
// Calculate the target handle Y offset for this node
|
||||
const targetHandleOffset = getTargetHandleYOffset(node.block, bestEdge?.targetHandle)
|
||||
|
||||
// Position node so its target handle aligns with the source handle Y
|
||||
node.position = { x: xPosition, y: bestSourceHandleY - targetHandleOffset }
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve vertical overlaps within layers (X overlaps prevented by cumulative positioning)
|
||||
resolveVerticalOverlaps(Array.from(layers.values()).flat(), verticalSpacing)
|
||||
}
|
||||
|
||||
@@ -403,7 +435,7 @@ export function layoutBlocksCore(
|
||||
return { nodes: new Map(), dimensions: { width: 0, height: 0 } }
|
||||
}
|
||||
|
||||
const layoutOptions: LayoutOptions =
|
||||
const layoutOptions =
|
||||
options.layoutOptions ??
|
||||
(options.isContainer ? CONTAINER_LAYOUT_OPTIONS : DEFAULT_LAYOUT_OPTIONS)
|
||||
|
||||
@@ -420,13 +452,7 @@ export function layoutBlocksCore(
|
||||
calculatePositions(layers, edges, layoutOptions)
|
||||
|
||||
// 5. Normalize positions
|
||||
let dimensions = normalizePositions(nodes, { isContainer: options.isContainer })
|
||||
|
||||
// 6. Snap to grid if gridSize is specified (recalculates dimensions)
|
||||
const snappedDimensions = snapNodesToGrid(nodes, layoutOptions.gridSize)
|
||||
if (snappedDimensions) {
|
||||
dimensions = snappedDimensions
|
||||
}
|
||||
const dimensions = normalizePositions(nodes, { isContainer: options.isContainer })
|
||||
|
||||
return { nodes, dimensions }
|
||||
}
|
||||
|
||||
@@ -36,13 +36,14 @@ export function applyAutoLayout(
|
||||
const horizontalSpacing = options.horizontalSpacing ?? DEFAULT_HORIZONTAL_SPACING
|
||||
const verticalSpacing = options.verticalSpacing ?? DEFAULT_VERTICAL_SPACING
|
||||
|
||||
// Pre-calculate container dimensions by laying out their children (bottom-up)
|
||||
// This ensures accurate widths/heights before root-level layout
|
||||
prepareContainerDimensions(
|
||||
blocksCopy,
|
||||
edges,
|
||||
layoutBlocksCore,
|
||||
horizontalSpacing,
|
||||
verticalSpacing,
|
||||
options.gridSize
|
||||
verticalSpacing
|
||||
)
|
||||
|
||||
const { root: rootBlockIds } = getBlocksByParent(blocksCopy)
|
||||
@@ -57,6 +58,8 @@ export function applyAutoLayout(
|
||||
(edge) => layoutRootIds.includes(edge.source) && layoutRootIds.includes(edge.target)
|
||||
)
|
||||
|
||||
// Calculate subflow depths before laying out root blocks
|
||||
// This ensures blocks connected to subflow ends are positioned correctly
|
||||
const subflowDepths = calculateSubflowDepths(blocksCopy, edges, assignLayers)
|
||||
|
||||
if (Object.keys(rootBlocks).length > 0) {
|
||||
@@ -92,12 +95,13 @@ export function applyAutoLayout(
|
||||
}
|
||||
|
||||
export type { TargetedLayoutOptions } from '@/lib/workflows/autolayout/targeted'
|
||||
// Function exports
|
||||
export { applyTargetedLayout } from '@/lib/workflows/autolayout/targeted'
|
||||
// Type exports
|
||||
export type { Edge, LayoutOptions, LayoutResult } from '@/lib/workflows/autolayout/types'
|
||||
export {
|
||||
getBlockMetrics,
|
||||
isContainerType,
|
||||
shouldSkipAutoLayout,
|
||||
snapPositionToGrid,
|
||||
transferBlockHeights,
|
||||
} from '@/lib/workflows/autolayout/utils'
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import {
|
||||
CONTAINER_PADDING,
|
||||
DEFAULT_HORIZONTAL_SPACING,
|
||||
@@ -13,11 +14,12 @@ import {
|
||||
isContainerType,
|
||||
prepareContainerDimensions,
|
||||
shouldSkipAutoLayout,
|
||||
snapPositionToGrid,
|
||||
} from '@/lib/workflows/autolayout/utils'
|
||||
import { CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('AutoLayout:Targeted')
|
||||
|
||||
export interface TargetedLayoutOptions extends LayoutOptions {
|
||||
changedBlockIds: string[]
|
||||
verticalSpacing?: number
|
||||
@@ -37,7 +39,6 @@ export function applyTargetedLayout(
|
||||
changedBlockIds,
|
||||
verticalSpacing = DEFAULT_VERTICAL_SPACING,
|
||||
horizontalSpacing = DEFAULT_HORIZONTAL_SPACING,
|
||||
gridSize,
|
||||
} = options
|
||||
|
||||
if (!changedBlockIds || changedBlockIds.length === 0) {
|
||||
@@ -47,17 +48,19 @@ export function applyTargetedLayout(
|
||||
const changedSet = new Set(changedBlockIds)
|
||||
const blocksCopy: Record<string, BlockState> = JSON.parse(JSON.stringify(blocks))
|
||||
|
||||
// Pre-calculate container dimensions by laying out their children (bottom-up)
|
||||
// This ensures accurate widths/heights before root-level layout
|
||||
prepareContainerDimensions(
|
||||
blocksCopy,
|
||||
edges,
|
||||
layoutBlocksCore,
|
||||
horizontalSpacing,
|
||||
verticalSpacing,
|
||||
gridSize
|
||||
verticalSpacing
|
||||
)
|
||||
|
||||
const groups = getBlocksByParent(blocksCopy)
|
||||
|
||||
// Calculate subflow depths before layout to properly position blocks after subflow ends
|
||||
const subflowDepths = calculateSubflowDepths(blocksCopy, edges, assignLayers)
|
||||
|
||||
layoutGroup(
|
||||
@@ -68,8 +71,7 @@ export function applyTargetedLayout(
|
||||
changedSet,
|
||||
verticalSpacing,
|
||||
horizontalSpacing,
|
||||
subflowDepths,
|
||||
gridSize
|
||||
subflowDepths
|
||||
)
|
||||
|
||||
for (const [parentId, childIds] of groups.children.entries()) {
|
||||
@@ -81,8 +83,7 @@ export function applyTargetedLayout(
|
||||
changedSet,
|
||||
verticalSpacing,
|
||||
horizontalSpacing,
|
||||
subflowDepths,
|
||||
gridSize
|
||||
subflowDepths
|
||||
)
|
||||
}
|
||||
|
||||
@@ -100,8 +101,7 @@ function layoutGroup(
|
||||
changedSet: Set<string>,
|
||||
verticalSpacing: number,
|
||||
horizontalSpacing: number,
|
||||
subflowDepths: Map<string, number>,
|
||||
gridSize?: number
|
||||
subflowDepths: Map<string, number>
|
||||
): void {
|
||||
if (childIds.length === 0) return
|
||||
|
||||
@@ -116,6 +116,7 @@ function layoutGroup(
|
||||
return
|
||||
}
|
||||
|
||||
// Determine which blocks need repositioning
|
||||
const requestedLayout = layoutEligibleChildIds.filter((id) => {
|
||||
const block = blocks[id]
|
||||
if (!block) return false
|
||||
@@ -140,6 +141,7 @@ function layoutGroup(
|
||||
return
|
||||
}
|
||||
|
||||
// Store old positions for anchor calculation
|
||||
const oldPositions = new Map<string, { x: number; y: number }>()
|
||||
for (const id of layoutEligibleChildIds) {
|
||||
const block = blocks[id]
|
||||
@@ -147,6 +149,8 @@ function layoutGroup(
|
||||
oldPositions.set(id, { ...block.position })
|
||||
}
|
||||
|
||||
// Compute layout positions using core function
|
||||
// Only pass subflowDepths for root-level layout (not inside containers)
|
||||
const layoutPositions = computeLayoutPositions(
|
||||
layoutEligibleChildIds,
|
||||
blocks,
|
||||
@@ -154,8 +158,7 @@ function layoutGroup(
|
||||
parentBlock,
|
||||
horizontalSpacing,
|
||||
verticalSpacing,
|
||||
parentId === null ? subflowDepths : undefined,
|
||||
gridSize
|
||||
parentId === null ? subflowDepths : undefined
|
||||
)
|
||||
|
||||
if (layoutPositions.size === 0) {
|
||||
@@ -165,6 +168,7 @@ function layoutGroup(
|
||||
return
|
||||
}
|
||||
|
||||
// Find anchor block (unchanged block with a layout position)
|
||||
let offsetX = 0
|
||||
let offsetY = 0
|
||||
|
||||
@@ -181,16 +185,20 @@ function layoutGroup(
|
||||
}
|
||||
}
|
||||
|
||||
// Apply new positions only to blocks that need layout
|
||||
for (const id of needsLayout) {
|
||||
const block = blocks[id]
|
||||
const newPos = layoutPositions.get(id)
|
||||
if (!block || !newPos) continue
|
||||
block.position = snapPositionToGrid({ x: newPos.x + offsetX, y: newPos.y + offsetY }, gridSize)
|
||||
block.position = {
|
||||
x: newPos.x + offsetX,
|
||||
y: newPos.y + offsetY,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes layout positions for a subset of blocks using the core layout function
|
||||
* Computes layout positions for a subset of blocks using the core layout
|
||||
*/
|
||||
function computeLayoutPositions(
|
||||
childIds: string[],
|
||||
@@ -199,8 +207,7 @@ function computeLayoutPositions(
|
||||
parentBlock: BlockState | undefined,
|
||||
horizontalSpacing: number,
|
||||
verticalSpacing: number,
|
||||
subflowDepths?: Map<string, number>,
|
||||
gridSize?: number
|
||||
subflowDepths?: Map<string, number>
|
||||
): Map<string, { x: number; y: number }> {
|
||||
const subsetBlocks: Record<string, BlockState> = {}
|
||||
for (const id of childIds) {
|
||||
@@ -221,11 +228,11 @@ function computeLayoutPositions(
|
||||
layoutOptions: {
|
||||
horizontalSpacing: isContainer ? horizontalSpacing * 0.85 : horizontalSpacing,
|
||||
verticalSpacing,
|
||||
gridSize,
|
||||
},
|
||||
subflowDepths,
|
||||
})
|
||||
|
||||
// Update parent container dimensions if applicable
|
||||
if (parentBlock) {
|
||||
parentBlock.data = {
|
||||
...parentBlock.data,
|
||||
@@ -234,6 +241,7 @@ function computeLayoutPositions(
|
||||
}
|
||||
}
|
||||
|
||||
// Convert nodes to position map
|
||||
const positions = new Map<string, { x: number; y: number }>()
|
||||
for (const node of nodes.values()) {
|
||||
positions.set(node.id, { x: node.position.x, y: node.position.y })
|
||||
|
||||
@@ -7,7 +7,6 @@ export interface LayoutOptions {
|
||||
horizontalSpacing?: number
|
||||
verticalSpacing?: number
|
||||
padding?: { x: number; y: number }
|
||||
gridSize?: number
|
||||
}
|
||||
|
||||
export interface LayoutResult {
|
||||
|
||||
@@ -18,61 +18,6 @@ function resolveNumeric(value: number | undefined, fallback: number): number {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Snaps a single coordinate value to the nearest grid position
|
||||
*/
|
||||
function snapToGrid(value: number, gridSize: number): number {
|
||||
return Math.round(value / gridSize) * gridSize
|
||||
}
|
||||
|
||||
/**
|
||||
* Snaps a position to the nearest grid point.
|
||||
* Returns the original position if gridSize is 0 or not provided.
|
||||
*/
|
||||
export function snapPositionToGrid(
|
||||
position: { x: number; y: number },
|
||||
gridSize: number | undefined
|
||||
): { x: number; y: number } {
|
||||
if (!gridSize || gridSize <= 0) {
|
||||
return position
|
||||
}
|
||||
return {
|
||||
x: snapToGrid(position.x, gridSize),
|
||||
y: snapToGrid(position.y, gridSize),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Snaps all node positions in a graph to grid positions and returns updated dimensions.
|
||||
* Returns null if gridSize is not set or no snapping was needed.
|
||||
*/
|
||||
export function snapNodesToGrid(
|
||||
nodes: Map<string, GraphNode>,
|
||||
gridSize: number | undefined
|
||||
): { width: number; height: number } | null {
|
||||
if (!gridSize || gridSize <= 0 || nodes.size === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
let minX = Number.POSITIVE_INFINITY
|
||||
let minY = Number.POSITIVE_INFINITY
|
||||
let maxX = Number.NEGATIVE_INFINITY
|
||||
let maxY = Number.NEGATIVE_INFINITY
|
||||
|
||||
for (const node of nodes.values()) {
|
||||
node.position = snapPositionToGrid(node.position, gridSize)
|
||||
minX = Math.min(minX, node.position.x)
|
||||
minY = Math.min(minY, node.position.y)
|
||||
maxX = Math.max(maxX, node.position.x + node.metrics.width)
|
||||
maxY = Math.max(maxY, node.position.y + node.metrics.height)
|
||||
}
|
||||
|
||||
return {
|
||||
width: maxX - minX + CONTAINER_PADDING * 2,
|
||||
height: maxY - minY + CONTAINER_PADDING * 2,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block type is a container (loop or parallel)
|
||||
*/
|
||||
@@ -369,7 +314,6 @@ export type LayoutFunction = (
|
||||
horizontalSpacing?: number
|
||||
verticalSpacing?: number
|
||||
padding?: { x: number; y: number }
|
||||
gridSize?: number
|
||||
}
|
||||
subflowDepths?: Map<string, number>
|
||||
}
|
||||
@@ -385,15 +329,13 @@ export type LayoutFunction = (
|
||||
* @param layoutFn - The layout function to use for calculating dimensions
|
||||
* @param horizontalSpacing - Horizontal spacing between blocks
|
||||
* @param verticalSpacing - Vertical spacing between blocks
|
||||
* @param gridSize - Optional grid size for snap-to-grid
|
||||
*/
|
||||
export function prepareContainerDimensions(
|
||||
blocks: Record<string, BlockState>,
|
||||
edges: Edge[],
|
||||
layoutFn: LayoutFunction,
|
||||
horizontalSpacing: number,
|
||||
verticalSpacing: number,
|
||||
gridSize?: number
|
||||
verticalSpacing: number
|
||||
): void {
|
||||
const { children } = getBlocksByParent(blocks)
|
||||
|
||||
@@ -460,7 +402,6 @@ export function prepareContainerDimensions(
|
||||
layoutOptions: {
|
||||
horizontalSpacing: horizontalSpacing * 0.85,
|
||||
verticalSpacing,
|
||||
gridSize,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -16,12 +16,7 @@ import {
|
||||
USER_FILE_PROPERTY_TYPES,
|
||||
} from '@/lib/workflows/types'
|
||||
import { getBlock } from '@/blocks'
|
||||
import {
|
||||
type BlockConfig,
|
||||
isHiddenFromDisplay,
|
||||
type OutputCondition,
|
||||
type OutputFieldDefinition,
|
||||
} from '@/blocks/types'
|
||||
import type { BlockConfig, OutputCondition, OutputFieldDefinition } from '@/blocks/types'
|
||||
import { getTool } from '@/tools/utils'
|
||||
import { getTrigger, isTriggerValid } from '@/triggers'
|
||||
|
||||
@@ -91,8 +86,8 @@ function evaluateOutputCondition(
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters outputs based on their conditions and hiddenFromDisplay flag.
|
||||
* Returns a new OutputDefinition with only the outputs that should be shown.
|
||||
* Filters outputs based on their conditions.
|
||||
* Returns a new OutputDefinition with only the outputs whose conditions are met.
|
||||
*/
|
||||
function filterOutputsByCondition(
|
||||
outputs: OutputDefinition,
|
||||
@@ -101,8 +96,6 @@ function filterOutputsByCondition(
|
||||
const filtered: OutputDefinition = {}
|
||||
|
||||
for (const [key, value] of Object.entries(outputs)) {
|
||||
if (isHiddenFromDisplay(value)) continue
|
||||
|
||||
if (!value || typeof value !== 'object' || !('condition' in value)) {
|
||||
filtered[key] = value
|
||||
continue
|
||||
@@ -112,7 +105,7 @@ function filterOutputsByCondition(
|
||||
const passes = !condition || evaluateOutputCondition(condition, subBlocks)
|
||||
|
||||
if (passes) {
|
||||
const { condition: _, hiddenFromDisplay: __, ...rest } = value
|
||||
const { condition: _, ...rest } = value
|
||||
filtered[key] = rest
|
||||
}
|
||||
}
|
||||
@@ -266,26 +259,50 @@ export function getBlockOutputs(
|
||||
}
|
||||
|
||||
if (blockType === 'human_in_the_loop') {
|
||||
// Start with block config outputs (respects hiddenFromDisplay via filterOutputsByCondition)
|
||||
const baseOutputs = filterOutputsByCondition(
|
||||
{ ...(blockConfig.outputs || {}) } as OutputDefinition,
|
||||
subBlocks
|
||||
)
|
||||
const hitlOutputs: OutputDefinition = {
|
||||
url: { type: 'string', description: 'Resume UI URL' },
|
||||
resumeEndpoint: {
|
||||
type: 'string',
|
||||
description: 'Resume API endpoint URL for direct curl requests',
|
||||
},
|
||||
}
|
||||
|
||||
// Add inputFormat fields (resume form fields)
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
|
||||
|
||||
for (const field of normalizedInputFormat) {
|
||||
const fieldName = field?.name?.trim()
|
||||
if (!fieldName) continue
|
||||
|
||||
baseOutputs[fieldName] = {
|
||||
hitlOutputs[fieldName] = {
|
||||
type: (field?.type || 'any') as any,
|
||||
description: field?.description || `Field from resume form`,
|
||||
description: `Field from resume form`,
|
||||
}
|
||||
}
|
||||
|
||||
return baseOutputs
|
||||
return hitlOutputs
|
||||
}
|
||||
|
||||
if (blockType === 'approval') {
|
||||
// Start with only url (apiUrl commented out - not accessible as output)
|
||||
const pauseResumeOutputs: OutputDefinition = {
|
||||
url: { type: 'string', description: 'Resume UI URL' },
|
||||
// apiUrl: { type: 'string', description: 'Resume API URL' }, // Commented out - not accessible as output
|
||||
}
|
||||
|
||||
const normalizedInputFormat = normalizeInputFormatValue(subBlocks?.inputFormat?.value)
|
||||
|
||||
// Add each input format field as a top-level output
|
||||
for (const field of normalizedInputFormat) {
|
||||
const fieldName = field?.name?.trim()
|
||||
if (!fieldName) continue
|
||||
|
||||
pauseResumeOutputs[fieldName] = {
|
||||
type: (field?.type || 'any') as any,
|
||||
description: `Field from input format`,
|
||||
}
|
||||
}
|
||||
|
||||
return pauseResumeOutputs
|
||||
}
|
||||
|
||||
if (startPath === StartBlockPath.LEGACY_STARTER) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { LoggingSession } from '@/lib/logs/execution/logging-session'
|
||||
import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
|
||||
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import type { ExecutionResult, PausePoint, SerializedSnapshot } from '@/executor/types'
|
||||
import { filterOutputForLog } from '@/executor/utils/output-filter'
|
||||
import type { SerializedConnection } from '@/serializer/types'
|
||||
|
||||
const logger = createLogger('HumanInTheLoopManager')
|
||||
@@ -577,11 +576,13 @@ export class PauseResumeManager {
|
||||
log.blockId === contextId
|
||||
)
|
||||
if (blockLogIndex !== -1) {
|
||||
// Filter output for logging using shared utility
|
||||
// 'resume' is redundant with url/resumeEndpoint so we filter it out
|
||||
const filteredOutput = filterOutputForLog('human_in_the_loop', mergedOutput, {
|
||||
additionalHiddenKeys: ['resume'],
|
||||
})
|
||||
// Filter output for logging (exclude internal fields and response)
|
||||
const filteredOutput: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(mergedOutput)) {
|
||||
if (key.startsWith('_')) continue
|
||||
if (key === 'response') continue
|
||||
filteredOutput[key] = value
|
||||
}
|
||||
stateCopy.blockLogs[blockLogIndex] = {
|
||||
...stateCopy.blockLogs[blockLogIndex],
|
||||
blockId: stateBlockKey,
|
||||
|
||||
@@ -102,7 +102,7 @@ export const azureOpenAIProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
if (request.temperature !== undefined) payload.temperature = request.temperature
|
||||
if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens
|
||||
if (request.maxTokens !== undefined) payload.max_tokens = request.maxTokens
|
||||
|
||||
if (request.reasoningEffort !== undefined) payload.reasoning_effort = request.reasoningEffort
|
||||
if (request.verbosity !== undefined) payload.verbosity = request.verbosity
|
||||
|
||||
@@ -77,7 +77,7 @@ export const cerebrasProvider: ProviderConfig = {
|
||||
messages: allMessages,
|
||||
}
|
||||
if (request.temperature !== undefined) payload.temperature = request.temperature
|
||||
if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens
|
||||
if (request.maxTokens !== undefined) payload.max_tokens = request.maxTokens
|
||||
if (request.responseFormat) {
|
||||
payload.response_format = {
|
||||
type: 'json_schema',
|
||||
|
||||
@@ -81,7 +81,7 @@ export const deepseekProvider: ProviderConfig = {
|
||||
}
|
||||
|
||||
if (request.temperature !== undefined) payload.temperature = request.temperature
|
||||
if (request.maxTokens != null) payload.max_tokens = request.maxTokens
|
||||
if (request.maxTokens !== undefined) payload.max_tokens = request.maxTokens
|
||||
|
||||
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user