mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-19 11:58:12 -05:00
Compare commits
10 Commits
fix/termin
...
fix/verbia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40de1d3c79 | ||
|
|
876db6cac0 | ||
|
|
dec523c6e4 | ||
|
|
f85def744e | ||
|
|
b300192110 | ||
|
|
3554d7f744 | ||
|
|
508807465c | ||
|
|
a5073aaffb | ||
|
|
93bb547222 | ||
|
|
ba48d08441 |
File diff suppressed because it is too large
Load Diff
@@ -66,9 +66,9 @@ List files and folders in Google Drive with complete metadata
|
||||
| --------- | ---- | ----------- |
|
||||
| `files` | array | Array of file metadata objects from Google Drive |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `description` | string | File description |
|
||||
| ↳ `originalFilename` | string | Original uploaded filename |
|
||||
| ↳ `fullFileExtension` | string | Full file extension |
|
||||
@@ -135,6 +135,7 @@ Get metadata for a specific file in Google Drive by its ID
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `description` | string | File description |
|
||||
@@ -175,9 +176,9 @@ Create a new folder in Google Drive with complete metadata returned
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | object | Complete created folder metadata from Google Drive |
|
||||
| ↳ `id` | string | Google Drive folder ID |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `name` | string | Folder name |
|
||||
| ↳ `mimeType` | string | MIME type \(application/vnd.google-apps.folder\) |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `description` | string | Folder description |
|
||||
| ↳ `owners` | json | List of folder owners |
|
||||
| ↳ `permissions` | json | Folder permissions |
|
||||
@@ -233,9 +234,9 @@ Upload a file to Google Drive with complete metadata returned
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | object | Complete uploaded file metadata from Google Drive |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `description` | string | File description |
|
||||
| ↳ `originalFilename` | string | Original uploaded filename |
|
||||
| ↳ `fullFileExtension` | string | Full file extension |
|
||||
@@ -309,9 +310,9 @@ Download a file from Google Drive with complete metadata (exports Google Workspa
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
| `metadata` | object | Complete file metadata from Google Drive |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `description` | string | File description |
|
||||
| ↳ `originalFilename` | string | Original uploaded filename |
|
||||
| ↳ `fullFileExtension` | string | Full file extension |
|
||||
@@ -380,6 +381,7 @@ Create a copy of a file in Google Drive
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The copied file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID of the copy |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
@@ -410,6 +412,7 @@ Update file metadata in Google Drive (rename, move, star, add description)
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The updated file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `description` | string | File description |
|
||||
@@ -434,34 +437,13 @@ Move a file to the trash in Google Drive (can be restored later)
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The trashed file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `kind` | string | Resource type identifier |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `trashed` | boolean | Whether file is in trash \(should be true\) |
|
||||
| ↳ `trashedTime` | string | When file was trashed |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
|
||||
### `google_drive_untrash`
|
||||
|
||||
Restore a file from the trash in Google Drive
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `fileId` | string | Yes | The ID of the file to restore from trash |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | json | The restored file metadata |
|
||||
| ↳ `id` | string | Google Drive file ID |
|
||||
| ↳ `name` | string | File name |
|
||||
| ↳ `mimeType` | string | MIME type |
|
||||
| ↳ `trashed` | boolean | Whether file is in trash \(should be false\) |
|
||||
| ↳ `webViewLink` | string | URL to view in browser |
|
||||
| ↳ `parents` | json | Parent folder IDs |
|
||||
|
||||
### `google_drive_delete`
|
||||
|
||||
Permanently delete a file from Google Drive (bypasses trash)
|
||||
@@ -557,6 +539,7 @@ List all permissions (who has access) for a file in Google Drive
|
||||
| ↳ `allowFileDiscovery` | boolean | Whether file is discoverable by grantee |
|
||||
| ↳ `pendingOwner` | boolean | Whether ownership transfer is pending |
|
||||
| ↳ `permissionDetails` | json | Details about inherited permissions |
|
||||
| `nextPageToken` | string | Token for fetching the next page of permissions |
|
||||
|
||||
### `google_drive_get_about`
|
||||
|
||||
|
||||
@@ -51,13 +51,17 @@ Retrieve a single response or list responses from a Google Form
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `response` | json | Operation response data |
|
||||
| `formId` | string | Form ID |
|
||||
| `title` | string | Form title |
|
||||
| `responderUri` | string | Form responder URL |
|
||||
| `items` | json | Form items |
|
||||
| `responses` | json | Form responses |
|
||||
| `watches` | json | Form watches |
|
||||
| `responses` | array | Array of form responses \(when no responseId provided\) |
|
||||
| ↳ `responseId` | string | Unique response ID |
|
||||
| ↳ `createTime` | string | When the response was created |
|
||||
| ↳ `lastSubmittedTime` | string | When the response was last submitted |
|
||||
| ↳ `answers` | json | Map of question IDs to answer values |
|
||||
| `response` | object | Single form response \(when responseId is provided\) |
|
||||
| ↳ `responseId` | string | Unique response ID |
|
||||
| ↳ `createTime` | string | When the response was created |
|
||||
| ↳ `lastSubmittedTime` | string | When the response was last submitted |
|
||||
| ↳ `answers` | json | Map of question IDs to answer values |
|
||||
| `raw` | json | Raw API response data |
|
||||
|
||||
### `google_forms_get_form`
|
||||
|
||||
@@ -126,8 +130,48 @@ Apply multiple updates to a form (add items, update info, change settings, etc.)
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `replies` | array | The replies from each update request |
|
||||
| `writeControl` | json | Write control information with revision IDs |
|
||||
| `form` | json | The updated form \(if includeFormInResponse was true\) |
|
||||
| `writeControl` | object | Write control information with revision IDs |
|
||||
| ↳ `requiredRevisionId` | string | Required revision ID for conflict detection |
|
||||
| ↳ `targetRevisionId` | string | Target revision ID |
|
||||
| `form` | object | The updated form \(if includeFormInResponse was true\) |
|
||||
| ↳ `formId` | string | The form ID |
|
||||
| ↳ `info` | object | Form info containing title and description |
|
||||
| ↳ `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 |
|
||||
| ↳ `publishSettings` | object | Form publish settings |
|
||||
| ↳ `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`
|
||||
|
||||
|
||||
@@ -194,9 +194,14 @@ Get detailed information about a specific slide/page in a Google Slides presenta
|
||||
| --------- | ---- | ----------- |
|
||||
| `objectId` | string | The object ID of the page |
|
||||
| `pageType` | string | The type of page \(SLIDE, MASTER, LAYOUT, NOTES, NOTES_MASTER\) |
|
||||
| `pageElements` | json | Array of page elements \(shapes, images, tables, etc.\) on this page |
|
||||
| `slideProperties` | json | Properties specific to slides \(layout, master, notes\) |
|
||||
| `metadata` | json | Operation metadata including presentation ID and URL |
|
||||
| `pageElements` | array | Array of page elements \(shapes, images, tables, etc.\) on this page |
|
||||
| `slideProperties` | object | Properties specific to slides \(layout, master, notes\) |
|
||||
| ↳ `layoutObjectId` | string | Object ID of the layout this slide is based on |
|
||||
| ↳ `masterObjectId` | string | Object ID of the master this slide is based on |
|
||||
| ↳ `notesPage` | json | The notes page associated with the slide |
|
||||
| `metadata` | object | Operation metadata including presentation ID and URL |
|
||||
| ↳ `presentationId` | string | The presentation ID |
|
||||
| ↳ `url` | string | URL to the presentation |
|
||||
|
||||
### `google_slides_delete_object`
|
||||
|
||||
@@ -215,7 +220,9 @@ Delete a page element (shape, image, table, etc.) or an entire slide from a Goog
|
||||
| --------- | ---- | ----------- |
|
||||
| `deleted` | boolean | Whether the object was successfully deleted |
|
||||
| `objectId` | string | The object ID that was deleted |
|
||||
| `metadata` | json | Operation metadata including presentation ID and URL |
|
||||
| `metadata` | object | Operation metadata including presentation ID and URL |
|
||||
| ↳ `presentationId` | string | The presentation ID |
|
||||
| ↳ `url` | string | URL to the presentation |
|
||||
|
||||
### `google_slides_duplicate_object`
|
||||
|
||||
@@ -235,7 +242,10 @@ Duplicate an object (slide, shape, image, table, etc.) in a Google Slides presen
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `duplicatedObjectId` | string | The object ID of the newly created duplicate |
|
||||
| `metadata` | json | Operation metadata including presentation ID and source object ID |
|
||||
| `metadata` | object | Operation metadata including presentation ID and source object ID |
|
||||
| ↳ `presentationId` | string | The presentation ID |
|
||||
| ↳ `sourceObjectId` | string | The original object ID that was duplicated |
|
||||
| ↳ `url` | string | URL to the presentation |
|
||||
|
||||
### `google_slides_update_slides_position`
|
||||
|
||||
@@ -256,7 +266,9 @@ Move one or more slides to a new position in a Google Slides presentation
|
||||
| `moved` | boolean | Whether the slides were successfully moved |
|
||||
| `slideObjectIds` | array | The slide object IDs that were moved |
|
||||
| `insertionIndex` | number | The index where the slides were moved to |
|
||||
| `metadata` | json | Operation metadata including presentation ID and URL |
|
||||
| `metadata` | object | Operation metadata including presentation ID and URL |
|
||||
| ↳ `presentationId` | string | The presentation ID |
|
||||
| ↳ `url` | string | URL to the presentation |
|
||||
|
||||
### `google_slides_create_table`
|
||||
|
||||
@@ -282,7 +294,10 @@ Create a new table on a slide in a Google Slides presentation
|
||||
| `tableId` | string | The object ID of the newly created table |
|
||||
| `rows` | number | Number of rows in the table |
|
||||
| `columns` | number | Number of columns in the table |
|
||||
| `metadata` | json | Operation metadata including presentation ID and page object ID |
|
||||
| `metadata` | object | Operation metadata including presentation ID and page object ID |
|
||||
| ↳ `presentationId` | string | The presentation ID |
|
||||
| ↳ `pageObjectId` | string | The page object ID where the table was created |
|
||||
| ↳ `url` | string | URL to the presentation |
|
||||
|
||||
### `google_slides_create_shape`
|
||||
|
||||
@@ -306,7 +321,10 @@ Create a shape (rectangle, ellipse, text box, arrow, etc.) on a slide in a Googl
|
||||
| --------- | ---- | ----------- |
|
||||
| `shapeId` | string | The object ID of the newly created shape |
|
||||
| `shapeType` | string | The type of shape that was created |
|
||||
| `metadata` | json | Operation metadata including presentation ID and page object ID |
|
||||
| `metadata` | object | Operation metadata including presentation ID and page object ID |
|
||||
| ↳ `presentationId` | string | The presentation ID |
|
||||
| ↳ `pageObjectId` | string | The page object ID where the shape was created |
|
||||
| ↳ `url` | string | URL to the presentation |
|
||||
|
||||
### `google_slides_insert_text`
|
||||
|
||||
@@ -328,6 +346,8 @@ Insert text into a shape or table cell in a Google Slides presentation. Use this
|
||||
| `inserted` | boolean | Whether the text was successfully inserted |
|
||||
| `objectId` | string | The object ID where text was inserted |
|
||||
| `text` | string | The text that was inserted |
|
||||
| `metadata` | json | Operation metadata including presentation ID and URL |
|
||||
| `metadata` | object | Operation metadata including presentation ID and URL |
|
||||
| ↳ `presentationId` | string | The presentation ID |
|
||||
| ↳ `url` | string | URL to the presentation |
|
||||
|
||||
|
||||
|
||||
@@ -409,7 +409,11 @@ export function ChatDeploy({
|
||||
<ModalHeader>Delete Chat</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-secondary)]'>
|
||||
Are you sure you want to delete this chat?{' '}
|
||||
Are you sure you want to delete{' '}
|
||||
<span className='font-medium text-[var(--text-primary)]'>
|
||||
{existingChat?.title || 'this chat'}
|
||||
</span>
|
||||
?{' '}
|
||||
<span className='text-[var(--text-error)]'>
|
||||
This will remove the chat at "{getEmailDomain()}/chat/{existingChat?.identifier}"
|
||||
and make it unavailable to all users.
|
||||
|
||||
@@ -375,8 +375,11 @@ export function TemplateDeploy({
|
||||
<ModalHeader>Delete Template</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-secondary)]'>
|
||||
Are you sure you want to delete this template?{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
Are you sure you want to delete{' '}
|
||||
<span className='font-medium text-[var(--text-primary)]'>
|
||||
{existingTemplate?.name || formData.name || 'this template'}
|
||||
</span>
|
||||
? <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -846,7 +846,11 @@ export function DeployModal({
|
||||
<ModalHeader>Delete A2A Agent</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-secondary)]'>
|
||||
Are you sure you want to delete this agent?{' '}
|
||||
Are you sure you want to delete{' '}
|
||||
<span className='font-medium text-[var(--text-primary)]'>
|
||||
{existingA2aAgent?.name || 'this agent'}
|
||||
</span>
|
||||
?{' '}
|
||||
<span className='text-[var(--text-error)]'>
|
||||
This will permanently remove the agent configuration.
|
||||
</span>
|
||||
|
||||
@@ -41,6 +41,7 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
|
||||
'https://www.googleapis.com/auth/calendar': 'View and manage calendar',
|
||||
'https://www.googleapis.com/auth/userinfo.email': 'View email address',
|
||||
'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info',
|
||||
'https://www.googleapis.com/auth/forms.body': 'View and manage Google Forms',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms',
|
||||
'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery',
|
||||
'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { usePopoverContext } from '@/components/emcn'
|
||||
import type { BlockTagGroup, NestedBlockTagGroup } from '../types'
|
||||
import { useNestedNavigation } from '../tag-dropdown'
|
||||
import type { BlockTagGroup, NestedBlockTagGroup, NestedTag } from '../types'
|
||||
|
||||
/**
|
||||
* Keyboard navigation handler component that uses popover context
|
||||
@@ -15,6 +16,90 @@ interface KeyboardNavigationHandlerProps {
|
||||
handleTagSelect: (tag: string, group?: BlockTagGroup) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively finds a folder in nested tags by its ID
|
||||
*/
|
||||
const findFolderInNested = (
|
||||
nestedTags: NestedTag[],
|
||||
blockId: string,
|
||||
targetFolderId: string
|
||||
): NestedTag | null => {
|
||||
for (const nestedTag of nestedTags) {
|
||||
const folderId = `${blockId}-${nestedTag.key}`
|
||||
if (folderId === targetFolderId) {
|
||||
return nestedTag
|
||||
}
|
||||
if (nestedTag.nestedChildren) {
|
||||
const found = findFolderInNested(nestedTag.nestedChildren, blockId, targetFolderId)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively finds folder info for a tag that can be expanded.
|
||||
* Returns both the folder metadata and the NestedTag object for navigation.
|
||||
*/
|
||||
const findFolderInfoForTag = (
|
||||
nestedTags: NestedTag[],
|
||||
targetTag: string,
|
||||
group: NestedBlockTagGroup
|
||||
): {
|
||||
id: string
|
||||
title: string
|
||||
parentTag: string
|
||||
group: NestedBlockTagGroup
|
||||
nestedTag: NestedTag
|
||||
} | null => {
|
||||
for (const nestedTag of nestedTags) {
|
||||
if (
|
||||
nestedTag.parentTag === targetTag &&
|
||||
(nestedTag.children?.length || nestedTag.nestedChildren?.length)
|
||||
) {
|
||||
return {
|
||||
id: `${group.blockId}-${nestedTag.key}`,
|
||||
title: nestedTag.display,
|
||||
parentTag: nestedTag.parentTag,
|
||||
group,
|
||||
nestedTag,
|
||||
}
|
||||
}
|
||||
if (nestedTag.nestedChildren) {
|
||||
const found = findFolderInfoForTag(nestedTag.nestedChildren, targetTag, group)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively checks if a tag is a child of any folder.
|
||||
* This includes both leaf children and nested folder parent tags.
|
||||
*/
|
||||
const isChildOfAnyFolder = (nestedTags: NestedTag[], tag: string): boolean => {
|
||||
for (const nestedTag of nestedTags) {
|
||||
if (nestedTag.children) {
|
||||
for (const child of nestedTag.children) {
|
||||
if (child.fullTag === tag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nestedTag.nestedChildren) {
|
||||
for (const nestedChild of nestedTag.nestedChildren) {
|
||||
if (nestedChild.parentTag === tag) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (isChildOfAnyFolder(nestedTag.nestedChildren, tag)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const KeyboardNavigationHandler: React.FC<KeyboardNavigationHandlerProps> = ({
|
||||
visible,
|
||||
selectedIndex,
|
||||
@@ -23,55 +108,66 @@ export const KeyboardNavigationHandler: React.FC<KeyboardNavigationHandlerProps>
|
||||
nestedBlockTagGroups,
|
||||
handleTagSelect,
|
||||
}) => {
|
||||
const { openFolder, closeFolder, isInFolder, currentFolder } = usePopoverContext()
|
||||
const { openFolder, closeFolder, isInFolder, currentFolder, setKeyboardNav } = usePopoverContext()
|
||||
const nestedNav = useNestedNavigation()
|
||||
|
||||
const visibleIndices = useMemo(() => {
|
||||
const indices: number[] = []
|
||||
const nestedPath = nestedNav?.nestedPath ?? []
|
||||
|
||||
if (isInFolder && currentFolder) {
|
||||
for (const group of nestedBlockTagGroups) {
|
||||
for (const nestedTag of group.nestedTags) {
|
||||
const folderId = `${group.blockId}-${nestedTag.key}`
|
||||
if (folderId === currentFolder && nestedTag.children) {
|
||||
// First, add the parent tag itself (so it's navigable as the first item)
|
||||
if (nestedTag.parentTag) {
|
||||
const parentIdx = flatTagList.findIndex((item) => item.tag === nestedTag.parentTag)
|
||||
if (parentIdx >= 0) {
|
||||
indices.push(parentIdx)
|
||||
}
|
||||
}
|
||||
// Then add all children
|
||||
for (const child of nestedTag.children) {
|
||||
const idx = flatTagList.findIndex((item) => item.tag === child.fullTag)
|
||||
if (idx >= 0) {
|
||||
indices.push(idx)
|
||||
}
|
||||
}
|
||||
let currentNestedTag: NestedTag | null = null
|
||||
|
||||
if (nestedPath.length > 0) {
|
||||
currentNestedTag = nestedPath[nestedPath.length - 1]
|
||||
} else {
|
||||
for (const group of nestedBlockTagGroups) {
|
||||
const folder = findFolderInNested(group.nestedTags, group.blockId, currentFolder)
|
||||
if (folder) {
|
||||
currentNestedTag = folder
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentNestedTag) {
|
||||
if (currentNestedTag.parentTag) {
|
||||
const parentIdx = flatTagList.findIndex(
|
||||
(item) => item.tag === currentNestedTag!.parentTag
|
||||
)
|
||||
if (parentIdx >= 0) {
|
||||
indices.push(parentIdx)
|
||||
}
|
||||
}
|
||||
if (currentNestedTag.children) {
|
||||
for (const child of currentNestedTag.children) {
|
||||
const idx = flatTagList.findIndex((item) => item.tag === child.fullTag)
|
||||
if (idx >= 0) {
|
||||
indices.push(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentNestedTag.nestedChildren) {
|
||||
for (const nestedChild of currentNestedTag.nestedChildren) {
|
||||
if (nestedChild.parentTag) {
|
||||
const idx = flatTagList.findIndex((item) => item.tag === nestedChild.parentTag)
|
||||
if (idx >= 0) {
|
||||
indices.push(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We're at root level, show all non-child items
|
||||
// (variables and parent tags, but not their children)
|
||||
for (let i = 0; i < flatTagList.length; i++) {
|
||||
const tag = flatTagList[i].tag
|
||||
|
||||
// Check if this is a child of a parent folder
|
||||
let isChild = false
|
||||
for (const group of nestedBlockTagGroups) {
|
||||
for (const nestedTag of group.nestedTags) {
|
||||
if (nestedTag.children) {
|
||||
for (const child of nestedTag.children) {
|
||||
if (child.fullTag === tag) {
|
||||
isChild = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isChild) break
|
||||
if (isChildOfAnyFolder(group.nestedTags, tag)) {
|
||||
isChild = true
|
||||
break
|
||||
}
|
||||
if (isChild) break
|
||||
}
|
||||
|
||||
if (!isChild) {
|
||||
@@ -81,16 +177,16 @@ export const KeyboardNavigationHandler: React.FC<KeyboardNavigationHandlerProps>
|
||||
}
|
||||
|
||||
return indices
|
||||
}, [isInFolder, currentFolder, flatTagList, nestedBlockTagGroups])
|
||||
}, [isInFolder, currentFolder, flatTagList, nestedBlockTagGroups, nestedNav])
|
||||
|
||||
const nestedPathLength = nestedNav?.nestedPath.length ?? 0
|
||||
|
||||
// Auto-select first visible item when entering/exiting folders
|
||||
useEffect(() => {
|
||||
if (!visible || visibleIndices.length === 0) return
|
||||
|
||||
if (!visibleIndices.includes(selectedIndex)) {
|
||||
setSelectedIndex(visibleIndices[0])
|
||||
}
|
||||
}, [visible, isInFolder, currentFolder, visibleIndices, selectedIndex, setSelectedIndex])
|
||||
setSelectedIndex(visibleIndices[0])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [visible, isInFolder, currentFolder, nestedPathLength])
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !flatTagList.length) return
|
||||
@@ -117,89 +213,98 @@ export const KeyboardNavigationHandler: React.FC<KeyboardNavigationHandlerProps>
|
||||
id: string
|
||||
title: string
|
||||
parentTag: string
|
||||
group: BlockTagGroup
|
||||
group: NestedBlockTagGroup
|
||||
nestedTag: NestedTag
|
||||
} | null = null
|
||||
|
||||
if (selected) {
|
||||
for (const group of nestedBlockTagGroups) {
|
||||
for (const nestedTag of group.nestedTags) {
|
||||
if (
|
||||
nestedTag.parentTag === selected.tag &&
|
||||
nestedTag.children &&
|
||||
nestedTag.children.length > 0
|
||||
) {
|
||||
currentFolderInfo = {
|
||||
id: `${selected.group?.blockId}-${nestedTag.key}`,
|
||||
title: nestedTag.display,
|
||||
parentTag: nestedTag.parentTag,
|
||||
group,
|
||||
}
|
||||
break
|
||||
}
|
||||
const folderInfo = findFolderInfoForTag(group.nestedTags, selected.tag, group)
|
||||
if (folderInfo) {
|
||||
currentFolderInfo = folderInfo
|
||||
break
|
||||
}
|
||||
if (currentFolderInfo) break
|
||||
}
|
||||
}
|
||||
|
||||
const scrollIntoView = () => {
|
||||
setTimeout(() => {
|
||||
const selectedItem = document.querySelector<HTMLElement>(
|
||||
'[data-radix-popper-content-wrapper] [aria-selected="true"]'
|
||||
)
|
||||
if (selectedItem) {
|
||||
selectedItem.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setKeyboardNav(true)
|
||||
if (visibleIndices.length > 0) {
|
||||
const currentVisibleIndex = visibleIndices.indexOf(selectedIndex)
|
||||
let newIndex: number
|
||||
if (currentVisibleIndex === -1) {
|
||||
setSelectedIndex(visibleIndices[0])
|
||||
newIndex = visibleIndices[0]
|
||||
} else if (currentVisibleIndex < visibleIndices.length - 1) {
|
||||
setSelectedIndex(visibleIndices[currentVisibleIndex + 1])
|
||||
newIndex = visibleIndices[currentVisibleIndex + 1]
|
||||
} else {
|
||||
newIndex = visibleIndices[0]
|
||||
}
|
||||
setSelectedIndex(newIndex)
|
||||
scrollIntoView()
|
||||
}
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setKeyboardNav(true)
|
||||
if (visibleIndices.length > 0) {
|
||||
const currentVisibleIndex = visibleIndices.indexOf(selectedIndex)
|
||||
let newIndex: number
|
||||
if (currentVisibleIndex === -1) {
|
||||
setSelectedIndex(visibleIndices[0])
|
||||
newIndex = visibleIndices[visibleIndices.length - 1]
|
||||
} else if (currentVisibleIndex > 0) {
|
||||
setSelectedIndex(visibleIndices[currentVisibleIndex - 1])
|
||||
newIndex = visibleIndices[currentVisibleIndex - 1]
|
||||
} else {
|
||||
newIndex = visibleIndices[visibleIndices.length - 1]
|
||||
}
|
||||
setSelectedIndex(newIndex)
|
||||
scrollIntoView()
|
||||
}
|
||||
break
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (selected && selectedIndex >= 0 && selectedIndex < flatTagList.length) {
|
||||
if (currentFolderInfo && !isInFolder) {
|
||||
// It's a folder, open it
|
||||
handleTagSelect(selected.tag, selected.group)
|
||||
}
|
||||
break
|
||||
case 'ArrowRight':
|
||||
if (currentFolderInfo) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (isInFolder && nestedNav) {
|
||||
nestedNav.navigateIn(currentFolderInfo.nestedTag, currentFolderInfo.group)
|
||||
} else {
|
||||
openFolderWithSelection(
|
||||
currentFolderInfo.id,
|
||||
currentFolderInfo.title,
|
||||
currentFolderInfo.parentTag,
|
||||
currentFolderInfo.group
|
||||
)
|
||||
} else {
|
||||
// Not a folder, select it
|
||||
handleTagSelect(selected.tag, selected.group)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'ArrowRight':
|
||||
if (currentFolderInfo && !isInFolder) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
openFolderWithSelection(
|
||||
currentFolderInfo.id,
|
||||
currentFolderInfo.title,
|
||||
currentFolderInfo.parentTag,
|
||||
currentFolderInfo.group
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
if (isInFolder) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (nestedNav?.navigateBack()) {
|
||||
return
|
||||
}
|
||||
closeFolder()
|
||||
let firstRootIndex = 0
|
||||
for (let i = 0; i < flatTagList.length; i++) {
|
||||
@@ -239,6 +344,8 @@ export const KeyboardNavigationHandler: React.FC<KeyboardNavigationHandlerProps>
|
||||
isInFolder,
|
||||
setSelectedIndex,
|
||||
handleTagSelect,
|
||||
nestedNav,
|
||||
setKeyboardNav,
|
||||
])
|
||||
|
||||
return null
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,14 +10,27 @@ export interface BlockTagGroup {
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested tag structure for hierarchical display
|
||||
* Child tag within a nested structure
|
||||
*/
|
||||
export interface NestedTagChild {
|
||||
key: string
|
||||
display: string
|
||||
fullTag: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Nested tag structure for hierarchical display.
|
||||
* Supports recursive nesting for deeply nested object structures.
|
||||
*/
|
||||
export interface NestedTag {
|
||||
key: string
|
||||
display: string
|
||||
fullTag?: string
|
||||
parentTag?: string // Tag for the parent object when it has children
|
||||
children?: Array<{ key: string; display: string; fullTag: string }>
|
||||
parentTag?: string
|
||||
/** Leaf children (no further nesting) */
|
||||
children?: NestedTagChild[]
|
||||
/** Recursively nested folders */
|
||||
nestedChildren?: NestedTag[]
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -169,6 +169,8 @@ const getPreviewValue = (
|
||||
* @param isValidJson - Whether the JSON content is valid (for code blocks)
|
||||
* @param subBlockValues - Current values of all subblocks for evaluating conditional requirements
|
||||
* @param wandState - Optional state and handlers for the AI wand feature
|
||||
* @param canonicalToggle - Optional canonical toggle metadata and handlers
|
||||
* @param canonicalToggleIsDisabled - Whether the canonical toggle is disabled
|
||||
* @returns The label JSX element, or `null` for switch types or when no title is defined
|
||||
*/
|
||||
const renderLabel = (
|
||||
@@ -193,7 +195,8 @@ const renderLabel = (
|
||||
mode: 'basic' | 'advanced'
|
||||
disabled?: boolean
|
||||
onToggle?: () => void
|
||||
}
|
||||
},
|
||||
canonicalToggleIsDisabled?: boolean
|
||||
): JSX.Element | null => {
|
||||
if (config.type === 'switch') return null
|
||||
if (!config.title) return null
|
||||
@@ -201,7 +204,7 @@ const renderLabel = (
|
||||
const required = isFieldRequired(config, subBlockValues)
|
||||
const showWand = wandState?.isWandEnabled && !wandState.isPreview && !wandState.disabled
|
||||
const showCanonicalToggle = !!canonicalToggle && !wandState?.isPreview
|
||||
const canonicalToggleDisabled = wandState?.disabled || canonicalToggle?.disabled
|
||||
const canonicalToggleDisabledResolved = canonicalToggleIsDisabled ?? canonicalToggle?.disabled
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-between gap-[6px] pl-[2px]'>
|
||||
@@ -286,7 +289,7 @@ const renderLabel = (
|
||||
type='button'
|
||||
className='flex h-[12px] w-[12px] flex-shrink-0 items-center justify-center bg-transparent p-0 disabled:cursor-not-allowed disabled:opacity-50'
|
||||
onClick={canonicalToggle?.onToggle}
|
||||
disabled={canonicalToggleDisabled}
|
||||
disabled={canonicalToggleDisabledResolved}
|
||||
aria-label={canonicalToggle?.mode === 'advanced' ? 'Use selector' : 'Enter manual ID'}
|
||||
>
|
||||
<ArrowLeftRight
|
||||
@@ -949,7 +952,8 @@ function SubBlockComponent({
|
||||
onSearchCancel: handleSearchCancel,
|
||||
searchInputRef,
|
||||
},
|
||||
canonicalToggle
|
||||
canonicalToggle,
|
||||
Boolean(canonicalToggle?.disabled || disabled || isPreview)
|
||||
)}
|
||||
{renderInput()}
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { useShallow } from 'zustand/react/shallow'
|
||||
import { Button, Tooltip } from '@/components/emcn'
|
||||
import {
|
||||
buildCanonicalIndex,
|
||||
evaluateSubBlockCondition,
|
||||
hasAdvancedValues,
|
||||
hasStandaloneAdvancedFields,
|
||||
isCanonicalPair,
|
||||
resolveCanonicalMode,
|
||||
} from '@/lib/workflows/subblocks/visibility'
|
||||
@@ -131,10 +131,24 @@ export function Editor() {
|
||||
)
|
||||
const displayAdvancedOptions = advancedMode || advancedValuesPresent
|
||||
|
||||
const hasAdvancedOnlyFields = useMemo(
|
||||
() => hasStandaloneAdvancedFields(subBlocksForCanonical, canonicalIndex),
|
||||
[subBlocksForCanonical, canonicalIndex]
|
||||
)
|
||||
const hasAdvancedOnlyFields = useMemo(() => {
|
||||
for (const subBlock of subBlocksForCanonical) {
|
||||
// Must be standalone advanced (mode: 'advanced' without canonicalParamId)
|
||||
if (subBlock.mode !== 'advanced') continue
|
||||
if (canonicalIndex.canonicalIdBySubBlockId[subBlock.id]) continue
|
||||
|
||||
// Check condition - skip if condition not met for current values
|
||||
if (
|
||||
subBlock.condition &&
|
||||
!evaluateSubBlockCondition(subBlock.condition, blockSubBlockValues)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, [subBlocksForCanonical, canonicalIndex.canonicalIdBySubBlockId, blockSubBlockValues])
|
||||
|
||||
// Get subblock layout using custom hook
|
||||
const { subBlocks, stateToUse: subBlockState } = useEditorSubblockLayout(
|
||||
@@ -480,7 +494,9 @@ export function Editor() {
|
||||
onClick={handleToggleAdvancedMode}
|
||||
className='flex items-center gap-[6px] whitespace-nowrap font-medium text-[13px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
|
||||
>
|
||||
{displayAdvancedOptions ? 'Hide advanced fields' : 'Show advanced fields'}
|
||||
{displayAdvancedOptions
|
||||
? 'Hide additional fields'
|
||||
: 'Show additional fields'}
|
||||
<ChevronDown
|
||||
className={`h-[14px] w-[14px] transition-transform duration-200 ${displayAdvancedOptions ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
|
||||
@@ -50,10 +50,10 @@ import {
|
||||
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks'
|
||||
import { useContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { useShowTrainingControls } from '@/hooks/queries/general-settings'
|
||||
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'
|
||||
import { OUTPUT_PANEL_WIDTH, TERMINAL_HEIGHT } from '@/stores/constants'
|
||||
import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
|
||||
import { useGeneralStore } from '@/stores/settings/general'
|
||||
import type { ConsoleEntry } from '@/stores/terminal'
|
||||
import { useTerminalConsoleStore, useTerminalStore } from '@/stores/terminal'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
@@ -830,7 +830,7 @@ export const Terminal = memo(function Terminal() {
|
||||
const [outputOptionsOpen, setOutputOptionsOpen] = useState(false)
|
||||
|
||||
const [isTrainingEnvEnabled, setIsTrainingEnvEnabled] = useState(false)
|
||||
const showTrainingControls = useGeneralStore((state) => state.showTrainingControls)
|
||||
const showTrainingControls = useShowTrainingControls()
|
||||
const { isTraining, toggleModal: toggleTrainingModal, stopTraining } = useCopilotTrainingStore()
|
||||
|
||||
const [isPlaygroundEnabled, setIsPlaygroundEnabled] = useState(false)
|
||||
|
||||
@@ -22,11 +22,10 @@ import {
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
|
||||
import { createCommand } from '@/app/workspace/[workspaceId]/utils/commands-utils'
|
||||
import { useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
|
||||
import { useShowActionBar, useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
|
||||
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { useCanvasModeStore } from '@/stores/canvas-mode'
|
||||
import { useGeneralStore } from '@/stores/settings/general'
|
||||
import { useTerminalStore } from '@/stores/terminal'
|
||||
import { useUndoRedoStore } from '@/stores/undo-redo'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
@@ -41,7 +40,7 @@ export const WorkflowControls = memo(function WorkflowControls() {
|
||||
const { fitViewToBounds } = useCanvasViewport(reactFlowInstance)
|
||||
const { mode, setMode } = useCanvasModeStore()
|
||||
const { undo, redo } = useCollaborativeWorkflow()
|
||||
const showWorkflowControls = useGeneralStore((s) => s.showActionBar)
|
||||
const showWorkflowControls = useShowActionBar()
|
||||
const updateSetting = useUpdateGeneralSetting()
|
||||
const isTerminalResizing = useTerminalStore((state) => state.isResizing)
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ type MenuType = 'block' | 'pane' | null
|
||||
interface UseCanvasContextMenuProps {
|
||||
blocks: Record<string, BlockState>
|
||||
getNodes: () => Node[]
|
||||
setNodes: (updater: (nodes: Node[]) => Node[]) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing workflow canvas context menus.
|
||||
* Handles right-click events, menu state, click-outside detection, and block info extraction.
|
||||
*/
|
||||
export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuProps) {
|
||||
export function useCanvasContextMenu({ blocks, getNodes, setNodes }: UseCanvasContextMenuProps) {
|
||||
const [activeMenu, setActiveMenu] = useState<MenuType>(null)
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
const [selectedBlocks, setSelectedBlocks] = useState<BlockInfo[]>([])
|
||||
@@ -44,14 +45,26 @@ export function useCanvasContextMenu({ blocks, getNodes }: UseCanvasContextMenuP
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const isMultiSelect = event.shiftKey || event.metaKey || event.ctrlKey
|
||||
setNodes((nodes) =>
|
||||
nodes.map((n) => ({
|
||||
...n,
|
||||
selected: isMultiSelect ? (n.id === node.id ? true : n.selected) : n.id === node.id,
|
||||
}))
|
||||
)
|
||||
|
||||
const selectedNodes = getNodes().filter((n) => n.selected)
|
||||
const nodesToUse = selectedNodes.some((n) => n.id === node.id) ? selectedNodes : [node]
|
||||
const nodesToUse = isMultiSelect
|
||||
? selectedNodes.some((n) => n.id === node.id)
|
||||
? selectedNodes
|
||||
: [...selectedNodes, node]
|
||||
: [node]
|
||||
|
||||
setPosition({ x: event.clientX, y: event.clientY })
|
||||
setSelectedBlocks(nodesToBlockInfos(nodesToUse))
|
||||
setActiveMenu('block')
|
||||
},
|
||||
[getNodes, nodesToBlockInfos]
|
||||
[getNodes, nodesToBlockInfos, setNodes]
|
||||
)
|
||||
|
||||
const handlePaneContextMenu = useCallback((event: React.MouseEvent) => {
|
||||
|
||||
@@ -63,6 +63,7 @@ import { useSocket } from '@/app/workspace/providers/socket-provider'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { isAnnotationOnlyBlock } from '@/executor/constants'
|
||||
import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
|
||||
import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings'
|
||||
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
@@ -74,7 +75,6 @@ import { useExecutionStore } from '@/stores/execution'
|
||||
import { useSearchModalStore } from '@/stores/modals/search/store'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { useCopilotStore, usePanelEditorStore } from '@/stores/panel'
|
||||
import { useGeneralStore } from '@/stores/settings/general'
|
||||
import { useUndoRedoStore } from '@/stores/undo-redo'
|
||||
import { useVariablesStore } from '@/stores/variables/store'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
|
||||
@@ -234,6 +234,7 @@ const WorkflowContent = React.memo(() => {
|
||||
const [potentialParentId, setPotentialParentId] = useState<string | null>(null)
|
||||
const [selectedEdges, setSelectedEdges] = useState<SelectedEdgesMap>(new Map())
|
||||
const [isErrorConnectionDrag, setIsErrorConnectionDrag] = useState(false)
|
||||
const selectedIdsRef = useRef<string[] | null>(null)
|
||||
const canvasMode = useCanvasModeStore((state) => state.mode)
|
||||
const isHandMode = canvasMode === 'hand'
|
||||
const { handleCanvasMouseDown, selectionProps } = useShiftSelectionLock({ isHandMode })
|
||||
@@ -308,9 +309,15 @@ const WorkflowContent = React.memo(() => {
|
||||
|
||||
const showTrainingModal = useCopilotTrainingStore((state) => state.showModal)
|
||||
|
||||
const snapToGridSize = useGeneralStore((state) => state.snapToGridSize)
|
||||
const snapToGridSize = useSnapToGridSize()
|
||||
const snapToGrid = snapToGridSize > 0
|
||||
|
||||
const isAutoConnectEnabled = useAutoConnect()
|
||||
const autoConnectRef = useRef(isAutoConnectEnabled)
|
||||
useEffect(() => {
|
||||
autoConnectRef.current = isAutoConnectEnabled
|
||||
}, [isAutoConnectEnabled])
|
||||
|
||||
// Panel open states for context menu
|
||||
const isVariablesOpen = useVariablesStore((state) => state.isOpen)
|
||||
const isChatOpen = useChatStore((state) => state.isChatOpen)
|
||||
@@ -858,7 +865,7 @@ const WorkflowContent = React.memo(() => {
|
||||
handlePaneContextMenu,
|
||||
handleSelectionContextMenu,
|
||||
closeMenu: closeContextMenu,
|
||||
} = useCanvasContextMenu({ blocks, getNodes })
|
||||
} = useCanvasContextMenu({ blocks, getNodes, setNodes })
|
||||
|
||||
const handleContextCopy = useCallback(() => {
|
||||
const blockIds = contextMenuBlocks.map((b) => b.id)
|
||||
@@ -1217,8 +1224,7 @@ const WorkflowContent = React.memo(() => {
|
||||
containerId?: string
|
||||
}
|
||||
): Edge | undefined => {
|
||||
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
||||
if (!isAutoConnectEnabled) return undefined
|
||||
if (!autoConnectRef.current) return undefined
|
||||
|
||||
// Don't auto-connect starter or annotation-only blocks
|
||||
if (options.blockType === 'starter' || isAnnotationOnlyBlock(options.blockType)) {
|
||||
@@ -2148,11 +2154,25 @@ const WorkflowContent = React.memo(() => {
|
||||
/** Handles node changes - applies changes and resolves parent-child selection conflicts. */
|
||||
const onNodesChange = useCallback(
|
||||
(changes: NodeChange[]) => {
|
||||
selectedIdsRef.current = null
|
||||
setDisplayNodes((nds) => {
|
||||
const updated = applyNodeChanges(changes, nds)
|
||||
const hasSelectionChange = changes.some((c) => c.type === 'select')
|
||||
return hasSelectionChange ? resolveParentChildSelectionConflicts(updated, blocks) : updated
|
||||
if (!hasSelectionChange) return updated
|
||||
const resolved = resolveParentChildSelectionConflicts(updated, blocks)
|
||||
selectedIdsRef.current = resolved.filter((node) => node.selected).map((node) => node.id)
|
||||
return resolved
|
||||
})
|
||||
const selectedIds = selectedIdsRef.current as string[] | null
|
||||
if (selectedIds !== null) {
|
||||
const { currentBlockId, clearCurrentBlock, setCurrentBlockId } =
|
||||
usePanelEditorStore.getState()
|
||||
if (selectedIds.length === 1 && selectedIds[0] !== currentBlockId) {
|
||||
setCurrentBlockId(selectedIds[0])
|
||||
} else if (selectedIds.length === 0 && currentBlockId) {
|
||||
clearCurrentBlock()
|
||||
}
|
||||
}
|
||||
},
|
||||
[blocks]
|
||||
)
|
||||
|
||||
@@ -32,11 +32,13 @@ import {
|
||||
UsageLimit,
|
||||
type UsageLimitRef,
|
||||
} from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/usage-limit'
|
||||
import { useUpdateGeneralSetting } from '@/hooks/queries/general-settings'
|
||||
import {
|
||||
useBillingUsageNotifications,
|
||||
useUpdateGeneralSetting,
|
||||
} from '@/hooks/queries/general-settings'
|
||||
import { useOrganizationBilling, useOrganizations } from '@/hooks/queries/organization'
|
||||
import { useSubscriptionData, useUsageLimitData } from '@/hooks/queries/subscription'
|
||||
import { useUpdateWorkspaceSettings, useWorkspaceSettings } from '@/hooks/queries/workspace'
|
||||
import { useGeneralStore } from '@/stores/settings/general'
|
||||
|
||||
const CONSTANTS = {
|
||||
UPGRADE_ERROR_TIMEOUT: 3000, // 3 seconds
|
||||
@@ -627,7 +629,7 @@ export function Subscription() {
|
||||
}
|
||||
|
||||
function BillingUsageNotificationsToggle() {
|
||||
const enabled = useGeneralStore((s) => s.isBillingUsageNotificationsEnabled)
|
||||
const enabled = useBillingUsageNotifications()
|
||||
const updateSetting = useUpdateGeneralSetting()
|
||||
const isLoading = updateSetting.isPending
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Check } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
@@ -11,10 +11,110 @@ import {
|
||||
PopoverDivider,
|
||||
PopoverFolder,
|
||||
PopoverItem,
|
||||
usePopoverContext,
|
||||
} from '@/components/emcn'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { WORKFLOW_COLORS } from '@/lib/workflows/colors'
|
||||
|
||||
const GRID_COLUMNS = 6
|
||||
|
||||
/**
|
||||
* Color grid with keyboard navigation support.
|
||||
* Uses roving tabindex pattern for accessibility.
|
||||
*/
|
||||
function ColorGrid({
|
||||
hexInput,
|
||||
setHexInput,
|
||||
}: {
|
||||
hexInput: string
|
||||
setHexInput: (color: string) => void
|
||||
}) {
|
||||
const { isInFolder } = usePopoverContext()
|
||||
const [focusedIndex, setFocusedIndex] = useState(-1)
|
||||
const gridRef = useRef<HTMLDivElement>(null)
|
||||
const buttonRefs = useRef<(HTMLButtonElement | null)[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (isInFolder && gridRef.current) {
|
||||
const selectedIndex = WORKFLOW_COLORS.findIndex(
|
||||
({ color }) => color.toLowerCase() === hexInput.toLowerCase()
|
||||
)
|
||||
const initialIndex = selectedIndex >= 0 ? selectedIndex : 0
|
||||
setFocusedIndex(initialIndex)
|
||||
setTimeout(() => {
|
||||
buttonRefs.current[initialIndex]?.focus()
|
||||
}, 50)
|
||||
}
|
||||
}, [isInFolder, hexInput])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent, index: number) => {
|
||||
const totalItems = WORKFLOW_COLORS.length
|
||||
let newIndex = index
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
e.preventDefault()
|
||||
newIndex = index + 1 < totalItems ? index + 1 : index
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault()
|
||||
newIndex = index - 1 >= 0 ? index - 1 : index
|
||||
break
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
newIndex = index + GRID_COLUMNS < totalItems ? index + GRID_COLUMNS : index
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
newIndex = index - GRID_COLUMNS >= 0 ? index - GRID_COLUMNS : index
|
||||
break
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
e.preventDefault()
|
||||
setHexInput(WORKFLOW_COLORS[index].color)
|
||||
return
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if (newIndex !== index) {
|
||||
setFocusedIndex(newIndex)
|
||||
buttonRefs.current[newIndex]?.focus()
|
||||
}
|
||||
},
|
||||
[setHexInput]
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={gridRef} className='grid grid-cols-6 gap-[4px]' role='grid'>
|
||||
{WORKFLOW_COLORS.map(({ color, name }, index) => (
|
||||
<button
|
||||
key={color}
|
||||
ref={(el) => {
|
||||
buttonRefs.current[index] = el
|
||||
}}
|
||||
type='button'
|
||||
role='gridcell'
|
||||
title={name}
|
||||
tabIndex={focusedIndex === index ? 0 : -1}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setHexInput(color)
|
||||
}}
|
||||
onKeyDown={(e) => handleKeyDown(e, index)}
|
||||
onFocus={() => setFocusedIndex(index)}
|
||||
className={cn(
|
||||
'h-[20px] w-[20px] rounded-[4px] focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-1 focus:ring-offset-[#1b1b1b]',
|
||||
hexInput.toLowerCase() === color.toLowerCase() && 'ring-1 ring-white'
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a hex color string.
|
||||
* Accepts 3 or 6 character hex codes with or without #.
|
||||
@@ -349,25 +449,8 @@ export function ContextMenu({
|
||||
className={disableColorChange ? 'pointer-events-none opacity-50' : ''}
|
||||
>
|
||||
<div className='flex w-[140px] flex-col gap-[8px] p-[2px]'>
|
||||
{/* Preset colors */}
|
||||
<div className='grid grid-cols-6 gap-[4px]'>
|
||||
{WORKFLOW_COLORS.map(({ color, name }) => (
|
||||
<button
|
||||
key={color}
|
||||
type='button'
|
||||
title={name}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setHexInput(color)
|
||||
}}
|
||||
className={cn(
|
||||
'h-[20px] w-[20px] rounded-[4px]',
|
||||
hexInput.toLowerCase() === color.toLowerCase() && 'ring-1 ring-white'
|
||||
)}
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Preset colors with keyboard navigation */}
|
||||
<ColorGrid hexInput={hexInput} setHexInput={setHexInput} />
|
||||
|
||||
{/* Hex input */}
|
||||
<div className='flex items-center gap-[4px]'>
|
||||
|
||||
@@ -97,6 +97,15 @@ export function DeleteModal({
|
||||
return 'Are you sure you want to delete this folder? This will permanently remove all associated workflows, logs, and knowledge bases.'
|
||||
}
|
||||
|
||||
if (isSingle && displayNames.length > 0) {
|
||||
return (
|
||||
<>
|
||||
Are you sure you want to delete{' '}
|
||||
<span className='font-medium text-[var(--text-primary)]'>{displayNames[0]}</span>? This
|
||||
will permanently remove all associated workflows, folders, logs, and knowledge bases.
|
||||
</>
|
||||
)
|
||||
}
|
||||
return 'Are you sure you want to delete this workspace? This will permanently remove all associated workflows, folders, logs, and knowledge bases.'
|
||||
}
|
||||
|
||||
|
||||
@@ -308,14 +308,30 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
||||
condition: { field: 'operation', value: 'update' },
|
||||
},
|
||||
|
||||
// Move Event Fields
|
||||
// Move Event Fields - Destination calendar selector (basic mode)
|
||||
{
|
||||
id: 'destinationCalendarId',
|
||||
id: 'destinationCalendar',
|
||||
title: 'Destination Calendar',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'destinationCalendarId',
|
||||
serviceId: 'google-calendar',
|
||||
requiredScopes: ['https://www.googleapis.com/auth/calendar'],
|
||||
placeholder: 'Select destination calendar',
|
||||
dependsOn: ['credential'],
|
||||
condition: { field: 'operation', value: 'move' },
|
||||
required: true,
|
||||
mode: 'basic',
|
||||
},
|
||||
// Move Event Fields - Manual destination calendar ID (advanced mode)
|
||||
{
|
||||
id: 'manualDestinationCalendarId',
|
||||
title: 'Destination Calendar ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'destinationCalendarId',
|
||||
placeholder: 'destination@group.calendar.google.com',
|
||||
condition: { field: 'operation', value: 'move' },
|
||||
required: true,
|
||||
mode: 'advanced',
|
||||
},
|
||||
|
||||
// Instances Fields
|
||||
@@ -502,17 +518,31 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
replaceExisting,
|
||||
calendarId,
|
||||
manualCalendarId,
|
||||
destinationCalendar,
|
||||
manualDestinationCalendarId,
|
||||
...rest
|
||||
} = params
|
||||
|
||||
// Handle calendar ID (selector or manual)
|
||||
const effectiveCalendarId = (calendarId || manualCalendarId || '').trim()
|
||||
|
||||
// Handle destination calendar ID for move operation (selector or manual)
|
||||
const effectiveDestinationCalendarId = (
|
||||
destinationCalendar ||
|
||||
manualDestinationCalendarId ||
|
||||
''
|
||||
).trim()
|
||||
|
||||
const processedParams: Record<string, any> = {
|
||||
...rest,
|
||||
calendarId: effectiveCalendarId || 'primary',
|
||||
}
|
||||
|
||||
// Add destination calendar ID for move operation
|
||||
if (operation === 'move' && effectiveDestinationCalendarId) {
|
||||
processedParams.destinationCalendarId = effectiveDestinationCalendarId
|
||||
}
|
||||
|
||||
// Convert comma-separated attendees string to array, only if it has content
|
||||
if (attendees && typeof attendees === 'string' && attendees.trim().length > 0) {
|
||||
const attendeeList = attendees
|
||||
@@ -579,7 +609,8 @@ Return ONLY the natural language event text - no explanations.`,
|
||||
eventId: { type: 'string', description: 'Event identifier' },
|
||||
|
||||
// Move operation inputs
|
||||
destinationCalendarId: { type: 'string', description: 'Destination calendar ID' },
|
||||
destinationCalendar: { type: 'string', description: 'Destination calendar selector' },
|
||||
manualDestinationCalendarId: { type: 'string', description: 'Manual destination calendar ID' },
|
||||
|
||||
// List Calendars operation inputs
|
||||
minAccessRole: { type: 'string', description: 'Minimum access role filter' },
|
||||
|
||||
@@ -30,7 +30,6 @@ export const GoogleDriveBlock: BlockConfig<GoogleDriveResponse> = {
|
||||
{ label: 'Copy File', id: 'copy' },
|
||||
{ label: 'Update File', id: 'update' },
|
||||
{ label: 'Move to Trash', id: 'trash' },
|
||||
{ label: 'Restore from Trash', id: 'untrash' },
|
||||
{ label: 'Delete Permanently', id: 'delete' },
|
||||
{ label: 'Share File', id: 'share' },
|
||||
{ label: 'Remove Sharing', id: 'unshare' },
|
||||
@@ -524,16 +523,6 @@ Return ONLY the description text - no explanations, no quotes, no extra text.`,
|
||||
condition: { field: 'operation', value: 'trash' },
|
||||
required: true,
|
||||
},
|
||||
// Untrash File Fields
|
||||
{
|
||||
id: 'manualFileId',
|
||||
title: 'File ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'fileId',
|
||||
placeholder: 'Enter file ID to restore from trash',
|
||||
condition: { field: 'operation', value: 'untrash' },
|
||||
required: true,
|
||||
},
|
||||
// Delete File Fields
|
||||
{
|
||||
id: 'fileSelector',
|
||||
@@ -745,7 +734,6 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
|
||||
'google_drive_copy',
|
||||
'google_drive_update',
|
||||
'google_drive_trash',
|
||||
'google_drive_untrash',
|
||||
'google_drive_delete',
|
||||
'google_drive_share',
|
||||
'google_drive_unshare',
|
||||
@@ -772,8 +760,6 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
|
||||
return 'google_drive_update'
|
||||
case 'trash':
|
||||
return 'google_drive_trash'
|
||||
case 'untrash':
|
||||
return 'google_drive_untrash'
|
||||
case 'delete':
|
||||
return 'google_drive_delete'
|
||||
case 'share':
|
||||
@@ -875,12 +861,22 @@ Return ONLY the message text - no subject line, no greetings/signatures, no extr
|
||||
permissionId: { type: 'string', description: 'Permission ID to remove' },
|
||||
},
|
||||
outputs: {
|
||||
file: { type: 'json', description: 'File metadata' },
|
||||
file: { type: 'json', description: 'File metadata or downloaded file data' },
|
||||
files: { type: 'json', description: 'List of files' },
|
||||
metadata: { type: 'json', description: 'Complete file metadata (from download)' },
|
||||
content: { type: 'string', description: 'File content as text' },
|
||||
nextPageToken: { type: 'string', description: 'Token for fetching the next page of results' },
|
||||
permission: { type: 'json', description: 'Permission details' },
|
||||
permissions: { type: 'json', description: 'List of permissions' },
|
||||
user: { type: 'json', description: 'User information' },
|
||||
storageQuota: { type: 'json', description: 'Storage quota information' },
|
||||
canCreateDrives: { type: 'boolean', description: 'Whether user can create shared drives' },
|
||||
importFormats: { type: 'json', description: 'Map of MIME types that can be imported' },
|
||||
exportFormats: {
|
||||
type: 'json',
|
||||
description: 'Map of Google Workspace MIME types and export formats',
|
||||
},
|
||||
maxUploadSize: { type: 'string', description: 'Maximum upload size in bytes' },
|
||||
deleted: { type: 'boolean', description: 'Whether file was deleted' },
|
||||
removed: { type: 'boolean', description: 'Whether permission was removed' },
|
||||
},
|
||||
|
||||
@@ -39,19 +39,40 @@ export const GoogleFormsBlock: BlockConfig = {
|
||||
requiredScopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/forms.body',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
placeholder: 'Select Google account',
|
||||
},
|
||||
// Form ID - required for most operations except create_form
|
||||
// Form selector (basic mode)
|
||||
{
|
||||
id: 'formId',
|
||||
title: 'Select Form',
|
||||
type: 'file-selector',
|
||||
canonicalParamId: 'formId',
|
||||
serviceId: 'google-forms',
|
||||
requiredScopes: [],
|
||||
mimeType: 'application/vnd.google-apps.form',
|
||||
placeholder: 'Select a form',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'basic',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'create_form',
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
// Manual form ID input (advanced mode)
|
||||
{
|
||||
id: 'manualFormId',
|
||||
title: 'Form ID',
|
||||
type: 'short-input',
|
||||
canonicalParamId: 'formId',
|
||||
required: true,
|
||||
placeholder: 'Enter the Google Form ID',
|
||||
dependsOn: ['credential'],
|
||||
mode: 'advanced',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'create_form',
|
||||
@@ -214,6 +235,7 @@ Example for "Add a required multiple choice question about favorite color":
|
||||
credential,
|
||||
operation,
|
||||
formId,
|
||||
manualFormId,
|
||||
responseId,
|
||||
pageSize,
|
||||
title,
|
||||
@@ -230,7 +252,7 @@ Example for "Add a required multiple choice question about favorite color":
|
||||
} = params
|
||||
|
||||
const baseParams = { ...rest, credential }
|
||||
const effectiveFormId = formId ? String(formId).trim() : undefined
|
||||
const effectiveFormId = (formId || manualFormId || '').toString().trim() || undefined
|
||||
|
||||
switch (operation) {
|
||||
case 'get_responses':
|
||||
@@ -299,7 +321,8 @@ Example for "Add a required multiple choice question about favorite color":
|
||||
inputs: {
|
||||
operation: { type: 'string', description: 'Operation to perform' },
|
||||
credential: { type: 'string', description: 'Google OAuth credential' },
|
||||
formId: { type: 'string', description: 'Google Form ID' },
|
||||
formId: { type: 'string', description: 'Google Form ID (from selector)' },
|
||||
manualFormId: { type: 'string', description: 'Google Form ID (manual entry)' },
|
||||
responseId: { type: 'string', description: 'Specific response ID' },
|
||||
pageSize: { type: 'string', description: 'Max responses to retrieve' },
|
||||
title: { type: 'string', description: 'Form title for creation' },
|
||||
@@ -314,13 +337,132 @@ Example for "Add a required multiple choice question about favorite color":
|
||||
watchId: { type: 'string', description: 'Watch ID' },
|
||||
},
|
||||
outputs: {
|
||||
response: { type: 'json', description: 'Operation response data' },
|
||||
formId: { type: 'string', description: 'Form ID' },
|
||||
title: { type: 'string', description: 'Form title' },
|
||||
responderUri: { type: 'string', description: 'Form responder URL' },
|
||||
items: { type: 'json', description: 'Form items' },
|
||||
responses: { type: 'json', description: 'Form responses' },
|
||||
watches: { type: 'json', description: 'Form watches' },
|
||||
responses: {
|
||||
type: 'json',
|
||||
description: 'Array of form responses',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'get_responses',
|
||||
and: { field: 'responseId', value: ['', undefined, null] },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
type: 'json',
|
||||
description: 'Single form response',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: 'get_responses',
|
||||
and: { field: 'responseId', value: ['', undefined, null], not: true },
|
||||
},
|
||||
},
|
||||
// Get Form outputs
|
||||
formId: {
|
||||
type: 'string',
|
||||
description: 'Form ID',
|
||||
condition: { field: 'operation', value: ['get_form', 'create_form', 'set_publish_settings'] },
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
description: 'Form title',
|
||||
condition: { field: 'operation', value: ['get_form', 'create_form'] },
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Form description',
|
||||
condition: { field: 'operation', value: 'get_form' },
|
||||
},
|
||||
documentTitle: {
|
||||
type: 'string',
|
||||
description: 'Document title in Drive',
|
||||
condition: { field: 'operation', value: ['get_form', 'create_form'] },
|
||||
},
|
||||
responderUri: {
|
||||
type: 'string',
|
||||
description: 'Form responder URL',
|
||||
condition: { field: 'operation', value: ['get_form', 'create_form'] },
|
||||
},
|
||||
linkedSheetId: {
|
||||
type: 'string',
|
||||
description: 'Linked Google Sheet ID',
|
||||
condition: { field: 'operation', value: 'get_form' },
|
||||
},
|
||||
revisionId: {
|
||||
type: 'string',
|
||||
description: 'Form revision ID',
|
||||
condition: { field: 'operation', value: ['get_form', 'create_form'] },
|
||||
},
|
||||
items: {
|
||||
type: 'json',
|
||||
description: 'Form items (questions, sections, etc.)',
|
||||
condition: { field: 'operation', value: 'get_form' },
|
||||
},
|
||||
settings: {
|
||||
type: 'json',
|
||||
description: 'Form settings',
|
||||
condition: { field: 'operation', value: 'get_form' },
|
||||
},
|
||||
publishSettings: {
|
||||
type: 'json',
|
||||
description: 'Form publish settings',
|
||||
condition: { field: 'operation', value: ['get_form', 'set_publish_settings'] },
|
||||
},
|
||||
// Batch Update outputs
|
||||
replies: {
|
||||
type: 'json',
|
||||
description: 'Replies from each update request',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
},
|
||||
writeControl: {
|
||||
type: 'json',
|
||||
description: 'Write control with revision IDs',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
},
|
||||
form: {
|
||||
type: 'json',
|
||||
description: 'Updated form (if includeFormInResponse is true)',
|
||||
condition: { field: 'operation', value: 'batch_update' },
|
||||
},
|
||||
// Watch outputs
|
||||
watches: {
|
||||
type: 'json',
|
||||
description: 'Array of form watches',
|
||||
condition: { field: 'operation', value: 'list_watches' },
|
||||
},
|
||||
id: {
|
||||
type: 'string',
|
||||
description: 'Watch ID',
|
||||
condition: { field: 'operation', value: ['create_watch', 'renew_watch'] },
|
||||
},
|
||||
eventType: {
|
||||
type: 'string',
|
||||
description: 'Watch event type',
|
||||
condition: { field: 'operation', value: ['create_watch', 'renew_watch'] },
|
||||
},
|
||||
topicName: {
|
||||
type: 'string',
|
||||
description: 'Cloud Pub/Sub topic',
|
||||
condition: { field: 'operation', value: 'create_watch' },
|
||||
},
|
||||
createTime: {
|
||||
type: 'string',
|
||||
description: 'Watch creation time',
|
||||
condition: { field: 'operation', value: 'create_watch' },
|
||||
},
|
||||
expireTime: {
|
||||
type: 'string',
|
||||
description: 'Watch expiration time',
|
||||
condition: { field: 'operation', value: ['create_watch', 'renew_watch'] },
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
description: 'Watch state (ACTIVE, SUSPENDED)',
|
||||
condition: { field: 'operation', value: ['create_watch', 'renew_watch'] },
|
||||
},
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the watch was deleted',
|
||||
condition: { field: 'operation', value: 'delete_watch' },
|
||||
},
|
||||
},
|
||||
triggers: {
|
||||
enabled: true,
|
||||
|
||||
@@ -135,7 +135,13 @@ export interface OutputCondition {
|
||||
not?: boolean
|
||||
and?: {
|
||||
field: string
|
||||
value: string | number | boolean | Array<string | number | boolean> | undefined
|
||||
value:
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Array<string | number | boolean | undefined | null>
|
||||
| undefined
|
||||
| null
|
||||
not?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,18 @@ interface PopoverContextValue {
|
||||
/** ID of the last hovered item (for hover submenus) */
|
||||
lastHoveredItem: string | null
|
||||
setLastHoveredItem: (id: string | null) => void
|
||||
/** Whether keyboard navigation is active. When true, hover styles are suppressed. */
|
||||
isKeyboardNav: boolean
|
||||
setKeyboardNav: (value: boolean) => void
|
||||
/** Currently selected item index for keyboard navigation */
|
||||
selectedIndex: number
|
||||
setSelectedIndex: (index: number) => void
|
||||
/** Register a menu item and get its index. Returns a cleanup function. */
|
||||
registerItem: (id: string) => number
|
||||
/** Unregister a menu item */
|
||||
unregisterItem: (id: string) => void
|
||||
/** Get the total number of registered items */
|
||||
itemCount: number
|
||||
}
|
||||
|
||||
const PopoverContext = React.createContext<PopoverContextValue | null>(null)
|
||||
@@ -220,6 +232,23 @@ const Popover: React.FC<PopoverProps> = ({
|
||||
const [onFolderSelect, setOnFolderSelect] = React.useState<(() => void) | null>(null)
|
||||
const [searchQuery, setSearchQuery] = React.useState<string>('')
|
||||
const [lastHoveredItem, setLastHoveredItem] = React.useState<string | null>(null)
|
||||
const [isKeyboardNav, setIsKeyboardNav] = React.useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = React.useState(-1)
|
||||
const registeredItemsRef = React.useRef<string[]>([])
|
||||
|
||||
const registerItem = React.useCallback((id: string) => {
|
||||
if (!registeredItemsRef.current.includes(id)) {
|
||||
registeredItemsRef.current.push(id)
|
||||
}
|
||||
return registeredItemsRef.current.indexOf(id)
|
||||
}, [])
|
||||
|
||||
const unregisterItem = React.useCallback((id: string) => {
|
||||
const index = registeredItemsRef.current.indexOf(id)
|
||||
if (index !== -1) {
|
||||
registeredItemsRef.current.splice(index, 1)
|
||||
}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open === false) {
|
||||
@@ -228,6 +257,9 @@ const Popover: React.FC<PopoverProps> = ({
|
||||
setOnFolderSelect(null)
|
||||
setSearchQuery('')
|
||||
setLastHoveredItem(null)
|
||||
setIsKeyboardNav(false)
|
||||
setSelectedIndex(-1)
|
||||
registeredItemsRef.current = []
|
||||
}
|
||||
}, [open])
|
||||
|
||||
@@ -249,6 +281,12 @@ const Popover: React.FC<PopoverProps> = ({
|
||||
setOnFolderSelect(null)
|
||||
}, [])
|
||||
|
||||
const setKeyboardNav = React.useCallback((value: boolean) => {
|
||||
setIsKeyboardNav(value)
|
||||
}, [])
|
||||
|
||||
const itemCount = registeredItemsRef.current.length
|
||||
|
||||
const contextValue = React.useMemo<PopoverContextValue>(
|
||||
() => ({
|
||||
openFolder,
|
||||
@@ -264,6 +302,13 @@ const Popover: React.FC<PopoverProps> = ({
|
||||
setSearchQuery,
|
||||
lastHoveredItem,
|
||||
setLastHoveredItem,
|
||||
isKeyboardNav,
|
||||
setKeyboardNav,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
registerItem,
|
||||
unregisterItem,
|
||||
itemCount,
|
||||
}),
|
||||
[
|
||||
openFolder,
|
||||
@@ -276,6 +321,12 @@ const Popover: React.FC<PopoverProps> = ({
|
||||
colorScheme,
|
||||
searchQuery,
|
||||
lastHoveredItem,
|
||||
isKeyboardNav,
|
||||
setKeyboardNav,
|
||||
selectedIndex,
|
||||
registerItem,
|
||||
unregisterItem,
|
||||
itemCount,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -382,6 +433,92 @@ const PopoverContent = React.forwardRef<
|
||||
|
||||
const effectiveSideOffset = sideOffset ?? (side === 'top' ? 20 : 14)
|
||||
|
||||
const handleMouseMove = React.useCallback(() => {
|
||||
if (context?.isKeyboardNav) {
|
||||
context.setKeyboardNav(false)
|
||||
}
|
||||
}, [context])
|
||||
|
||||
const contentRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const mergedRef = React.useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
contentRef.current = node
|
||||
if (typeof ref === 'function') {
|
||||
ref(node)
|
||||
} else if (ref) {
|
||||
ref.current = node
|
||||
}
|
||||
},
|
||||
[ref]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!context) return
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const content = contentRef.current
|
||||
if (!content) return
|
||||
|
||||
const items = content.querySelectorAll<HTMLElement>(
|
||||
'[role="menuitem"]:not([aria-disabled="true"])'
|
||||
)
|
||||
if (items.length === 0) return
|
||||
|
||||
const currentIndex = context.selectedIndex
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
context.setKeyboardNav(true)
|
||||
if (currentIndex < 0) {
|
||||
context.setSelectedIndex(0)
|
||||
} else {
|
||||
context.setSelectedIndex(currentIndex < items.length - 1 ? currentIndex + 1 : 0)
|
||||
}
|
||||
break
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
context.setKeyboardNav(true)
|
||||
if (currentIndex < 0) {
|
||||
context.setSelectedIndex(items.length - 1)
|
||||
} else {
|
||||
context.setSelectedIndex(currentIndex > 0 ? currentIndex - 1 : items.length - 1)
|
||||
}
|
||||
break
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (currentIndex >= 0) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const selectedItem = items[currentIndex]
|
||||
if (selectedItem) {
|
||||
selectedItem.click()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown, true)
|
||||
}, [context])
|
||||
|
||||
React.useEffect(() => {
|
||||
const content = contentRef.current
|
||||
if (!content || !context?.isKeyboardNav || context.selectedIndex < 0) return
|
||||
|
||||
const items = content.querySelectorAll<HTMLElement>(
|
||||
'[role="menuitem"]:not([aria-disabled="true"])'
|
||||
)
|
||||
const selectedItem = items[context.selectedIndex]
|
||||
if (selectedItem) {
|
||||
selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
||||
}
|
||||
}, [context?.selectedIndex, context?.isKeyboardNav])
|
||||
|
||||
const hasUserWidthConstraint =
|
||||
maxWidth !== undefined ||
|
||||
minWidth !== undefined ||
|
||||
@@ -425,7 +562,7 @@ const PopoverContent = React.forwardRef<
|
||||
|
||||
const content = (
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
ref={mergedRef}
|
||||
side={side}
|
||||
align={align}
|
||||
sideOffset={effectiveSideOffset}
|
||||
@@ -434,6 +571,7 @@ const PopoverContent = React.forwardRef<
|
||||
sticky='partial'
|
||||
hideWhenDetached={false}
|
||||
onWheel={handleWheel}
|
||||
onMouseMove={handleMouseMove}
|
||||
onOpenAutoFocus={handleOpenAutoFocus}
|
||||
onCloseAutoFocus={handleCloseAutoFocus}
|
||||
{...restProps}
|
||||
@@ -534,6 +672,29 @@ const PopoverItem = React.forwardRef<HTMLDivElement, PopoverItemProps>(
|
||||
const variant = context?.variant || 'default'
|
||||
const size = context?.size || 'md'
|
||||
const colorScheme = context?.colorScheme || 'default'
|
||||
const itemRef = React.useRef<HTMLDivElement>(null)
|
||||
const [itemIndex, setItemIndex] = React.useState(-1)
|
||||
|
||||
const mergedRef = React.useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
itemRef.current = node
|
||||
if (typeof ref === 'function') {
|
||||
ref(node)
|
||||
} else if (ref) {
|
||||
ref.current = node
|
||||
}
|
||||
},
|
||||
[ref]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!itemRef.current) return
|
||||
const content = itemRef.current.closest('[data-radix-popper-content-wrapper]')
|
||||
if (!content) return
|
||||
const items = content.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])')
|
||||
const index = Array.from(items).indexOf(itemRef.current)
|
||||
setItemIndex(index)
|
||||
}, [])
|
||||
|
||||
if (rootOnly && context?.isInFolder) return null
|
||||
|
||||
@@ -546,24 +707,36 @@ const PopoverItem = React.forwardRef<HTMLDivElement, PopoverItemProps>(
|
||||
}
|
||||
|
||||
const handleMouseEnter = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
// Clear last hovered item to close any open hover submenus
|
||||
context?.setLastHoveredItem(null)
|
||||
if (itemIndex >= 0 && context) {
|
||||
context.setSelectedIndex(itemIndex)
|
||||
}
|
||||
onMouseEnter?.(e)
|
||||
}
|
||||
|
||||
// Determine if this item is active:
|
||||
// - Use explicit `active` prop if provided
|
||||
// - Otherwise use context selectedIndex match
|
||||
const isActive =
|
||||
active !== undefined ? active : itemIndex >= 0 && context?.selectedIndex === itemIndex
|
||||
|
||||
// Suppress hover when in keyboard mode to prevent dual highlights
|
||||
const suppressHover = context?.isKeyboardNav && !isActive
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={mergedRef}
|
||||
className={cn(
|
||||
STYLES.itemBase,
|
||||
STYLES.colorScheme[colorScheme].text,
|
||||
STYLES.size[size].item,
|
||||
getItemStateClasses(variant, colorScheme, !!active),
|
||||
getItemStateClasses(variant, colorScheme, !!isActive),
|
||||
suppressHover && 'hover:!bg-transparent',
|
||||
disabled && 'pointer-events-none cursor-not-allowed opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
role='menuitem'
|
||||
aria-selected={active}
|
||||
aria-selected={isActive}
|
||||
aria-disabled={disabled}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
@@ -666,17 +839,19 @@ const PopoverFolder = React.forwardRef<HTMLDivElement, PopoverFolderProps>(
|
||||
colorScheme,
|
||||
lastHoveredItem,
|
||||
setLastHoveredItem,
|
||||
isKeyboardNav,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
} = usePopoverContext()
|
||||
const [submenuPosition, setSubmenuPosition] = React.useState<{ top: number; left: number }>({
|
||||
top: 0,
|
||||
left: 0,
|
||||
})
|
||||
const triggerRef = React.useRef<HTMLDivElement>(null)
|
||||
const [itemIndex, setItemIndex] = React.useState(-1)
|
||||
|
||||
// Submenu is open when this folder is the last hovered item (for expandOnHover mode)
|
||||
const isHoverOpen = expandOnHover && lastHoveredItem === id
|
||||
|
||||
// Merge refs
|
||||
const mergedRef = React.useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
triggerRef.current = node
|
||||
@@ -689,9 +864,16 @@ const PopoverFolder = React.forwardRef<HTMLDivElement, PopoverFolderProps>(
|
||||
[ref]
|
||||
)
|
||||
|
||||
// If we're in a folder and this isn't the current one, hide
|
||||
React.useEffect(() => {
|
||||
if (!triggerRef.current) return
|
||||
const content = triggerRef.current.closest('[data-radix-popper-content-wrapper]')
|
||||
if (!content) return
|
||||
const items = content.querySelectorAll('[role="menuitem"]:not([aria-disabled="true"])')
|
||||
const index = Array.from(items).indexOf(triggerRef.current)
|
||||
setItemIndex(index)
|
||||
}, [])
|
||||
|
||||
if (isInFolder && currentFolder !== id) return null
|
||||
// If this folder is open via click (inline mode), render children directly
|
||||
if (currentFolder === id) return <>{children}</>
|
||||
|
||||
const handleClickOpen = () => {
|
||||
@@ -701,22 +883,23 @@ const PopoverFolder = React.forwardRef<HTMLDivElement, PopoverFolderProps>(
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (expandOnHover) {
|
||||
// In hover mode, clicking opens inline and clears hover state
|
||||
setLastHoveredItem(null)
|
||||
}
|
||||
handleClickOpen()
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (itemIndex >= 0) {
|
||||
setSelectedIndex(itemIndex)
|
||||
}
|
||||
|
||||
if (!expandOnHover) return
|
||||
|
||||
// Calculate position for submenu
|
||||
if (triggerRef.current) {
|
||||
const rect = triggerRef.current.getBoundingClientRect()
|
||||
const parentPopover = triggerRef.current.closest('[data-radix-popper-content-wrapper]')
|
||||
const parentRect = parentPopover?.getBoundingClientRect()
|
||||
|
||||
// Position to the right of the parent popover with a small gap
|
||||
setSubmenuPosition({
|
||||
top: rect.top,
|
||||
left: parentRect ? parentRect.right + 4 : rect.right + 4,
|
||||
@@ -727,6 +910,11 @@ const PopoverFolder = React.forwardRef<HTMLDivElement, PopoverFolderProps>(
|
||||
onOpen?.()
|
||||
}
|
||||
|
||||
const isActive = active !== undefined ? active : itemIndex >= 0 && selectedIndex === itemIndex
|
||||
|
||||
// Suppress hover when in keyboard mode to prevent dual highlights
|
||||
const suppressHover = isKeyboardNav && !isActive && !isHoverOpen
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -735,12 +923,14 @@ const PopoverFolder = React.forwardRef<HTMLDivElement, PopoverFolderProps>(
|
||||
STYLES.itemBase,
|
||||
STYLES.colorScheme[colorScheme].text,
|
||||
STYLES.size[size].item,
|
||||
getItemStateClasses(variant, colorScheme, !!active || isHoverOpen),
|
||||
getItemStateClasses(variant, colorScheme, isActive || isHoverOpen),
|
||||
suppressHover && 'hover:!bg-transparent',
|
||||
className
|
||||
)}
|
||||
role='menuitem'
|
||||
aria-haspopup='true'
|
||||
aria-expanded={isHoverOpen}
|
||||
aria-selected={isActive}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
{...props}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { syncThemeToNextThemes } from '@/lib/core/utils/theme'
|
||||
import { useGeneralStore } from '@/stores/settings/general'
|
||||
|
||||
const logger = createLogger('GeneralSettingsQuery')
|
||||
|
||||
@@ -53,48 +52,16 @@ async function fetchGeneralSettings(): Promise<GeneralSettings> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync React Query cache to Zustand store and next-themes.
|
||||
* This ensures the rest of the app (which uses Zustand) stays in sync.
|
||||
* Uses shallow comparison to prevent unnecessary updates and flickering.
|
||||
* @param settings - The general settings to sync
|
||||
*/
|
||||
function syncSettingsToZustand(settings: GeneralSettings) {
|
||||
const store = useGeneralStore.getState()
|
||||
|
||||
const newSettings = {
|
||||
isAutoConnectEnabled: settings.autoConnect,
|
||||
showTrainingControls: settings.showTrainingControls,
|
||||
superUserModeEnabled: settings.superUserModeEnabled,
|
||||
theme: settings.theme,
|
||||
telemetryEnabled: settings.telemetryEnabled,
|
||||
isBillingUsageNotificationsEnabled: settings.billingUsageNotificationsEnabled,
|
||||
isErrorNotificationsEnabled: settings.errorNotificationsEnabled,
|
||||
snapToGridSize: settings.snapToGridSize,
|
||||
showActionBar: settings.showActionBar,
|
||||
}
|
||||
|
||||
const hasChanges = Object.entries(newSettings).some(
|
||||
([key, value]) => store[key as keyof typeof newSettings] !== value
|
||||
)
|
||||
|
||||
if (hasChanges) {
|
||||
store.setSettings(newSettings)
|
||||
}
|
||||
|
||||
syncThemeToNextThemes(settings.theme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch general settings.
|
||||
* Syncs to Zustand store only on successful fetch (not on cache updates from mutations).
|
||||
* TanStack Query is now the single source of truth for general settings.
|
||||
*/
|
||||
export function useGeneralSettings() {
|
||||
return useQuery({
|
||||
queryKey: generalSettingsKeys.settings(),
|
||||
queryFn: async () => {
|
||||
const settings = await fetchGeneralSettings()
|
||||
syncSettingsToZustand(settings)
|
||||
syncThemeToNextThemes(settings.theme)
|
||||
return settings
|
||||
},
|
||||
staleTime: 60 * 60 * 1000,
|
||||
@@ -102,6 +69,41 @@ export function useGeneralSettings() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience selector hooks for individual settings.
|
||||
* These provide a simple API for components that only need a single setting value.
|
||||
*/
|
||||
|
||||
export function useAutoConnect(): boolean {
|
||||
const { data } = useGeneralSettings()
|
||||
return data?.autoConnect ?? true
|
||||
}
|
||||
|
||||
export function useShowTrainingControls(): boolean {
|
||||
const { data } = useGeneralSettings()
|
||||
return data?.showTrainingControls ?? false
|
||||
}
|
||||
|
||||
export function useSnapToGridSize(): number {
|
||||
const { data } = useGeneralSettings()
|
||||
return data?.snapToGridSize ?? 0
|
||||
}
|
||||
|
||||
export function useShowActionBar(): boolean {
|
||||
const { data } = useGeneralSettings()
|
||||
return data?.showActionBar ?? true
|
||||
}
|
||||
|
||||
export function useBillingUsageNotifications(): boolean {
|
||||
const { data } = useGeneralSettings()
|
||||
return data?.billingUsageNotificationsEnabled ?? true
|
||||
}
|
||||
|
||||
export function useErrorNotificationsEnabled(): boolean {
|
||||
const { data } = useGeneralSettings()
|
||||
return data?.errorNotificationsEnabled ?? true
|
||||
}
|
||||
|
||||
/**
|
||||
* Update general settings mutation
|
||||
*/
|
||||
@@ -141,7 +143,10 @@ export function useUpdateGeneralSetting() {
|
||||
}
|
||||
|
||||
queryClient.setQueryData<GeneralSettings>(generalSettingsKeys.settings(), newSettings)
|
||||
syncSettingsToZustand(newSettings)
|
||||
|
||||
if (key === 'theme') {
|
||||
syncThemeToNextThemes(value as GeneralSettings['theme'])
|
||||
}
|
||||
}
|
||||
|
||||
return { previousSettings }
|
||||
@@ -149,7 +154,7 @@ export function useUpdateGeneralSetting() {
|
||||
onError: (err, _variables, context) => {
|
||||
if (context?.previousSettings) {
|
||||
queryClient.setQueryData(generalSettingsKeys.settings(), context.previousSettings)
|
||||
syncSettingsToZustand(context.previousSettings)
|
||||
syncThemeToNextThemes(context.previousSettings.theme)
|
||||
}
|
||||
logger.error('Failed to update setting:', err)
|
||||
},
|
||||
|
||||
@@ -110,6 +110,8 @@ function resolveFileSelector(
|
||||
return { key: 'google.drive', context, allowSearch: true }
|
||||
case 'google-slides':
|
||||
return { key: 'google.drive', context, allowSearch: true }
|
||||
case 'google-forms':
|
||||
return { key: 'google.drive', context, allowSearch: true }
|
||||
case 'onedrive': {
|
||||
const key: SelectorKey = subBlock.mimeType === 'file' ? 'onedrive.files' : 'onedrive.folders'
|
||||
return { key, context, allowSearch: true }
|
||||
|
||||
@@ -884,6 +884,8 @@ export const auth = betterAuth({
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/forms.body',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
prompt: 'consent',
|
||||
|
||||
@@ -96,13 +96,15 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
|
||||
},
|
||||
'google-forms': {
|
||||
name: 'Google Forms',
|
||||
description: 'Retrieve Google Form responses.',
|
||||
description: 'Create, modify, and read Google Forms.',
|
||||
providerId: 'google-forms',
|
||||
icon: GoogleFormsIcon,
|
||||
baseProviderIcon: GoogleIcon,
|
||||
scopes: [
|
||||
'https://www.googleapis.com/auth/userinfo.email',
|
||||
'https://www.googleapis.com/auth/userinfo.profile',
|
||||
'https://www.googleapis.com/auth/drive',
|
||||
'https://www.googleapis.com/auth/forms.body',
|
||||
'https://www.googleapis.com/auth/forms.responses.readonly',
|
||||
],
|
||||
},
|
||||
|
||||
@@ -62,8 +62,11 @@ function evaluateOutputCondition(
|
||||
let andMatches: boolean
|
||||
|
||||
if (Array.isArray(condition.and.value)) {
|
||||
andMatches =
|
||||
const primitiveMatch =
|
||||
isConditionPrimitive(andFieldValue) && condition.and.value.includes(andFieldValue)
|
||||
const undefinedMatch = andFieldValue === undefined && condition.and.value.includes(undefined)
|
||||
const nullMatch = andFieldValue === null && condition.and.value.includes(null)
|
||||
andMatches = primitiveMatch || undefinedMatch || nullMatch
|
||||
} else {
|
||||
andMatches = andFieldValue === condition.and.value
|
||||
}
|
||||
@@ -95,7 +98,9 @@ function filterOutputsByCondition(
|
||||
}
|
||||
|
||||
const condition = value.condition as OutputCondition | undefined
|
||||
if (!condition || evaluateOutputCondition(condition, subBlocks)) {
|
||||
const passes = !condition || evaluateOutputCondition(condition, subBlocks)
|
||||
|
||||
if (passes) {
|
||||
const { condition: _, ...rest } = value
|
||||
filtered[key] = rest
|
||||
}
|
||||
@@ -565,11 +570,32 @@ export function getToolOutputs(blockConfig: BlockConfig, operation: string): Rec
|
||||
*
|
||||
* @param blockConfig - The block configuration containing tools config
|
||||
* @param operation - The selected operation for the tool
|
||||
* @param subBlocks - Optional subBlock values for condition evaluation
|
||||
* @returns Array of output paths for the tool, or empty array on error
|
||||
*/
|
||||
export function getToolOutputPaths(blockConfig: BlockConfig, operation: string): string[] {
|
||||
export function getToolOutputPaths(
|
||||
blockConfig: BlockConfig,
|
||||
operation: string,
|
||||
subBlocks?: Record<string, SubBlockWithValue>
|
||||
): string[] {
|
||||
const outputs = getToolOutputs(blockConfig, operation)
|
||||
|
||||
if (!outputs || Object.keys(outputs).length === 0) return []
|
||||
|
||||
if (subBlocks && blockConfig.outputs) {
|
||||
const filteredBlockOutputs = filterOutputsByCondition(blockConfig.outputs, subBlocks)
|
||||
const allowedKeys = new Set(Object.keys(filteredBlockOutputs))
|
||||
|
||||
const filteredOutputs: Record<string, any> = {}
|
||||
for (const [key, value] of Object.entries(outputs)) {
|
||||
if (allowedKeys.has(key)) {
|
||||
filteredOutputs[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return generateOutputPaths(filteredOutputs)
|
||||
}
|
||||
|
||||
return generateOutputPaths(outputs)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { useGeneralStore } from './store'
|
||||
export type { General, GeneralStore, UserSettings } from './types'
|
||||
@@ -1,37 +0,0 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { create } from 'zustand'
|
||||
import { devtools } from 'zustand/middleware'
|
||||
import type { General, GeneralStore } from './types'
|
||||
|
||||
const logger = createLogger('GeneralStore')
|
||||
|
||||
const initialState: General = {
|
||||
isAutoConnectEnabled: true,
|
||||
showTrainingControls: false,
|
||||
superUserModeEnabled: true,
|
||||
theme: 'dark',
|
||||
telemetryEnabled: true,
|
||||
isBillingUsageNotificationsEnabled: true,
|
||||
isErrorNotificationsEnabled: true,
|
||||
snapToGridSize: 0,
|
||||
showActionBar: true,
|
||||
}
|
||||
|
||||
export const useGeneralStore = create<GeneralStore>()(
|
||||
devtools(
|
||||
(set) => ({
|
||||
...initialState,
|
||||
setSettings: (settings) => {
|
||||
logger.debug('Updating general settings store', {
|
||||
keys: Object.keys(settings),
|
||||
})
|
||||
set((state) => ({
|
||||
...state,
|
||||
...settings,
|
||||
}))
|
||||
},
|
||||
reset: () => set(initialState),
|
||||
}),
|
||||
{ name: 'general-store' }
|
||||
)
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
export interface General {
|
||||
isAutoConnectEnabled: boolean
|
||||
showTrainingControls: boolean
|
||||
superUserModeEnabled: boolean
|
||||
theme: 'system' | 'light' | 'dark'
|
||||
telemetryEnabled: boolean
|
||||
isBillingUsageNotificationsEnabled: boolean
|
||||
isErrorNotificationsEnabled: boolean
|
||||
snapToGridSize: number
|
||||
showActionBar: boolean
|
||||
}
|
||||
|
||||
export interface GeneralStore extends General {
|
||||
setSettings: (settings: Partial<General>) => void
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export type UserSettings = {
|
||||
theme: 'system' | 'light' | 'dark'
|
||||
autoConnect: boolean
|
||||
showTrainingControls: boolean
|
||||
superUserModeEnabled: boolean
|
||||
telemetryEnabled: boolean
|
||||
isBillingUsageNotificationsEnabled: boolean
|
||||
errorNotificationsEnabled: boolean
|
||||
snapToGridSize: number
|
||||
showActionBar: boolean
|
||||
}
|
||||
@@ -2,10 +2,11 @@ import { createLogger } from '@sim/logger'
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, devtools, persist } from 'zustand/middleware'
|
||||
import { redactApiKeys } from '@/lib/core/security/redaction'
|
||||
import { getQueryClient } from '@/app/_shell/providers/query-provider'
|
||||
import type { NormalizedBlockOutput } from '@/executor/types'
|
||||
import { type GeneralSettings, generalSettingsKeys } from '@/hooks/queries/general-settings'
|
||||
import { useExecutionStore } from '@/stores/execution'
|
||||
import { useNotificationStore } from '@/stores/notifications'
|
||||
import { useGeneralStore } from '@/stores/settings/general'
|
||||
import { indexedDBStorage } from '@/stores/terminal/console/storage'
|
||||
import type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from '@/stores/terminal/console/types'
|
||||
|
||||
@@ -153,7 +154,10 @@ export const useTerminalConsoleStore = create<ConsoleStore>()(
|
||||
const newEntry = get().entries[0]
|
||||
|
||||
if (newEntry?.error) {
|
||||
const { isErrorNotificationsEnabled } = useGeneralStore.getState()
|
||||
const settings = getQueryClient().getQueryData<GeneralSettings>(
|
||||
generalSettingsKeys.settings()
|
||||
)
|
||||
const isErrorNotificationsEnabled = settings?.errorNotificationsEnabled ?? true
|
||||
|
||||
if (isErrorNotificationsEnabled) {
|
||||
try {
|
||||
|
||||
@@ -241,16 +241,94 @@ export const compareCommitsV2Tool: ToolConfig<CompareCommitsParams, any> = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
status: { type: 'string', description: 'Comparison status' },
|
||||
ahead_by: { type: 'number', description: 'Commits ahead' },
|
||||
behind_by: { type: 'number', description: 'Commits behind' },
|
||||
total_commits: { type: 'number', description: 'Total commits' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
diff_url: { type: 'string', description: 'Diff URL' },
|
||||
patch_url: { type: 'string', description: 'Patch URL' },
|
||||
base_commit: { type: 'object', description: 'Base commit' },
|
||||
merge_base_commit: { type: 'object', description: 'Merge base' },
|
||||
commits: { type: 'array', description: 'Commits between' },
|
||||
files: { type: 'array', description: 'Changed files' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
permalink_url: { type: 'string', description: 'Permanent link URL' },
|
||||
diff_url: { type: 'string', description: 'Diff download URL' },
|
||||
patch_url: { type: 'string', description: 'Patch download URL' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'Comparison status (ahead, behind, identical, diverged)',
|
||||
},
|
||||
ahead_by: { type: 'number', description: 'Commits head is ahead of base' },
|
||||
behind_by: { type: 'number', description: 'Commits head is behind base' },
|
||||
total_commits: { type: 'number', description: 'Total commits in comparison' },
|
||||
base_commit: {
|
||||
type: 'object',
|
||||
description: 'Base commit object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Commit SHA' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
commit: {
|
||||
type: 'object',
|
||||
description: 'Commit data',
|
||||
properties: {
|
||||
message: { type: 'string', description: 'Commit message' },
|
||||
author: { type: 'object', description: 'Git author (name, email, date)' },
|
||||
committer: { type: 'object', description: 'Git committer (name, email, date)' },
|
||||
},
|
||||
},
|
||||
author: { type: 'object', description: 'GitHub user (author)', optional: true },
|
||||
committer: { type: 'object', description: 'GitHub user (committer)', optional: true },
|
||||
},
|
||||
},
|
||||
merge_base_commit: {
|
||||
type: 'object',
|
||||
description: 'Merge base commit object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Commit SHA' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
},
|
||||
},
|
||||
commits: {
|
||||
type: 'array',
|
||||
description: 'Commits between base and head',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Commit SHA' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
commit: {
|
||||
type: 'object',
|
||||
description: 'Commit data',
|
||||
properties: {
|
||||
message: { type: 'string', description: 'Commit message' },
|
||||
author: { type: 'object', description: 'Git author (name, email, date)' },
|
||||
committer: { type: 'object', description: 'Git committer (name, email, date)' },
|
||||
},
|
||||
},
|
||||
author: { type: 'object', description: 'GitHub user', optional: true },
|
||||
committer: { type: 'object', description: 'GitHub user', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
files: {
|
||||
type: 'array',
|
||||
description: 'Changed files (diff entries)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Blob SHA', optional: true },
|
||||
filename: { type: 'string', description: 'File path' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Change status (added, removed, modified, renamed, copied, changed, unchanged)',
|
||||
},
|
||||
additions: { type: 'number', description: 'Lines added' },
|
||||
deletions: { type: 'number', description: 'Lines deleted' },
|
||||
changes: { type: 'number', description: 'Total changes' },
|
||||
blob_url: { type: 'string', description: 'Blob URL' },
|
||||
raw_url: { type: 'string', description: 'Raw file URL' },
|
||||
contents_url: { type: 'string', description: 'Contents API URL' },
|
||||
patch: { type: 'string', description: 'Diff patch', optional: true },
|
||||
previous_filename: {
|
||||
type: 'string',
|
||||
description: 'Previous filename (for renames)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -131,8 +131,26 @@ export const createCommentReactionV2Tool: ToolConfig<CreateCommentReactionParams
|
||||
|
||||
outputs: {
|
||||
id: { type: 'number', description: 'Reaction ID' },
|
||||
user: { type: 'object', description: 'User who reacted' },
|
||||
content: { type: 'string', description: 'Reaction type' },
|
||||
created_at: { type: 'string', description: 'Creation date' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Reaction type (+1, -1, laugh, confused, heart, hooray, rocket, eyes)',
|
||||
},
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
user: {
|
||||
type: 'object',
|
||||
description: 'User who reacted',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -170,14 +170,39 @@ export const createGistV2Tool: ToolConfig<CreateGistParams, any> = {
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Gist ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
forks_url: { type: 'string', description: 'Forks API URL' },
|
||||
commits_url: { type: 'string', description: 'Commits API URL' },
|
||||
git_pull_url: { type: 'string', description: 'Git pull URL' },
|
||||
git_push_url: { type: 'string', description: 'Git push URL' },
|
||||
description: { type: 'string', description: 'Description', optional: true },
|
||||
public: { type: 'boolean', description: 'Is public' },
|
||||
created_at: { type: 'string', description: 'Creation date' },
|
||||
updated_at: { type: 'string', description: 'Update date' },
|
||||
files: { type: 'object', description: 'Files in gist' },
|
||||
owner: { type: 'object', description: 'Owner info' },
|
||||
description: { type: 'string', description: 'Gist description', optional: true },
|
||||
public: { type: 'boolean', description: 'Whether gist is public' },
|
||||
truncated: { type: 'boolean', description: 'Whether files are truncated' },
|
||||
comments: { type: 'number', description: 'Number of comments' },
|
||||
comments_url: { type: 'string', description: 'Comments API URL' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
files: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Files in the gist (object with filenames as keys, each containing filename, type, language, raw_url, size, truncated, content)',
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Gist owner',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -131,8 +131,26 @@ export const createIssueReactionV2Tool: ToolConfig<CreateIssueReactionParams, an
|
||||
|
||||
outputs: {
|
||||
id: { type: 'number', description: 'Reaction ID' },
|
||||
user: { type: 'object', description: 'User who reacted' },
|
||||
content: { type: 'string', description: 'Reaction type' },
|
||||
created_at: { type: 'string', description: 'Creation date' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'Reaction type (+1, -1, laugh, confused, heart, hooray, rocket, eyes)',
|
||||
},
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
user: {
|
||||
type: 'object',
|
||||
description: 'User who reacted',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -167,13 +167,63 @@ export const forkRepoV2Tool: ToolConfig<ForkRepoParams, any> = {
|
||||
|
||||
outputs: {
|
||||
id: { type: 'number', description: 'Repository ID' },
|
||||
full_name: { type: 'string', description: 'Full name' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
clone_url: { type: 'string', description: 'Clone URL' },
|
||||
ssh_url: { type: 'string', description: 'SSH URL' },
|
||||
default_branch: { type: 'string', description: 'Default branch' },
|
||||
fork: { type: 'boolean', description: 'Is a fork' },
|
||||
parent: { type: 'object', description: 'Parent repository', optional: true },
|
||||
owner: { type: 'object', description: 'Owner' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
name: { type: 'string', description: 'Repository name' },
|
||||
full_name: { type: 'string', description: 'Full name (owner/repo)' },
|
||||
private: { type: 'boolean', description: 'Whether repository is private' },
|
||||
description: { type: 'string', description: 'Repository description', optional: true },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
clone_url: { type: 'string', description: 'HTTPS clone URL' },
|
||||
ssh_url: { type: 'string', description: 'SSH clone URL' },
|
||||
git_url: { type: 'string', description: 'Git protocol URL' },
|
||||
default_branch: { type: 'string', description: 'Default branch name' },
|
||||
fork: { type: 'boolean', description: 'Whether this is a fork' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
pushed_at: { type: 'string', description: 'Last push timestamp', optional: true },
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Fork owner',
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
type: 'object',
|
||||
description: 'Parent repository (source of the fork)',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Repository ID' },
|
||||
full_name: { type: 'string', description: 'Full name' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
description: { type: 'string', description: 'Description', optional: true },
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Parent owner',
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
source: {
|
||||
type: 'object',
|
||||
description: 'Source repository (ultimate origin)',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Repository ID' },
|
||||
full_name: { type: 'string', description: 'Full name' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -191,12 +191,128 @@ export const getCommitV2Tool: ToolConfig<GetCommitParams, any> = {
|
||||
|
||||
outputs: {
|
||||
sha: { type: 'string', description: 'Commit SHA' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
commit: { type: 'object', description: 'Commit data' },
|
||||
author: { type: 'object', description: 'GitHub user', optional: true },
|
||||
committer: { type: 'object', description: 'GitHub user', optional: true },
|
||||
stats: { type: 'object', description: 'Change stats', optional: true },
|
||||
files: { type: 'array', description: 'Changed files' },
|
||||
parents: { type: 'array', description: 'Parent commits' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
comments_url: { type: 'string', description: 'Comments API URL' },
|
||||
commit: {
|
||||
type: 'object',
|
||||
description: 'Core commit data',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'Commit API URL' },
|
||||
message: { type: 'string', description: 'Commit message' },
|
||||
comment_count: { type: 'number', description: 'Number of comments' },
|
||||
author: {
|
||||
type: 'object',
|
||||
description: 'Git author',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Author name' },
|
||||
email: { type: 'string', description: 'Author email' },
|
||||
date: { type: 'string', description: 'Author date (ISO 8601)' },
|
||||
},
|
||||
},
|
||||
committer: {
|
||||
type: 'object',
|
||||
description: 'Git committer',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Committer name' },
|
||||
email: { type: 'string', description: 'Committer email' },
|
||||
date: { type: 'string', description: 'Commit date (ISO 8601)' },
|
||||
},
|
||||
},
|
||||
tree: {
|
||||
type: 'object',
|
||||
description: 'Tree object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Tree SHA' },
|
||||
url: { type: 'string', description: 'Tree API URL' },
|
||||
},
|
||||
},
|
||||
verification: {
|
||||
type: 'object',
|
||||
description: 'Signature verification',
|
||||
properties: {
|
||||
verified: { type: 'boolean', description: 'Whether signature is verified' },
|
||||
reason: { type: 'string', description: 'Verification reason' },
|
||||
signature: { type: 'string', description: 'GPG signature', optional: true },
|
||||
payload: { type: 'string', description: 'Signed payload', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
type: 'object',
|
||||
description: 'GitHub user (author)',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar URL' },
|
||||
html_url: { type: 'string', description: 'Profile URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
},
|
||||
},
|
||||
committer: {
|
||||
type: 'object',
|
||||
description: 'GitHub user (committer)',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar URL' },
|
||||
html_url: { type: 'string', description: 'Profile URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
type: 'object',
|
||||
description: 'Change statistics',
|
||||
optional: true,
|
||||
properties: {
|
||||
additions: { type: 'number', description: 'Lines added' },
|
||||
deletions: { type: 'number', description: 'Lines deleted' },
|
||||
total: { type: 'number', description: 'Total changes' },
|
||||
},
|
||||
},
|
||||
files: {
|
||||
type: 'array',
|
||||
description: 'Changed files (diff entries)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Blob SHA', optional: true },
|
||||
filename: { type: 'string', description: 'File path' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Change status (added, removed, modified, renamed, copied, changed, unchanged)',
|
||||
},
|
||||
additions: { type: 'number', description: 'Lines added' },
|
||||
deletions: { type: 'number', description: 'Lines deleted' },
|
||||
changes: { type: 'number', description: 'Total changes' },
|
||||
blob_url: { type: 'string', description: 'Blob URL' },
|
||||
raw_url: { type: 'string', description: 'Raw file URL' },
|
||||
contents_url: { type: 'string', description: 'Contents API URL' },
|
||||
patch: { type: 'string', description: 'Diff patch', optional: true },
|
||||
previous_filename: {
|
||||
type: 'string',
|
||||
description: 'Previous filename (for renames)',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
parents: {
|
||||
type: 'array',
|
||||
description: 'Parent commits',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Parent SHA' },
|
||||
url: { type: 'string', description: 'Parent API URL' },
|
||||
html_url: { type: 'string', description: 'Parent web URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -165,13 +165,52 @@ export const getGistV2Tool: ToolConfig<GetGistParams, any> = {
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Gist ID' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
description: { type: 'string', description: 'Description', optional: true },
|
||||
public: { type: 'boolean', description: 'Is public' },
|
||||
created_at: { type: 'string', description: 'Creation date' },
|
||||
updated_at: { type: 'string', description: 'Update date' },
|
||||
files: { type: 'object', description: 'Files with content' },
|
||||
owner: { type: 'object', description: 'Owner info' },
|
||||
comments: { type: 'number', description: 'Comment count' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
forks_url: { type: 'string', description: 'Forks API URL' },
|
||||
commits_url: { type: 'string', description: 'Commits API URL' },
|
||||
git_pull_url: { type: 'string', description: 'Git clone URL' },
|
||||
git_push_url: { type: 'string', description: 'Git push URL' },
|
||||
description: { type: 'string', description: 'Gist description', optional: true },
|
||||
public: { type: 'boolean', description: 'Whether gist is public' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
comments: { type: 'number', description: 'Number of comments' },
|
||||
comments_url: { type: 'string', description: 'Comments API URL' },
|
||||
truncated: { type: 'boolean', description: 'Whether content is truncated' },
|
||||
files: {
|
||||
type: 'object',
|
||||
description: 'Files in the gist (keyed by filename)',
|
||||
properties: {
|
||||
'[filename]': {
|
||||
type: 'object',
|
||||
description: 'File object',
|
||||
properties: {
|
||||
filename: { type: 'string', description: 'File name' },
|
||||
type: { type: 'string', description: 'MIME type' },
|
||||
language: { type: 'string', description: 'Programming language', optional: true },
|
||||
raw_url: { type: 'string', description: 'Raw file URL' },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
truncated: { type: 'boolean', description: 'Whether content is truncated' },
|
||||
content: { type: 'string', description: 'File content' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Gist owner',
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -153,15 +153,35 @@ export const getMilestoneV2Tool: ToolConfig<GetMilestoneParams, any> = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'number', description: 'Milestone ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
number: { type: 'number', description: 'Milestone number' },
|
||||
title: { type: 'string', description: 'Title' },
|
||||
description: { type: 'string', description: 'Description', optional: true },
|
||||
state: { type: 'string', description: 'State' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
due_on: { type: 'string', description: 'Due date', optional: true },
|
||||
open_issues: { type: 'number', description: 'Open issues' },
|
||||
closed_issues: { type: 'number', description: 'Closed issues' },
|
||||
closed_at: { type: 'string', description: 'Close date', optional: true },
|
||||
creator: { type: 'object', description: 'Creator' },
|
||||
title: { type: 'string', description: 'Milestone title' },
|
||||
description: { type: 'string', description: 'Milestone description', optional: true },
|
||||
state: { type: 'string', description: 'State (open or closed)' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
labels_url: { type: 'string', description: 'Labels API URL' },
|
||||
due_on: { type: 'string', description: 'Due date (ISO 8601)', optional: true },
|
||||
open_issues: { type: 'number', description: 'Number of open issues' },
|
||||
closed_issues: { type: 'number', description: 'Number of closed issues' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
closed_at: { type: 'string', description: 'Close timestamp', optional: true },
|
||||
creator: {
|
||||
type: 'object',
|
||||
description: 'Milestone creator',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -230,11 +230,97 @@ export const listCommitsV2Tool: ToolConfig<ListCommitsParams, any> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Commit SHA' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
commit: { type: 'object', description: 'Commit data' },
|
||||
author: { type: 'object', description: 'GitHub user', optional: true },
|
||||
committer: { type: 'object', description: 'GitHub user', optional: true },
|
||||
parents: { type: 'array', description: 'Parent commits' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
comments_url: { type: 'string', description: 'Comments API URL' },
|
||||
commit: {
|
||||
type: 'object',
|
||||
description: 'Core commit data',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'Commit API URL' },
|
||||
message: { type: 'string', description: 'Commit message' },
|
||||
comment_count: { type: 'number', description: 'Number of comments' },
|
||||
author: {
|
||||
type: 'object',
|
||||
description: 'Git author',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Author name' },
|
||||
email: { type: 'string', description: 'Author email' },
|
||||
date: { type: 'string', description: 'Author date (ISO 8601)' },
|
||||
},
|
||||
},
|
||||
committer: {
|
||||
type: 'object',
|
||||
description: 'Git committer',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Committer name' },
|
||||
email: { type: 'string', description: 'Committer email' },
|
||||
date: { type: 'string', description: 'Commit date (ISO 8601)' },
|
||||
},
|
||||
},
|
||||
tree: {
|
||||
type: 'object',
|
||||
description: 'Tree object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Tree SHA' },
|
||||
url: { type: 'string', description: 'Tree API URL' },
|
||||
},
|
||||
},
|
||||
verification: {
|
||||
type: 'object',
|
||||
description: 'Signature verification',
|
||||
properties: {
|
||||
verified: { type: 'boolean', description: 'Whether signature is verified' },
|
||||
reason: { type: 'string', description: 'Verification reason' },
|
||||
signature: { type: 'string', description: 'GPG signature', optional: true },
|
||||
payload: { type: 'string', description: 'Signed payload', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
type: 'object',
|
||||
description: 'GitHub user (author)',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
committer: {
|
||||
type: 'object',
|
||||
description: 'GitHub user (committer)',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
parents: {
|
||||
type: 'array',
|
||||
description: 'Parent commits',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Parent SHA' },
|
||||
url: { type: 'string', description: 'Parent API URL' },
|
||||
html_url: { type: 'string', description: 'Parent web URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -188,11 +188,41 @@ export const listForksV2Tool: ToolConfig<ListForksParams, any> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Repository ID' },
|
||||
full_name: { type: 'string', description: 'Full name' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
owner: { type: 'object', description: 'Owner' },
|
||||
stargazers_count: { type: 'number', description: 'Stars' },
|
||||
forks_count: { type: 'number', description: 'Forks' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
name: { type: 'string', description: 'Repository name' },
|
||||
full_name: { type: 'string', description: 'Full name (owner/repo)' },
|
||||
private: { type: 'boolean', description: 'Whether repository is private' },
|
||||
description: { type: 'string', description: 'Repository description', optional: true },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
fork: { type: 'boolean', description: 'Whether this is a fork' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
pushed_at: { type: 'string', description: 'Last push timestamp', optional: true },
|
||||
size: { type: 'number', description: 'Repository size in KB' },
|
||||
stargazers_count: { type: 'number', description: 'Number of stars' },
|
||||
watchers_count: { type: 'number', description: 'Number of watchers' },
|
||||
forks_count: { type: 'number', description: 'Number of forks' },
|
||||
open_issues_count: { type: 'number', description: 'Number of open issues' },
|
||||
language: { type: 'string', description: 'Primary programming language', optional: true },
|
||||
default_branch: { type: 'string', description: 'Default branch name' },
|
||||
visibility: { type: 'string', description: 'Repository visibility' },
|
||||
archived: { type: 'boolean', description: 'Whether repository is archived' },
|
||||
disabled: { type: 'boolean', description: 'Whether repository is disabled' },
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Fork owner',
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -188,11 +188,40 @@ export const listGistsV2Tool: ToolConfig<ListGistsParams, any> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Gist ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
description: { type: 'string', description: 'Description', optional: true },
|
||||
public: { type: 'boolean', description: 'Is public' },
|
||||
files: { type: 'object', description: 'Files' },
|
||||
owner: { type: 'object', description: 'Owner' },
|
||||
forks_url: { type: 'string', description: 'Forks API URL' },
|
||||
commits_url: { type: 'string', description: 'Commits API URL' },
|
||||
git_pull_url: { type: 'string', description: 'Git pull URL' },
|
||||
git_push_url: { type: 'string', description: 'Git push URL' },
|
||||
description: { type: 'string', description: 'Gist description', optional: true },
|
||||
public: { type: 'boolean', description: 'Whether gist is public' },
|
||||
truncated: { type: 'boolean', description: 'Whether files are truncated' },
|
||||
comments: { type: 'number', description: 'Number of comments' },
|
||||
comments_url: { type: 'string', description: 'Comments API URL' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
files: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Files in the gist (object with filenames as keys, each containing filename, type, language, raw_url, size)',
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Gist owner',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -212,12 +212,36 @@ export const listMilestonesV2Tool: ToolConfig<ListMilestonesParams, any> = {
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Milestone ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
number: { type: 'number', description: 'Milestone number' },
|
||||
title: { type: 'string', description: 'Title' },
|
||||
state: { type: 'string', description: 'State' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
open_issues: { type: 'number', description: 'Open issues' },
|
||||
closed_issues: { type: 'number', description: 'Closed issues' },
|
||||
title: { type: 'string', description: 'Milestone title' },
|
||||
description: { type: 'string', description: 'Milestone description', optional: true },
|
||||
state: { type: 'string', description: 'State (open or closed)' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
labels_url: { type: 'string', description: 'Labels API URL' },
|
||||
due_on: { type: 'string', description: 'Due date (ISO 8601)', optional: true },
|
||||
open_issues: { type: 'number', description: 'Number of open issues' },
|
||||
closed_issues: { type: 'number', description: 'Number of closed issues' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
closed_at: { type: 'string', description: 'Close timestamp', optional: true },
|
||||
creator: {
|
||||
type: 'object',
|
||||
description: 'Milestone creator',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -161,9 +161,18 @@ export const listStargazersV2Tool: ToolConfig<ListStargazersParams, any> = {
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar URL' },
|
||||
html_url: { type: 'string', description: 'Profile URL' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
gravatar_id: { type: 'string', description: 'Gravatar ID' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
followers_url: { type: 'string', description: 'Followers API URL' },
|
||||
following_url: { type: 'string', description: 'Following API URL' },
|
||||
gists_url: { type: 'string', description: 'Gists API URL' },
|
||||
starred_url: { type: 'string', description: 'Starred API URL' },
|
||||
repos_url: { type: 'string', description: 'Repos API URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -202,8 +202,67 @@ export const searchCodeV2Tool: ToolConfig<SearchCodeParams, any> = {
|
||||
name: { type: 'string', description: 'File name' },
|
||||
path: { type: 'string', description: 'File path' },
|
||||
sha: { type: 'string', description: 'Blob SHA' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
git_url: { type: 'string', description: 'Git blob URL' },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
repository: { type: 'object', description: 'Repository object' },
|
||||
score: { type: 'number', description: 'Search relevance score' },
|
||||
repository: {
|
||||
type: 'object',
|
||||
description: 'Repository containing the code',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Repository ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
name: { type: 'string', description: 'Repository name' },
|
||||
full_name: { type: 'string', description: 'Full name (owner/repo)' },
|
||||
private: { type: 'boolean', description: 'Whether repository is private' },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Repository description',
|
||||
optional: true,
|
||||
},
|
||||
fork: { type: 'boolean', description: 'Whether this is a fork' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Repository owner',
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
text_matches: {
|
||||
type: 'array',
|
||||
description: 'Text matches showing context',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
object_url: { type: 'string', description: 'Object URL' },
|
||||
object_type: { type: 'string', description: 'Object type', optional: true },
|
||||
property: { type: 'string', description: 'Property matched' },
|
||||
fragment: { type: 'string', description: 'Text fragment with match' },
|
||||
matches: {
|
||||
type: 'array',
|
||||
description: 'Match indices',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
text: { type: 'string', description: 'Matched text' },
|
||||
indices: { type: 'array', description: 'Start and end indices' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -212,11 +212,119 @@ export const searchCommitsV2Tool: ToolConfig<SearchCommitsParams, any> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Commit SHA' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
commit: { type: 'object', description: 'Commit data' },
|
||||
author: { type: 'object', description: 'GitHub user', optional: true },
|
||||
committer: { type: 'object', description: 'GitHub user', optional: true },
|
||||
repository: { type: 'object', description: 'Repository' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
comments_url: { type: 'string', description: 'Comments API URL' },
|
||||
score: { type: 'number', description: 'Search relevance score' },
|
||||
commit: {
|
||||
type: 'object',
|
||||
description: 'Core commit data',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'Commit API URL' },
|
||||
message: { type: 'string', description: 'Commit message' },
|
||||
comment_count: { type: 'number', description: 'Number of comments' },
|
||||
author: {
|
||||
type: 'object',
|
||||
description: 'Git author',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Author name' },
|
||||
email: { type: 'string', description: 'Author email' },
|
||||
date: { type: 'string', description: 'Author date (ISO 8601)' },
|
||||
},
|
||||
},
|
||||
committer: {
|
||||
type: 'object',
|
||||
description: 'Git committer',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'Committer name' },
|
||||
email: { type: 'string', description: 'Committer email' },
|
||||
date: { type: 'string', description: 'Commit date (ISO 8601)' },
|
||||
},
|
||||
},
|
||||
tree: {
|
||||
type: 'object',
|
||||
description: 'Tree object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Tree SHA' },
|
||||
url: { type: 'string', description: 'Tree API URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
author: {
|
||||
type: 'object',
|
||||
description: 'GitHub user (author)',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
committer: {
|
||||
type: 'object',
|
||||
description: 'GitHub user (committer)',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
repository: {
|
||||
type: 'object',
|
||||
description: 'Repository containing the commit',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Repository ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
name: { type: 'string', description: 'Repository name' },
|
||||
full_name: { type: 'string', description: 'Full name (owner/repo)' },
|
||||
private: { type: 'boolean', description: 'Whether repository is private' },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Repository description',
|
||||
optional: true,
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Repository owner',
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
parents: {
|
||||
type: 'array',
|
||||
description: 'Parent commits',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
sha: { type: 'string', description: 'Parent SHA' },
|
||||
url: { type: 'string', description: 'Parent API URL' },
|
||||
html_url: { type: 'string', description: 'Parent web URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -222,17 +222,110 @@ export const searchIssuesV2Tool: ToolConfig<SearchIssuesParams, any> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Issue ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
number: { type: 'number', description: 'Issue number' },
|
||||
title: { type: 'string', description: 'Title' },
|
||||
state: { type: 'string', description: 'State' },
|
||||
state: { type: 'string', description: 'State (open or closed)' },
|
||||
locked: { type: 'boolean', description: 'Whether issue is locked' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
repository_url: { type: 'string', description: 'Repository API URL' },
|
||||
comments_url: { type: 'string', description: 'Comments API URL' },
|
||||
body: { type: 'string', description: 'Body text', optional: true },
|
||||
user: { type: 'object', description: 'Author' },
|
||||
labels: { type: 'array', description: 'Labels' },
|
||||
assignees: { type: 'array', description: 'Assignees' },
|
||||
created_at: { type: 'string', description: 'Creation date' },
|
||||
updated_at: { type: 'string', description: 'Update date' },
|
||||
closed_at: { type: 'string', description: 'Close date', optional: true },
|
||||
comments: { type: 'number', description: 'Number of comments' },
|
||||
score: { type: 'number', description: 'Search relevance score' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
closed_at: { type: 'string', description: 'Close timestamp', optional: true },
|
||||
user: {
|
||||
type: 'object',
|
||||
description: 'Issue author',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
labels: {
|
||||
type: 'array',
|
||||
description: 'Issue labels',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Label ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
name: { type: 'string', description: 'Label name' },
|
||||
description: { type: 'string', description: 'Label description', optional: true },
|
||||
color: { type: 'string', description: 'Hex color code' },
|
||||
default: { type: 'boolean', description: 'Whether this is a default label' },
|
||||
},
|
||||
},
|
||||
},
|
||||
assignee: {
|
||||
type: 'object',
|
||||
description: 'Primary assignee',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
assignees: {
|
||||
type: 'array',
|
||||
description: 'All assignees',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
milestone: {
|
||||
type: 'object',
|
||||
description: 'Associated milestone',
|
||||
optional: true,
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Milestone ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
number: { type: 'number', description: 'Milestone number' },
|
||||
title: { type: 'string', description: 'Milestone title' },
|
||||
description: { type: 'string', description: 'Milestone description', optional: true },
|
||||
state: { type: 'string', description: 'State (open or closed)' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
due_on: { type: 'string', description: 'Due date', optional: true },
|
||||
},
|
||||
},
|
||||
pull_request: {
|
||||
type: 'object',
|
||||
description: 'Pull request details (if this is a PR)',
|
||||
optional: true,
|
||||
properties: {
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
diff_url: { type: 'string', description: 'Diff URL' },
|
||||
patch_url: { type: 'string', description: 'Patch URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -210,15 +210,50 @@ export const searchReposV2Tool: ToolConfig<SearchReposParams, any> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'Repository ID' },
|
||||
full_name: { type: 'string', description: 'Full name' },
|
||||
description: { type: 'string', description: 'Description', optional: true },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
stargazers_count: { type: 'number', description: 'Stars' },
|
||||
forks_count: { type: 'number', description: 'Forks' },
|
||||
open_issues_count: { type: 'number', description: 'Open issues' },
|
||||
language: { type: 'string', description: 'Language', optional: true },
|
||||
topics: { type: 'array', description: 'Topics' },
|
||||
owner: { type: 'object', description: 'Owner' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
name: { type: 'string', description: 'Repository name' },
|
||||
full_name: { type: 'string', description: 'Full name (owner/repo)' },
|
||||
private: { type: 'boolean', description: 'Whether repository is private' },
|
||||
description: { type: 'string', description: 'Repository description', optional: true },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
fork: { type: 'boolean', description: 'Whether this is a fork' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
pushed_at: { type: 'string', description: 'Last push timestamp', optional: true },
|
||||
size: { type: 'number', description: 'Repository size in KB' },
|
||||
stargazers_count: { type: 'number', description: 'Number of stars' },
|
||||
watchers_count: { type: 'number', description: 'Number of watchers' },
|
||||
forks_count: { type: 'number', description: 'Number of forks' },
|
||||
open_issues_count: { type: 'number', description: 'Number of open issues' },
|
||||
language: { type: 'string', description: 'Primary programming language', optional: true },
|
||||
default_branch: { type: 'string', description: 'Default branch name' },
|
||||
score: { type: 'number', description: 'Search relevance score' },
|
||||
topics: { type: 'array', description: 'Repository topics' },
|
||||
license: {
|
||||
type: 'object',
|
||||
description: 'License information',
|
||||
optional: true,
|
||||
properties: {
|
||||
key: { type: 'string', description: 'License key (e.g., mit)' },
|
||||
name: { type: 'string', description: 'License name' },
|
||||
spdx_id: { type: 'string', description: 'SPDX identifier' },
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Repository owner',
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -181,11 +181,21 @@ export const searchUsersV2Tool: ToolConfig<SearchUsersParams, any> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
login: { type: 'string', description: 'Username' },
|
||||
html_url: { type: 'string', description: 'Profile URL' },
|
||||
avatar_url: { type: 'string', description: 'Avatar URL' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
gravatar_id: { type: 'string', description: 'Gravatar ID' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
followers_url: { type: 'string', description: 'Followers API URL' },
|
||||
following_url: { type: 'string', description: 'Following API URL' },
|
||||
gists_url: { type: 'string', description: 'Gists API URL' },
|
||||
starred_url: { type: 'string', description: 'Starred API URL' },
|
||||
repos_url: { type: 'string', description: 'Repos API URL' },
|
||||
organizations_url: { type: 'string', description: 'Organizations API URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'Is site admin' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
score: { type: 'number', description: 'Search relevance score' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -155,10 +155,39 @@ export const updateGistV2Tool: ToolConfig<UpdateGistParams, any> = {
|
||||
|
||||
outputs: {
|
||||
id: { type: 'string', description: 'Gist ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
description: { type: 'string', description: 'Description', optional: true },
|
||||
public: { type: 'boolean', description: 'Is public' },
|
||||
updated_at: { type: 'string', description: 'Update date' },
|
||||
files: { type: 'object', description: 'Current files' },
|
||||
forks_url: { type: 'string', description: 'Forks API URL' },
|
||||
commits_url: { type: 'string', description: 'Commits API URL' },
|
||||
git_pull_url: { type: 'string', description: 'Git pull URL' },
|
||||
git_push_url: { type: 'string', description: 'Git push URL' },
|
||||
description: { type: 'string', description: 'Gist description', optional: true },
|
||||
public: { type: 'boolean', description: 'Whether gist is public' },
|
||||
truncated: { type: 'boolean', description: 'Whether files are truncated' },
|
||||
comments: { type: 'number', description: 'Number of comments' },
|
||||
comments_url: { type: 'string', description: 'Comments API URL' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
files: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Files in the gist (object with filenames as keys, each containing filename, type, language, raw_url, size, truncated, content)',
|
||||
},
|
||||
owner: {
|
||||
type: 'object',
|
||||
description: 'Gist owner',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -174,13 +174,35 @@ export const updateMilestoneV2Tool: ToolConfig<UpdateMilestoneParams, any> = {
|
||||
},
|
||||
|
||||
outputs: {
|
||||
id: { type: 'number', description: 'Milestone ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
number: { type: 'number', description: 'Milestone number' },
|
||||
title: { type: 'string', description: 'Title' },
|
||||
description: { type: 'string', description: 'Description', optional: true },
|
||||
state: { type: 'string', description: 'State' },
|
||||
html_url: { type: 'string', description: 'Web URL' },
|
||||
due_on: { type: 'string', description: 'Due date', optional: true },
|
||||
open_issues: { type: 'number', description: 'Open issues' },
|
||||
closed_issues: { type: 'number', description: 'Closed issues' },
|
||||
title: { type: 'string', description: 'Milestone title' },
|
||||
description: { type: 'string', description: 'Milestone description', optional: true },
|
||||
state: { type: 'string', description: 'State (open or closed)' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'GitHub web URL' },
|
||||
labels_url: { type: 'string', description: 'Labels API URL' },
|
||||
due_on: { type: 'string', description: 'Due date (ISO 8601)', optional: true },
|
||||
open_issues: { type: 'number', description: 'Number of open issues' },
|
||||
closed_issues: { type: 'number', description: 'Number of closed issues' },
|
||||
created_at: { type: 'string', description: 'Creation timestamp' },
|
||||
updated_at: { type: 'string', description: 'Last update timestamp' },
|
||||
closed_at: { type: 'string', description: 'Close timestamp', optional: true },
|
||||
creator: {
|
||||
type: 'object',
|
||||
description: 'Milestone creator',
|
||||
optional: true,
|
||||
properties: {
|
||||
login: { type: 'string', description: 'Username' },
|
||||
id: { type: 'number', description: 'User ID' },
|
||||
node_id: { type: 'string', description: 'GraphQL node ID' },
|
||||
avatar_url: { type: 'string', description: 'Avatar image URL' },
|
||||
url: { type: 'string', description: 'API URL' },
|
||||
html_url: { type: 'string', description: 'Profile page URL' },
|
||||
type: { type: 'string', description: 'User or Organization' },
|
||||
site_admin: { type: 'boolean', description: 'GitHub staff indicator' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -97,6 +97,7 @@ export const copyTool: ToolConfig<GoogleDriveCopyParams, GoogleDriveCopyResponse
|
||||
description: 'The copied file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID of the copy' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
webViewLink: { type: 'string', description: 'URL to view in browser' },
|
||||
|
||||
@@ -134,9 +134,9 @@ export const createFolderTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUplo
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive folder ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'Folder name' },
|
||||
mimeType: { type: 'string', description: 'MIME type (application/vnd.google-apps.folder)' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
description: { type: 'string', description: 'Folder description' },
|
||||
// Ownership & Sharing
|
||||
owners: { type: 'json', description: 'List of folder owners' },
|
||||
|
||||
@@ -239,9 +239,9 @@ export const downloadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveDownload
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
description: { type: 'string', description: 'File description' },
|
||||
originalFilename: { type: 'string', description: 'Original uploaded filename' },
|
||||
fullFileExtension: { type: 'string', description: 'Full file extension' },
|
||||
|
||||
@@ -212,9 +212,9 @@ export const getContentTool: ToolConfig<GoogleDriveToolParams, GoogleDriveGetCon
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
description: { type: 'string', description: 'File description' },
|
||||
originalFilename: { type: 'string', description: 'Original uploaded filename' },
|
||||
fullFileExtension: { type: 'string', description: 'Full file extension' },
|
||||
|
||||
@@ -72,6 +72,7 @@ export const getFileTool: ToolConfig<GoogleDriveGetFileParams, GoogleDriveGetFil
|
||||
description: 'The file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
description: { type: 'string', description: 'File description', optional: true },
|
||||
|
||||
@@ -123,9 +123,9 @@ export const listTool: ToolConfig<GoogleDriveToolParams, GoogleDriveListResponse
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
description: { type: 'string', description: 'File description' },
|
||||
originalFilename: { type: 'string', description: 'Original uploaded filename' },
|
||||
fullFileExtension: { type: 'string', description: 'Full file extension' },
|
||||
|
||||
@@ -8,6 +8,7 @@ interface GoogleDriveListPermissionsParams extends GoogleDriveToolParams {
|
||||
interface GoogleDriveListPermissionsResponse extends ToolResponse {
|
||||
output: {
|
||||
permissions: GoogleDrivePermission[]
|
||||
nextPageToken?: string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +49,7 @@ export const listPermissionsTool: ToolConfig<
|
||||
url.searchParams.append('supportsAllDrives', 'true')
|
||||
url.searchParams.append(
|
||||
'fields',
|
||||
'permissions(id,type,role,emailAddress,displayName,photoLink,domain,expirationTime,deleted,allowFileDiscovery,pendingOwner,permissionDetails)'
|
||||
'nextPageToken,permissions(id,type,role,emailAddress,displayName,photoLink,domain,expirationTime,deleted,allowFileDiscovery,pendingOwner,permissionDetails)'
|
||||
)
|
||||
return url.toString()
|
||||
},
|
||||
@@ -84,6 +85,7 @@ export const listPermissionsTool: ToolConfig<
|
||||
success: true,
|
||||
output: {
|
||||
permissions,
|
||||
nextPageToken: data.nextPageToken,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -120,5 +122,9 @@ export const listPermissionsTool: ToolConfig<
|
||||
},
|
||||
},
|
||||
},
|
||||
nextPageToken: {
|
||||
type: 'string',
|
||||
description: 'Token for fetching the next page of permissions',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ export const trashTool: ToolConfig<GoogleDriveTrashParams, GoogleDriveTrashRespo
|
||||
description: 'The trashed file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash (should be true)' },
|
||||
|
||||
@@ -274,6 +274,11 @@ export interface GoogleDriveFile {
|
||||
resourceKey?: string
|
||||
shortcutDetails?: GoogleDriveShortcutDetails
|
||||
linkShareMetadata?: GoogleDriveLinkShareMetadata
|
||||
hasAugmentedPermissions?: boolean
|
||||
inheritedPermissionsDisabled?: boolean
|
||||
downloadRestrictions?: {
|
||||
restrictedForReaders?: boolean
|
||||
}
|
||||
|
||||
// Revisions (fetched separately but included in response)
|
||||
revisions?: GoogleDriveRevision[]
|
||||
|
||||
@@ -76,6 +76,7 @@ export const untrashTool: ToolConfig<GoogleDriveUntrashParams, GoogleDriveUntras
|
||||
description: 'The restored file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
trashed: { type: 'boolean', description: 'Whether file is in trash (should be false)' },
|
||||
|
||||
@@ -127,6 +127,7 @@ export const updateTool: ToolConfig<GoogleDriveUpdateParams, GoogleDriveUpdateRe
|
||||
description: 'The updated file metadata',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
description: { type: 'string', description: 'File description', optional: true },
|
||||
|
||||
@@ -263,9 +263,9 @@ export const uploadTool: ToolConfig<GoogleDriveToolParams, GoogleDriveUploadResp
|
||||
properties: {
|
||||
// Basic Info
|
||||
id: { type: 'string', description: 'Google Drive file ID' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
kind: { type: 'string', description: 'Resource type identifier' },
|
||||
description: { type: 'string', description: 'File description' },
|
||||
originalFilename: { type: 'string', description: 'Original uploaded filename' },
|
||||
fullFileExtension: { type: 'string', description: 'Full file extension' },
|
||||
|
||||
@@ -73,6 +73,10 @@ export const ALL_FILE_FIELDS = [
|
||||
'resourceKey',
|
||||
'shortcutDetails',
|
||||
'linkShareMetadata',
|
||||
'labelInfo',
|
||||
'hasAugmentedPermissions',
|
||||
'inheritedPermissionsDisabled',
|
||||
'downloadRestrictions',
|
||||
].join(',')
|
||||
|
||||
// All revision fields from Google Drive API v3
|
||||
|
||||
@@ -105,14 +105,85 @@ export const batchUpdateTool: ToolConfig<
|
||||
},
|
||||
},
|
||||
writeControl: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'Write control information with revision IDs',
|
||||
optional: true,
|
||||
properties: {
|
||||
requiredRevisionId: {
|
||||
type: 'string',
|
||||
description: 'Required revision ID for conflict detection',
|
||||
},
|
||||
targetRevisionId: { type: 'string', description: 'Target revision ID' },
|
||||
},
|
||||
},
|
||||
form: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'The updated form (if includeFormInResponse was true)',
|
||||
optional: true,
|
||||
properties: {
|
||||
formId: { type: 'string', description: 'The form ID' },
|
||||
info: {
|
||||
type: 'object',
|
||||
description: 'Form info containing title and description',
|
||||
properties: {
|
||||
title: { type: 'string', description: 'The form title visible to responders' },
|
||||
description: { type: 'string', description: 'The form description' },
|
||||
documentTitle: { type: 'string', description: 'The document title visible in Drive' },
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
type: 'object',
|
||||
description: 'Form settings',
|
||||
properties: {
|
||||
quizSettings: {
|
||||
type: 'object',
|
||||
description: 'Quiz settings',
|
||||
properties: {
|
||||
isQuiz: { type: 'boolean', description: 'Whether the form is a quiz' },
|
||||
},
|
||||
},
|
||||
emailCollectionType: { type: 'string', description: 'Email collection type' },
|
||||
},
|
||||
},
|
||||
items: {
|
||||
type: 'array',
|
||||
description: 'The form items (questions, sections, etc.)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
itemId: { type: 'string', description: 'Item ID' },
|
||||
title: { type: 'string', description: 'Item title' },
|
||||
description: { type: 'string', description: 'Item description' },
|
||||
questionItem: { type: 'json', description: 'Question item configuration' },
|
||||
questionGroupItem: { type: 'json', description: 'Question group configuration' },
|
||||
pageBreakItem: { type: 'json', description: 'Page break configuration' },
|
||||
textItem: { type: 'json', description: 'Text item configuration' },
|
||||
imageItem: { type: 'json', description: 'Image item configuration' },
|
||||
videoItem: { type: 'json', description: 'Video item configuration' },
|
||||
},
|
||||
},
|
||||
},
|
||||
revisionId: { type: 'string', description: 'The revision ID of the form' },
|
||||
responderUri: { type: 'string', description: 'The URI to share with responders' },
|
||||
linkedSheetId: { type: 'string', description: 'The ID of the linked Google Sheet' },
|
||||
publishSettings: {
|
||||
type: 'object',
|
||||
description: 'Form publish settings',
|
||||
properties: {
|
||||
publishState: {
|
||||
type: 'object',
|
||||
description: 'Current publish state',
|
||||
properties: {
|
||||
isPublished: { type: 'boolean', description: 'Whether the form is published' },
|
||||
isAcceptingResponses: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the form is accepting responses',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -165,4 +165,43 @@ export const getResponsesTool: ToolConfig<GoogleFormsGetResponsesParams> = {
|
||||
} as unknown as Record<string, unknown>,
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
responses: {
|
||||
type: 'array',
|
||||
description: 'Array of form responses (when no responseId provided)',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
responseId: { type: 'string', description: 'Unique response ID' },
|
||||
createTime: { type: 'string', description: 'When the response was created' },
|
||||
lastSubmittedTime: {
|
||||
type: 'string',
|
||||
description: 'When the response was last submitted',
|
||||
},
|
||||
answers: {
|
||||
type: 'json',
|
||||
description: 'Map of question IDs to answer values',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
response: {
|
||||
type: 'object',
|
||||
description: 'Single form response (when responseId is provided)',
|
||||
properties: {
|
||||
responseId: { type: 'string', description: 'Unique response ID' },
|
||||
createTime: { type: 'string', description: 'When the response was created' },
|
||||
lastSubmittedTime: { type: 'string', description: 'When the response was last submitted' },
|
||||
answers: {
|
||||
type: 'json',
|
||||
description: 'Map of question IDs to answer values',
|
||||
},
|
||||
},
|
||||
},
|
||||
raw: {
|
||||
type: 'json',
|
||||
description: 'Raw API response data',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -351,21 +351,15 @@ export const createShapeTool: ToolConfig<CreateShapeParams, CreateShapeResponse>
|
||||
description: 'The type of shape that was created',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and page object ID',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
pageObjectId: {
|
||||
type: 'string',
|
||||
description: 'The page object ID where the shape was created',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -220,21 +220,15 @@ export const createTableTool: ToolConfig<CreateTableParams, CreateTableResponse>
|
||||
description: 'Number of columns in the table',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and page object ID',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
pageObjectId: {
|
||||
type: 'string',
|
||||
description: 'The page object ID where the table was created',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -124,17 +124,11 @@ export const deleteObjectTool: ToolConfig<DeleteObjectParams, DeleteObjectRespon
|
||||
description: 'The object ID that was deleted',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -145,21 +145,15 @@ export const duplicateObjectTool: ToolConfig<DuplicateObjectParams, DuplicateObj
|
||||
description: 'The object ID of the newly created duplicate',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and source object ID',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
sourceObjectId: {
|
||||
type: 'string',
|
||||
description: 'The object ID that was duplicated',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
description: 'The original object ID that was duplicated',
|
||||
},
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -126,26 +126,38 @@ export const getPageTool: ToolConfig<GetPageParams, GetPageResponse> = {
|
||||
description: 'The type of page (SLIDE, MASTER, LAYOUT, NOTES, NOTES_MASTER)',
|
||||
},
|
||||
pageElements: {
|
||||
type: 'json',
|
||||
type: 'array',
|
||||
description: 'Array of page elements (shapes, images, tables, etc.) on this page',
|
||||
items: {
|
||||
type: 'json',
|
||||
},
|
||||
},
|
||||
slideProperties: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'Properties specific to slides (layout, master, notes)',
|
||||
optional: true,
|
||||
properties: {
|
||||
layoutObjectId: {
|
||||
type: 'string',
|
||||
description: 'Object ID of the layout this slide is based on',
|
||||
},
|
||||
masterObjectId: {
|
||||
type: 'string',
|
||||
description: 'Object ID of the master this slide is based on',
|
||||
},
|
||||
notesPage: {
|
||||
type: 'json',
|
||||
description: 'The notes page associated with the slide',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -152,17 +152,11 @@ export const insertTextTool: ToolConfig<InsertTextParams, InsertTextResponse> =
|
||||
description: 'The text that was inserted',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -160,17 +160,11 @@ export const updateSlidesPositionTool: ToolConfig<
|
||||
description: 'The index where the slides were moved to',
|
||||
},
|
||||
metadata: {
|
||||
type: 'json',
|
||||
type: 'object',
|
||||
description: 'Operation metadata including presentation ID and URL',
|
||||
properties: {
|
||||
presentationId: {
|
||||
type: 'string',
|
||||
description: 'The presentation ID',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'URL to open the presentation',
|
||||
},
|
||||
presentationId: { type: 'string', description: 'The presentation ID' },
|
||||
url: { type: 'string', description: 'URL to the presentation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user