Compare commits

...

32 Commits

Author SHA1 Message Date
Vikhyath Mondreti
b09f683072 v0.5.63: ui and performance improvements, more google tools 2026-01-18 15:22:42 -08:00
Vikhyath Mondreti
1dbf92db3f fix(api): tool input parsing into table from agent output (#2879)
* fix(api): transformTable to map agent output to table subblock format

* fix api

* add test
2026-01-18 14:43:02 -08:00
Waleed
3a923648cb feat(ux): more explicit verbiage on some dialog menus, google drive updates, advanved to additional fields, remove general settings store sync in favor of tanstack (#2875)
* fix(verbiage): more explicit verbiage on some dialog menus, google drive updates, advanved to additional fields, remove general settings store sync in favor of tanstack

* updated docs

* nested tag dropdown, more well-defined nested outputs, keyboard nav for context menus, etc

* cleanup

* allow cannonical toggle even if depends on not satisfied

* remove smooth scroll in tag drop

* fix selection

* fix

---------

Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
2026-01-18 13:40:59 -08:00
Vikhyath Mondreti
a8bb0db660 v0.5.62: webhook bug fixes, seeding default subblock values, block selection fixes 2026-01-16 20:27:06 -08:00
Waleed
af82820a28 v0.5.61: webhook improvements, workflow controls, react query for deployment status, chat fixes, reducto and pulse OCR, linear fixes 2026-01-16 18:06:23 -08:00
Waleed
4372841797 v0.5.60: invitation flow improvements, chat fixes, a2a improvements, additional copilot actions 2026-01-15 00:02:18 -08:00
Waleed
5e8c843241 v0.5.59: a2a support, documentation 2026-01-13 13:21:21 -08:00
Waleed
7bf3d73ee6 v0.5.58: export folders, new tools, permissions groups enhancements 2026-01-13 00:56:59 -08:00
Vikhyath Mondreti
7ffc11a738 v0.5.57: subagents, context menu improvements, bug fixes 2026-01-11 11:38:40 -08:00
Waleed
be578e2ed7 v0.5.56: batch operations, access control and permission groups, billing fixes 2026-01-10 00:31:34 -08:00
Waleed
f415e5edc4 v0.5.55: polling groups, bedrock provider, devcontainer fixes, workflow preview enhancements 2026-01-08 23:36:56 -08:00
Waleed
13a6e6c3fa v0.5.54: seo, model blacklist, helm chart updates, fireflies integration, autoconnect improvements, billing fixes 2026-01-07 16:09:45 -08:00
Waleed
f5ab7f21ae v0.5.53: hotkey improvements, added redis fallback, fixes for workflow tool 2026-01-06 23:34:52 -08:00
Waleed
bfb6fffe38 v0.5.52: new port-based router block, combobox expression and variable support 2026-01-06 16:14:10 -08:00
Waleed
4fbec0a43f v0.5.51: triggers, kb, condition block improvements, supabase and grain integration updates 2026-01-06 14:26:46 -08:00
Waleed
585f5e365b v0.5.50: import improvements, ui upgrades, kb styling and performance improvements 2026-01-05 00:35:55 -08:00
Waleed
3792bdd252 v0.5.49: hitl improvements, new email styles, imap trigger, logs context menu (#2672)
* feat(logs-context-menu): consolidated logs utils and types, added logs record context menu (#2659)

* feat(email): welcome email; improvement(emails): ui/ux (#2658)

* feat(email): welcome email; improvement(emails): ui/ux

* improvement(emails): links, accounts, preview

* refactor(emails): file structure and wrapper components

* added envvar for personal emails sent, added isHosted gate

* fixed failing tests, added env mock

* fix: removed comment

---------

Co-authored-by: waleed <walif6@gmail.com>

* fix(logging): hitl + trigger dev crash protection (#2664)

* hitl gaps

* deal with trigger worker crashes

* cleanup import strcuture

* feat(imap): added support for imap trigger (#2663)

* feat(tools): added support for imap trigger

* feat(imap): added parity, tested

* ack PR comments

* final cleanup

* feat(i18n): update translations (#2665)

Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>

* fix(grain): updated grain trigger to auto-establish trigger (#2666)

Co-authored-by: aadamgough <adam@sim.ai>

* feat(admin): routes to manage deployments (#2667)

* feat(admin): routes to manage deployments

* fix naming fo deployed by

* feat(time-picker): added timepicker emcn component, added to playground, added searchable prop for dropdown, added more timezones for schedule, updated license and notice date (#2668)

* feat(time-picker): added timepicker emcn component, added to playground, added searchable prop for dropdown, added more timezones for schedule, updated license and notice date

* removed unused params, cleaned up redundant utils

* improvement(invite): aligned styling (#2669)

* improvement(invite): aligned with rest of app

* fix(invite): error handling

* fix: addressed comments

---------

Co-authored-by: Emir Karabeg <78010029+emir-karabeg@users.noreply.github.com>
Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: waleedlatif1 <waleedlatif1@users.noreply.github.com>
Co-authored-by: Adam Gough <77861281+aadamgough@users.noreply.github.com>
Co-authored-by: aadamgough <adam@sim.ai>
2026-01-03 13:19:18 -08:00
Waleed
eb5d1f3e5b v0.5.48: copy-paste workflow blocks, docs updates, mcp tool fixes 2025-12-31 18:00:04 -08:00
Waleed
54ab82c8dd v0.5.47: deploy workflow as mcp, kb chunks tokenizer, UI improvements, jira service management tools 2025-12-30 23:18:58 -08:00
Waleed
f895bf469b v0.5.46: build improvements, greptile, light mode improvements 2025-12-29 02:17:52 -08:00
Waleed
dd3209af06 v0.5.45: light mode fixes, realtime usage indicator, docker build improvements 2025-12-27 19:57:42 -08:00
Waleed
b6ba3b50a7 v0.5.44: keyboard shortcuts, autolayout, light mode, byok, testing improvements 2025-12-26 21:25:19 -08:00
Waleed
b304233062 v0.5.43: export logs, circleback, grain, vertex, code hygiene, schedule improvements 2025-12-23 19:19:18 -08:00
Vikhyath Mondreti
57e4b49bd6 v0.5.42: fix memory migration 2025-12-23 01:24:54 -08:00
Vikhyath Mondreti
e12dd204ed v0.5.41: memory fixes, copilot improvements, knowledgebase improvements, LLM providers standardization 2025-12-23 00:15:18 -08:00
Vikhyath Mondreti
3d9d9cbc54 v0.5.40: supabase ops to allow non-public schemas, jira uuid 2025-12-21 22:28:05 -08:00
Waleed
0f4ec962ad v0.5.39: notion, workflow variables fixes 2025-12-20 20:44:00 -08:00
Waleed
4827866f9a v0.5.38: snap to grid, copilot ux improvements, billing line items 2025-12-20 17:24:38 -08:00
Waleed
3e697d9ed9 v0.5.37: redaction utils consolidation, logs updates, autoconnect improvements, additional kb tag types 2025-12-19 22:31:55 -08:00
Martin Yankov
4431a1a484 fix(helm): add custom egress rules to realtime network policy (#2481)
The realtime service network policy was missing the custom egress rules section
that allows configuration of additional egress rules via values.yaml. This caused
the realtime pods to be unable to connect to external databases (e.g., PostgreSQL
on port 5432) when using external database configurations.

The app network policy already had this section, but the realtime network policy
was missing it, creating an inconsistency and preventing the realtime service
from accessing external databases configured via networkPolicy.egress values.

This fix adds the same custom egress rules template section to the realtime
network policy, matching the app network policy behavior and allowing users to
configure database connectivity via values.yaml.
2025-12-19 18:59:08 -08:00
Waleed
4d1a9a3f22 v0.5.36: hitl improvements, opengraph, slack fixes, one-click unsubscribe, auth checks, new db indexes 2025-12-19 01:27:49 -08:00
Vikhyath Mondreti
eb07a080fb v0.5.35: helm updates, copilot improvements, 404 for docs, salesforce fixes, subflow resize clamping 2025-12-18 16:23:19 -08:00
85 changed files with 4064 additions and 1299 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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`

View File

@@ -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`

View File

@@ -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 |

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>

View File

@@ -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',

View File

@@ -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

View File

@@ -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[]
}
/**

View File

@@ -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>

View File

@@ -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' : ''}`}
/>

View File

@@ -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)

View File

@@ -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)

View File

@@ -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) => {

View File

@@ -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]
)

View File

@@ -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

View File

@@ -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]'>

View File

@@ -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.'
}

View File

@@ -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' },

View File

@@ -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' },
},

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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}

View File

@@ -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)
},

View File

@@ -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 }

View File

@@ -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',

View File

@@ -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',
],
},

View File

@@ -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)
}

View File

@@ -1,2 +0,0 @@
export { useGeneralStore } from './store'
export type { General, GeneralStore, UserSettings } from './types'

View File

@@ -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' }
)
)

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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,
},
},
},
},
},
}

View File

@@ -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' },
},
},
},
}

View File

@@ -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' },
},
},
},
}

View File

@@ -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' },
},
},
},
}

View File

@@ -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' },
},
},
},
}

View File

@@ -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' },
},
},
},
},
}

View File

@@ -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' },
},
},
},
}

View File

@@ -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' },
},
},
},
}

View File

@@ -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' },
},
},
},
},
},
},

View File

@@ -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' },
},
},
},
},
},

View File

@@ -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' },
},
},
},
},
},

View File

@@ -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' },
},
},
},
},
},

View File

@@ -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' },
},
},
},

View File

@@ -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' },
},
},
},
},
},
},
},
},
},

View File

@@ -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' },
},
},
},
},
},
},

View File

@@ -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' },
},
},
},
},
},

View File

@@ -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' },
},
},
},
},
},

View File

@@ -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' },
},
},
},

View File

@@ -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' },
},
},
},
}

View File

@@ -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' },
},
},
},
}

View File

@@ -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' },

View File

@@ -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' },

View File

@@ -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' },

View File

@@ -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' },

View File

@@ -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 },

View File

@@ -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' },

View File

@@ -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',
},
},
}

View File

@@ -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)' },

View File

@@ -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[]

View File

@@ -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)' },

View File

@@ -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 },

View File

@@ -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' },

View File

@@ -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

View File

@@ -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',
},
},
},
},
},
},
},
},
}

View File

@@ -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',
},
},
}

View File

@@ -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' },
},
},
},

View File

@@ -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' },
},
},
},

View File

@@ -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' },
},
},
},

View File

@@ -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' },
},
},
},

View File

@@ -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' },
},
},
},

View File

@@ -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' },
},
},
},

View File

@@ -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' },
},
},
},

View File

@@ -1,5 +1,6 @@
import type { RequestParams, RequestResponse } from '@/tools/http/types'
import { getDefaultHeaders, processUrl, transformTable } from '@/tools/http/utils'
import { getDefaultHeaders, processUrl } from '@/tools/http/utils'
import { transformTable } from '@/tools/shared/table'
import type { ToolConfig } from '@/tools/types'
export const requestTool: ToolConfig<RequestParams, RequestResponse> = {

View File

@@ -1,6 +1,7 @@
import { createLogger } from '@sim/logger'
import { isTest } from '@/lib/core/config/feature-flags'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { transformTable } from '@/tools/shared/table'
import type { TableRow } from '@/tools/types'
const logger = createLogger('HTTPRequestUtils')
@@ -119,28 +120,3 @@ export const shouldUseProxy = (url: string): boolean => {
return false
}
}
/**
* Transforms a table from the store format to a key-value object
* Local copy of the function to break circular dependencies
* @param table Array of table rows from the store
* @returns Record of key-value pairs
*/
export const transformTable = (table: TableRow[] | null): Record<string, any> => {
if (!table) return {}
return table.reduce(
(acc, row) => {
if (row.cells?.Key && row.cells?.Value !== undefined) {
// Extract the Value cell as is - it should already be properly resolved
// by the InputResolver based on variable type (number, string, boolean etc.)
const value = row.cells.Value
// Store the correctly typed value in the result object
acc[row.cells.Key] = value
}
return acc
},
{} as Record<string, any>
)
}

View File

@@ -1,6 +1,6 @@
import type { KnowledgeCreateDocumentResponse } from '@/tools/knowledge/types'
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/params'
import { enrichKBTagsSchema } from '@/tools/schema-enrichers'
import { formatDocumentTagsForAPI, parseDocumentTags } from '@/tools/shared/tags'
import type { ToolConfig } from '@/tools/types'
export const knowledgeCreateDocumentTool: ToolConfig<any, KnowledgeCreateDocumentResponse> = {

View File

@@ -1,6 +1,6 @@
import type { KnowledgeSearchResponse } from '@/tools/knowledge/types'
import { parseTagFilters } from '@/tools/params'
import { enrichKBTagFiltersSchema } from '@/tools/schema-enrichers'
import { parseTagFilters } from '@/tools/shared/tags'
import type { ToolConfig } from '@/tools/types'
export const knowledgeSearchTool: ToolConfig<any, KnowledgeSearchResponse> = {

View File

@@ -1,11 +1,11 @@
import { createLogger } from '@sim/logger'
import type { StructuredFilter } from '@/lib/knowledge/types'
import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format'
import {
evaluateSubBlockCondition,
type SubBlockCondition,
} from '@/lib/workflows/subblocks/visibility'
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
import { isEmptyTagValue } from '@/tools/shared/tags'
import type { ParameterVisibility, ToolConfig } from '@/tools/types'
import { getTool } from '@/tools/utils'
@@ -23,194 +23,6 @@ export function isNonEmpty(value: unknown): boolean {
// Tag/Value Parsing Utilities
// ============================================================================
/**
* Document tag entry format used in create_document tool
*/
export interface DocumentTagEntry {
tagName: string
value: string
}
/**
* Tag filter entry format used in search tool
*/
export interface TagFilterEntry {
tagName: string
tagSlot?: string
tagValue: string | number | boolean
fieldType?: string
operator?: string
valueTo?: string | number
}
/**
* Checks if a tag value is effectively empty (unfilled/default entry)
*/
function isEmptyTagEntry(entry: Record<string, unknown>): boolean {
if (!entry.tagName || (typeof entry.tagName === 'string' && entry.tagName.trim() === '')) {
return true
}
return false
}
/**
* Checks if a tag-based value is effectively empty (only contains default/unfilled entries).
* Works for both documentTags and tagFilters parameters in various formats.
*
* @param value - The tag value to check (can be JSON string, array, or object)
* @returns true if the value is empty or only contains unfilled entries
*/
export function isEmptyTagValue(value: unknown): boolean {
if (!value) return true
// Handle JSON string format
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
if (!Array.isArray(parsed)) return false
if (parsed.length === 0) return true
return parsed.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
} catch {
return false
}
}
// Handle array format directly
if (Array.isArray(value)) {
if (value.length === 0) return true
return value.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
}
// Handle object format (LLM format: { "Category": "foo", "Priority": 5 })
if (typeof value === 'object' && value !== null) {
const entries = Object.entries(value)
if (entries.length === 0) return true
return entries.every(([, val]) => val === undefined || val === null || val === '')
}
return false
}
/**
* Filters valid document tags from an array, removing empty entries
*/
function filterValidDocumentTags(tags: unknown[]): DocumentTagEntry[] {
return tags
.filter((entry): entry is Record<string, unknown> => {
if (typeof entry !== 'object' || entry === null) return false
const e = entry as Record<string, unknown>
if (!e.tagName || (typeof e.tagName === 'string' && e.tagName.trim() === '')) return false
if (e.value === undefined || e.value === null || e.value === '') return false
return true
})
.map((entry) => ({
tagName: String(entry.tagName),
value: String(entry.value),
}))
}
/**
* Parses document tags from various formats into a normalized array format.
* Used by create_document tool to handle tags from both UI and LLM sources.
*
* @param value - Document tags in object, array, or JSON string format
* @returns Normalized array of document tag entries, or empty array if invalid
*/
export function parseDocumentTags(value: unknown): DocumentTagEntry[] {
if (!value) return []
// Handle object format from LLM: { "Category": "foo", "Priority": 5 }
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
return Object.entries(value)
.filter(([tagName, tagValue]) => {
if (!tagName || tagName.trim() === '') return false
if (tagValue === undefined || tagValue === null || tagValue === '') return false
return true
})
.map(([tagName, tagValue]) => ({
tagName,
value: String(tagValue),
}))
}
// Handle JSON string format from UI
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
if (Array.isArray(parsed)) {
return filterValidDocumentTags(parsed)
}
} catch {
// Invalid JSON, return empty
}
return []
}
// Handle array format directly
if (Array.isArray(value)) {
return filterValidDocumentTags(value)
}
return []
}
/**
* Parses tag filters from various formats into a normalized StructuredFilter array.
* Used by search tool to handle tag filters from both UI and LLM sources.
*
* @param value - Tag filters in array or JSON string format
* @returns Normalized array of structured filters, or empty array if invalid
*/
export function parseTagFilters(value: unknown): StructuredFilter[] {
if (!value) return []
let tagFilters = value
// Handle JSON string format
if (typeof tagFilters === 'string') {
try {
tagFilters = JSON.parse(tagFilters)
} catch {
return []
}
}
// Must be an array at this point
if (!Array.isArray(tagFilters)) return []
return tagFilters
.filter((filter): filter is Record<string, unknown> => {
if (typeof filter !== 'object' || filter === null) return false
const f = filter as Record<string, unknown>
if (!f.tagName || (typeof f.tagName === 'string' && f.tagName.trim() === '')) return false
if (f.fieldType === 'boolean') {
return f.tagValue !== undefined
}
if (f.tagValue === undefined || f.tagValue === null) return false
if (typeof f.tagValue === 'string' && f.tagValue.trim().length === 0) return false
return true
})
.map((filter) => ({
tagName: filter.tagName as string,
tagSlot: (filter.tagSlot as string) || '',
fieldType: (filter.fieldType as string) || 'text',
operator: (filter.operator as string) || 'eq',
value: filter.tagValue as string | number | boolean,
valueTo: filter.valueTo as string | number | undefined,
}))
}
/**
* Converts parsed document tags to the format expected by the create document API.
* Returns the documentTagsData JSON string if there are valid tags.
*/
export function formatDocumentTagsForAPI(tags: DocumentTagEntry[]): { documentTagsData?: string } {
if (tags.length === 0) return {}
return {
documentTagsData: JSON.stringify(tags),
}
}
export interface Option {
label: string
value: string

View File

@@ -0,0 +1,38 @@
import type { TableRow } from '@/tools/types'
/**
* Transforms a table from the store format to a key-value object.
*/
export const transformTable = (
table: TableRow[] | Record<string, any> | string | null
): Record<string, any> => {
if (!table) return {}
if (typeof table === 'string') {
try {
const parsed = JSON.parse(table) as TableRow[] | Record<string, any>
return transformTable(parsed)
} catch {
return {}
}
}
if (Array.isArray(table)) {
return table.reduce(
(acc, row) => {
if (row.cells?.Key && row.cells?.Value !== undefined) {
const value = row.cells.Value
acc[row.cells.Key] = value
}
return acc
},
{} as Record<string, any>
)
}
if (typeof table === 'object') {
return table
}
return {}
}

View File

@@ -0,0 +1,168 @@
import type { StructuredFilter } from '@/lib/knowledge/types'
/**
* Document tag entry format used in create_document tool.
*/
export interface DocumentTagEntry {
tagName: string
value: string
}
/**
* Tag filter entry format used in search tool.
*/
export interface TagFilterEntry {
tagName: string
tagSlot?: string
tagValue: string | number | boolean
fieldType?: string
operator?: string
valueTo?: string | number
}
/**
* Checks if a tag value is effectively empty (unfilled/default entry).
*/
function isEmptyTagEntry(entry: Record<string, unknown>): boolean {
if (!entry.tagName || (typeof entry.tagName === 'string' && entry.tagName.trim() === '')) {
return true
}
return false
}
/**
* Checks if a tag-based value is effectively empty (only contains default/unfilled entries).
*/
export function isEmptyTagValue(value: unknown): boolean {
if (!value) return true
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
if (!Array.isArray(parsed)) return false
if (parsed.length === 0) return true
return parsed.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
} catch {
return false
}
}
if (Array.isArray(value)) {
if (value.length === 0) return true
return value.every((entry: Record<string, unknown>) => isEmptyTagEntry(entry))
}
if (typeof value === 'object' && value !== null) {
const entries = Object.entries(value)
if (entries.length === 0) return true
return entries.every(([, val]) => val === undefined || val === null || val === '')
}
return false
}
/**
* Filters valid document tags from an array, removing empty entries.
*/
function filterValidDocumentTags(tags: unknown[]): DocumentTagEntry[] {
return tags
.filter((entry): entry is Record<string, unknown> => {
if (typeof entry !== 'object' || entry === null) return false
const e = entry as Record<string, unknown>
if (!e.tagName || (typeof e.tagName === 'string' && e.tagName.trim() === '')) return false
if (e.value === undefined || e.value === null || e.value === '') return false
return true
})
.map((entry) => ({
tagName: String(entry.tagName),
value: String(entry.value),
}))
}
/**
* Parses document tags from various formats into a normalized array format.
*/
export function parseDocumentTags(value: unknown): DocumentTagEntry[] {
if (!value) return []
if (typeof value === 'object' && !Array.isArray(value) && value !== null) {
return Object.entries(value)
.filter(([tagName, tagValue]) => {
if (!tagName || tagName.trim() === '') return false
if (tagValue === undefined || tagValue === null || tagValue === '') return false
return true
})
.map(([tagName, tagValue]) => ({
tagName,
value: String(tagValue),
}))
}
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value)
if (Array.isArray(parsed)) {
return filterValidDocumentTags(parsed)
}
} catch {
return []
}
return []
}
if (Array.isArray(value)) {
return filterValidDocumentTags(value)
}
return []
}
/**
* Parses tag filters from various formats into a normalized StructuredFilter array.
*/
export function parseTagFilters(value: unknown): StructuredFilter[] {
if (!value) return []
let tagFilters = value
if (typeof tagFilters === 'string') {
try {
tagFilters = JSON.parse(tagFilters)
} catch {
return []
}
}
if (!Array.isArray(tagFilters)) return []
return tagFilters
.filter((filter): filter is Record<string, unknown> => {
if (typeof filter !== 'object' || filter === null) return false
const f = filter as Record<string, unknown>
if (!f.tagName || (typeof f.tagName === 'string' && f.tagName.trim() === '')) return false
if (f.fieldType === 'boolean') {
return f.tagValue !== undefined
}
if (f.tagValue === undefined || f.tagValue === null) return false
if (typeof f.tagValue === 'string' && f.tagValue.trim().length === 0) return false
return true
})
.map((filter) => ({
tagName: filter.tagName as string,
tagSlot: (filter.tagSlot as string) || '',
fieldType: (filter.fieldType as string) || 'text',
operator: (filter.operator as string) || 'eq',
value: filter.tagValue as string | number | boolean,
valueTo: filter.valueTo as string | number | undefined,
}))
}
/**
* Converts parsed document tags to the format expected by the create document API.
*/
export function formatDocumentTagsForAPI(tags: DocumentTagEntry[]): { documentTagsData?: string } {
if (tags.length === 0) return {}
return {
documentTagsData: JSON.stringify(tags),
}
}

View File

@@ -1,5 +1,6 @@
import { createMockFetch, loggerMock } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { transformTable } from '@/tools/shared/table'
import type { ToolConfig } from '@/tools/types'
import {
createCustomToolRequestBody,
@@ -7,7 +8,6 @@ import {
executeRequest,
formatRequestParams,
getClientEnvVars,
transformTable,
validateRequiredParametersAfterMerge,
} from '@/tools/utils'
@@ -91,6 +91,25 @@ describe('transformTable', () => {
enabled: false,
})
})
it.concurrent('should parse JSON string inputs and transform rows', () => {
const table = [
{ id: '1', cells: { Key: 'city', Value: 'SF' } },
{ id: '2', cells: { Key: 'temp', Value: 64 } },
]
const result = transformTable(JSON.stringify(table))
expect(result).toEqual({
city: 'SF',
temp: 64,
})
})
it.concurrent('should parse JSON string object inputs', () => {
const result = transformTable(JSON.stringify({ a: 1, b: 'two' }))
expect(result).toEqual({ a: 1, b: 'two' })
})
})
describe('formatRequestParams', () => {

View File

@@ -5,7 +5,7 @@ import { useCustomToolsStore } from '@/stores/custom-tools'
import { useEnvironmentStore } from '@/stores/settings/environment'
import { extractErrorMessage } from '@/tools/error-extractors'
import { tools } from '@/tools/registry'
import type { TableRow, ToolConfig, ToolResponse } from '@/tools/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
const logger = createLogger('ToolsUtils')
@@ -70,30 +70,6 @@ export function resolveToolId(toolName: string): string {
return toolName
}
/**
* Transforms a table from the store format to a key-value object
* @param table Array of table rows from the store
* @returns Record of key-value pairs
*/
export const transformTable = (table: TableRow[] | null): Record<string, any> => {
if (!table) return {}
return table.reduce(
(acc, row) => {
if (row.cells?.Key && row.cells?.Value !== undefined) {
// Extract the Value cell as is - it should already be properly resolved
// by the InputResolver based on variable type (number, string, boolean etc.)
const value = row.cells.Value
// Store the correctly typed value in the result object
acc[row.cells.Key] = value
}
return acc
},
{} as Record<string, any>
)
}
interface RequestParams {
url: string
method: string