Compare commits

..

1 Commits

33 changed files with 208 additions and 1870 deletions

View File

@@ -90,20 +90,14 @@ Ein Jira-Issue erstellen
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Ja | Ihre Jira-Domain \(z.B. ihrfirma.atlassian.net\) |
| `domain` | string | Ja | Ihre Jira-Domain (z.B. ihrfirma.atlassian.net) |
| `projectId` | string | Ja | Projekt-ID für das Issue |
| `summary` | string | Ja | Zusammenfassung für das Issue |
| `description` | string | Nein | Beschreibung für das Issue |
| `priority` | string | Nein | Prioritäts-ID oder -Name für das Issue \(z.B. "10000" oder "High"\) |
| `assignee` | string | Nein | Account-ID des Bearbeiters für das Issue |
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz. Wenn nicht angegeben, wird sie über die Domain abgerufen. |
| `issueType` | string | Ja | Typ des zu erstellenden Issues \(z.B. Task, Story\) |
| `labels` | array | Nein | Labels für das Issue \(Array von Label-Namen\) |
| `duedate` | string | Nein | Fälligkeitsdatum für das Issue \(Format: YYYY-MM-DD\) |
| `reporter` | string | Nein | Account-ID des Melders für das Issue |
| `environment` | string | Nein | Umgebungsinformationen für das Issue |
| `customFieldId` | string | Nein | Benutzerdefinierte Feld-ID \(z.B. customfield_10001\) |
| `customFieldValue` | string | Nein | Wert für das benutzerdefinierte Feld |
| `priority` | string | Nein | Priorität für das Issue |
| `assignee` | string | Nein | Bearbeiter für das Issue |
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz. Wenn nicht angegeben, wird sie anhand der Domain abgerufen. |
| `issueType` | string | Ja | Art des zu erstellenden Issues (z.B. Task, Story) |
#### Ausgabe
@@ -113,7 +107,6 @@ Ein Jira-Issue erstellen
| `issueKey` | string | Erstellter Issue-Key \(z.B. PROJ-123\) |
| `summary` | string | Issue-Zusammenfassung |
| `url` | string | URL zum erstellten Issue |
| `assigneeId` | string | Account-ID des zugewiesenen Benutzers \(falls zugewiesen\) |
### `jira_bulk_read`
@@ -527,30 +520,6 @@ Einen Beobachter von einem Jira-Issue entfernen
| `issueKey` | string | Issue-Key |
| `watcherAccountId` | string | Account-ID des entfernten Beobachters |
### `jira_get_users`
Jira-Benutzer abrufen. Wenn eine Account-ID angegeben wird, wird ein einzelner Benutzer zurückgegeben. Andernfalls wird eine Liste aller Benutzer zurückgegeben.
#### Eingabe
| Parameter | Typ | Erforderlich | Beschreibung |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Ja | Ihre Jira-Domain \(z.B. ihrfirma.atlassian.net\) |
| `accountId` | string | Nein | Optionale Account-ID, um einen bestimmten Benutzer abzurufen. Wenn nicht angegeben, werden alle Benutzer zurückgegeben. |
| `startAt` | number | Nein | Der Index des ersten zurückzugebenden Benutzers \(für Paginierung, Standard: 0\) |
| `maxResults` | number | Nein | Maximale Anzahl der zurückzugebenden Benutzer \(Standard: 50\) |
| `cloudId` | string | Nein | Jira Cloud-ID für die Instanz. Wenn nicht angegeben, wird sie anhand der Domain abgerufen. |
#### Ausgabe
| Parameter | Typ | Beschreibung |
| --------- | ---- | ----------- |
| `ts` | string | Zeitstempel der Operation |
| `users` | json | Array von Benutzern mit accountId, displayName, emailAddress, active-Status und avatarUrls |
| `total` | number | Gesamtanzahl der zurückgegebenen Benutzer |
| `startAt` | number | Startindex für Paginierung |
| `maxResults` | number | Maximale Ergebnisse pro Seite |
## Hinweise
- Kategorie: `tools`

View File

@@ -97,16 +97,10 @@ Write a Jira issue
| `projectId` | string | Yes | Project ID for the issue |
| `summary` | string | Yes | Summary for the issue |
| `description` | string | No | Description for the issue |
| `priority` | string | No | Priority ID or name for the issue \(e.g., "10000" or "High"\) |
| `assignee` | string | No | Assignee account ID for the issue |
| `priority` | string | No | Priority for the issue |
| `assignee` | string | No | Assignee for the issue |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
| `issueType` | string | Yes | Type of issue to create \(e.g., Task, Story\) |
| `labels` | array | No | Labels for the issue \(array of label names\) |
| `duedate` | string | No | Due date for the issue \(format: YYYY-MM-DD\) |
| `reporter` | string | No | Reporter account ID for the issue |
| `environment` | string | No | Environment information for the issue |
| `customFieldId` | string | No | Custom field ID \(e.g., customfield_10001\) |
| `customFieldValue` | string | No | Value for the custom field |
#### Output
@@ -116,7 +110,6 @@ Write a Jira issue
| `issueKey` | string | Created issue key \(e.g., PROJ-123\) |
| `summary` | string | Issue summary |
| `url` | string | URL to the created issue |
| `assigneeId` | string | Account ID of the assigned user \(if assigned\) |
### `jira_bulk_read`
@@ -530,30 +523,6 @@ Remove a watcher from a Jira issue
| `issueKey` | string | Issue key |
| `watcherAccountId` | string | Removed watcher account ID |
### `jira_get_users`
Get Jira users. If an account ID is provided, returns a single user. Otherwise, returns a list of all users.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `accountId` | string | No | Optional account ID to get a specific user. If not provided, returns all users. |
| `startAt` | number | No | The index of the first user to return \(for pagination, default: 0\) |
| `maxResults` | number | No | Maximum number of users to return \(default: 50\) |
| `cloudId` | string | No | Jira Cloud ID for the instance. If not provided, it will be fetched using the domain. |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `users` | json | Array of users with accountId, displayName, emailAddress, active status, and avatarUrls |
| `total` | number | Total number of users returned |
| `startAt` | number | Pagination start index |
| `maxResults` | number | Maximum results per page |
## Notes

View File

@@ -89,31 +89,24 @@ Escribir una incidencia de Jira
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
| `projectId` | string | Sí | ID del proyecto para la incidencia |
| `summary` | string | Sí | Resumen de la incidencia |
| `description` | string | No | Descripción de la incidencia |
| `priority` | string | No | ID o nombre de prioridad para la incidencia \(p. ej., "10000" o "Alta"\) |
| `assignee` | string | No | ID de cuenta del asignado para la incidencia |
| `cloudId` | string | No | ID de Jira Cloud para la instancia. Si no se proporciona, se obtendrá usando el dominio. |
| `priority` | string | No | Prioridad de la incidencia |
| `assignee` | string | No | Asignado para la incidencia |
| `cloudId` | string | No | ID de Jira Cloud para la instancia. Si no se proporciona, se obtendrá utilizando el dominio. |
| `issueType` | string | Sí | Tipo de incidencia a crear \(p. ej., Tarea, Historia\) |
| `labels` | array | No | Etiquetas para la incidencia \(array de nombres de etiquetas\) |
| `duedate` | string | No | Fecha de vencimiento para la incidencia \(formato: AAAA-MM-DD\) |
| `reporter` | string | No | ID de cuenta del informador para la incidencia |
| `environment` | string | No | Información del entorno para la incidencia |
| `customFieldId` | string | No | ID del campo personalizado \(p. ej., customfield_10001\) |
| `customFieldValue` | string | No | Valor para el campo personalizado |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `ts` | string | Marca de tiempo de la operación |
| `issueKey` | string | Clave de la incidencia creada \(p. ej., PROJ-123\) |
| `issueKey` | string | Clave de la incidencia creada (p. ej., PROJ-123) |
| `summary` | string | Resumen de la incidencia |
| `url` | string | URL de la incidencia creada |
| `assigneeId` | string | ID de cuenta del usuario asignado \(si está asignado\) |
### `jira_bulk_read`
@@ -527,30 +520,6 @@ Eliminar un observador de una incidencia de Jira
| `issueKey` | string | Clave de incidencia |
| `watcherAccountId` | string | ID de cuenta del observador eliminado |
### `jira_get_users`
Obtener usuarios de Jira. Si se proporciona un ID de cuenta, devuelve un solo usuario. De lo contrario, devuelve una lista de todos los usuarios.
#### Entrada
| Parámetro | Tipo | Obligatorio | Descripción |
| --------- | ---- | ----------- | ----------- |
| `domain` | string | Sí | Tu dominio de Jira \(p. ej., tuempresa.atlassian.net\) |
| `accountId` | string | No | ID de cuenta opcional para obtener un usuario específico. Si no se proporciona, devuelve todos los usuarios. |
| `startAt` | number | No | El índice del primer usuario a devolver \(para paginación, predeterminado: 0\) |
| `maxResults` | number | No | Número máximo de usuarios a devolver \(predeterminado: 50\) |
| `cloudId` | string | No | ID de Jira Cloud para la instancia. Si no se proporciona, se obtendrá usando el dominio. |
#### Salida
| Parámetro | Tipo | Descripción |
| --------- | ---- | ----------- |
| `ts` | string | Marca de tiempo de la operación |
| `users` | json | Array de usuarios con accountId, displayName, emailAddress, estado activo y avatarUrls |
| `total` | number | Número total de usuarios devueltos |
| `startAt` | number | Índice de inicio de paginación |
| `maxResults` | number | Máximo de resultados por página |
## Notas
- Categoría: `tools`

View File

@@ -89,21 +89,15 @@ Rédiger une demande Jira
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `domain` | chaîne | Oui | Votre domaine Jira \(ex. : votreentreprise.atlassian.net\) |
| `projectId` | chaîne | Oui | ID du projet pour le ticket |
| `summary` | chaîne | Oui | Résumé du ticket |
| `description` | chaîne | Non | Description du ticket |
| `priority` | chaîne | Non | ID ou nom de la priorité du ticket \(ex. : "10000" ou "Haute"\) |
| `assignee` | chaîne | Non | ID de compte de l'assigné pour le ticket |
| `cloudId` | chaîne | Non | ID Cloud Jira pour l'instance. S'il n'est pas fourni, il sera récupéré à l'aide du domaine. |
| `issueType` | chaîne | Oui | Type de ticket à créer \(ex. : tâche, story\) |
| `labels` | tableau | Non | Étiquettes pour le ticket \(tableau de noms d'étiquettes\) |
| `duedate` | chaîne | Non | Date d'échéance du ticket \(format : AAAA-MM-JJ\) |
| `reporter` | chaîne | Non | ID de compte du rapporteur pour le ticket |
| `environment` | chaîne | Non | Informations d'environnement pour le ticket |
| `customFieldId` | chaîne | Non | ID du champ personnalisé \(ex. : customfield_10001\) |
| `customFieldValue` | chaîne | Non | Valeur pour le champ personnalisé |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Oui | Votre domaine Jira (ex. : votreentreprise.atlassian.net) |
| `projectId` | string | Oui | ID du projet pour la demande |
| `summary` | string | Oui | Résumé de la demande |
| `description` | string | Non | Description de la demande |
| `priority` | string | Non | Priorité de la demande |
| `assignee` | string | Non | Assigné de la demande |
| `cloudId` | string | Non | ID Jira Cloud pour l'instance. S'il n'est pas fourni, il sera récupéré à l'aide du domaine. |
| `issueType` | string | Oui | Type de demande à créer (ex. : Tâche, Story) |
#### Sortie
@@ -113,7 +107,6 @@ Rédiger une demande Jira
| `issueKey` | chaîne | Clé du ticket créé \(ex. : PROJ-123\) |
| `summary` | chaîne | Résumé du ticket |
| `url` | chaîne | URL vers le ticket créé |
| `assigneeId` | chaîne | ID de compte de l'utilisateur assigné \(si assigné\) |
### `jira_bulk_read`
@@ -527,31 +520,7 @@ Supprimer un observateur d'un ticket Jira
| `issueKey` | string | Clé du ticket |
| `watcherAccountId` | string | ID du compte observateur supprimé |
### `jira_get_users`
Récupère les utilisateurs Jira. Si un ID de compte est fourni, renvoie un seul utilisateur. Sinon, renvoie une liste de tous les utilisateurs.
#### Entrée
| Paramètre | Type | Obligatoire | Description |
| --------- | ---- | ----------- | ----------- |
| `domain` | chaîne | Oui | Votre domaine Jira \(ex. : votreentreprise.atlassian.net\) |
| `accountId` | chaîne | Non | ID de compte optionnel pour obtenir un utilisateur spécifique. S'il n'est pas fourni, renvoie tous les utilisateurs. |
| `startAt` | nombre | Non | L'index du premier utilisateur à renvoyer \(pour la pagination, par défaut : 0\) |
| `maxResults` | nombre | Non | Nombre maximum d'utilisateurs à renvoyer \(par défaut : 50\) |
| `cloudId` | chaîne | Non | ID Cloud Jira pour l'instance. S'il n'est pas fourni, il sera récupéré à l'aide du domaine. |
#### Sortie
| Paramètre | Type | Description |
| --------- | ---- | ----------- |
| `ts` | chaîne | Horodatage de l'opération |
| `users` | json | Tableau d'utilisateurs avec accountId, displayName, emailAddress, statut actif et avatarUrls |
| `total` | nombre | Nombre total d'utilisateurs renvoyés |
| `startAt` | nombre | Index de début de pagination |
| `maxResults` | nombre | Nombre maximum de résultats par page |
## Remarques
## Notes
- Catégorie : `tools`
- Type : `jira`

View File

@@ -94,16 +94,10 @@ Jira課題を作成する
| `projectId` | string | はい | 課題のプロジェクトID |
| `summary` | string | はい | 課題の要約 |
| `description` | string | いいえ | 課題の説明 |
| `priority` | string | いいえ | 課題の優先度IDまたは名前「10000」または「高」 |
| `assignee` | string | いいえ | 課題の担当者アカウントID |
| `priority` | string | いいえ | 課題の優先度 |
| `assignee` | string | いいえ | 課題の担当者 |
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID。提供されない場合、ドメインを使用して取得されます。 |
| `issueType` | string | はい | 作成する課題のタイプ(例:タスク、ストーリー) |
| `labels` | array | いいえ | 課題のラベル(ラベル名の配列) |
| `duedate` | string | いいえ | 課題の期限形式YYYY-MM-DD |
| `reporter` | string | いいえ | 課題の報告者アカウントID |
| `environment` | string | いいえ | 課題の環境情報 |
| `customFieldId` | string | いいえ | カスタムフィールドIDcustomfield_10001 |
| `customFieldValue` | string | いいえ | カスタムフィールドの値 |
#### 出力
@@ -112,8 +106,7 @@ Jira課題を作成する
| `ts` | string | 操作のタイムスタンプ |
| `issueKey` | string | 作成された課題キーPROJ-123 |
| `summary` | string | 課題の要約 |
| `url` | string | 作成された課題のURL |
| `assigneeId` | string | 割り当てられたユーザーのアカウントID割り当てられている場合 |
| `url` | string | 作成された課題のURL |
### `jira_bulk_read`
@@ -527,31 +520,7 @@ Jira課題からウォッチャーを削除する
| `issueKey` | string | 課題キー |
| `watcherAccountId` | string | 削除されたウォッチャーのアカウントID |
### `jira_get_users`
## 注意事項
Jiraユーザーを取得します。アカウントIDが提供された場合、単一のユーザーを返します。それ以外の場合、すべてのユーザーのリストを返します。
#### 入力
| パラメータ | 型 | 必須 | 説明 |
| --------- | ---- | -------- | ----------- |
| `domain` | string | はい | あなたのJiraドメインyourcompany.atlassian.net |
| `accountId` | string | いいえ | 特定のユーザーを取得するためのオプションのアカウントID。提供されない場合、すべてのユーザーを返します。 |
| `startAt` | number | いいえ | 返す最初のユーザーのインデックスページネーション用、デフォルト0 |
| `maxResults` | number | いいえ | 返すユーザーの最大数デフォルト50 |
| `cloudId` | string | いいえ | インスタンスのJira Cloud ID。提供されない場合、ドメインを使用して取得されます。 |
#### 出力
| パラメータ | 型 | 説明 |
| --------- | ---- | ----------- |
| `ts` | string | 操作のタイムスタンプ |
| `users` | json | accountId、displayName、emailAddress、activeステータス、avatarUrlsを含むユーザーの配列 |
| `total` | number | 返されたユーザーの総数 |
| `startAt` | number | ページネーション開始インデックス |
| `maxResults` | number | ページあたりの最大結果数 |
## 注記
- カテゴリ:`tools`
- タイプ:`jira`
- カテゴリー: `tools`
- タイプ: `jira`

View File

@@ -91,19 +91,13 @@ Jira 的主要功能包括:
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `domain` | 字符串 | 是 | 您的 Jira 域名 \(例如yourcompany.atlassian.net\) |
| `projectId` | 字符串 | 是 | 问题所属项目 ID |
| `summary` | 字符串 | 是 | 问题摘要 |
| `description` | 字符串 | 否 | 问题描述 |
| `priority` | 字符串 | 否 | 问题优先级 ID 或名称 \(例如“10000”或“High”\) |
| `assignee` | 字符串 | 否 | 问题负责人账户 ID |
| `cloudId` | 字符串 | 否 | 实例的 Jira Cloud ID。如果未提供将使用域名获取。 |
| `issueType` | 字符串 | 是 | 要创建的问题类型 \(例如:Task、Story\) |
| `labels` | 数组 | 否 | 问题标签 \(标签名称数组\) |
| `duedate` | 字符串 | 否 | 问题截止日期 \(格式YYYY-MM-DD\) |
| `reporter` | 字符串 | 否 | 问题报告人账户 ID |
| `environment` | 字符串 | 否 | 问题环境信息 |
| `customFieldId` | 字符串 | 否 | 自定义字段 ID \(例如customfield_10001\) |
| `customFieldValue` | 字符串 | 否 | 自定义字段的值 |
| `projectId` | 字符串 | 是 | 问题项目 ID |
| `summary` | 字符串 | 是 | 问题摘要 |
| `description` | 字符串 | 否 | 问题描述 |
| `priority` | 字符串 | 否 | 问题优先级 |
| `assignee` | 字符串 | 否 | 问题负责人 |
| `cloudId` | 字符串 | 否 | 实例的 Jira ID。如果未提供将使用域名获取。 |
| `issueType` | 字符串 | 是 | 要创建的问题类型 \(例如:任务、故事\) |
#### 输出
@@ -113,7 +107,6 @@ Jira 的主要功能包括:
| `issueKey` | 字符串 | 创建的问题键 \(例如PROJ-123\) |
| `summary` | 字符串 | 问题摘要 |
| `url` | 字符串 | 创建的问题的 URL |
| `assigneeId` | 字符串 | 已分配用户的账户 ID如已分配 |
### `jira_bulk_read`
@@ -527,31 +520,7 @@ Jira 的主要功能包括:
| `issueKey` | string | 问题键 |
| `watcherAccountId` | string | 移除的观察者账户 ID |
### `jira_get_users`
## 注意事项
获取 Jira 用户。如果提供了账户 ID则返回单个用户否则返回所有用户的列表。
#### 输入
| 参数 | 类型 | 必需 | 描述 |
| --------- | ---- | -------- | ----------- |
| `domain` | 字符串 | 是 | 您的 Jira 域名 \(例如yourcompany.atlassian.net\) |
| `accountId` | 字符串 | 否 | 可选账户 ID用于获取特定用户。如果未提供则返回所有用户。 |
| `startAt` | 数字 | 否 | 要返回的第一个用户的索引 \(用于分页默认值0\) |
| `maxResults` | 数字 | 否 | 要返回的最大用户数 \(默认值50\) |
| `cloudId` | 字符串 | 否 | 实例的 Jira Cloud ID。如果未提供将使用域名获取。 |
#### 输出
| 参数 | 类型 | 描述 |
| --------- | ---- | ----------- |
| `ts` | 字符串 | 操作的时间戳 |
| `users` | json | 用户数组,包含 accountId、displayName、emailAddress、active 状态和 avatarUrls |
| `total` | 数字 | 返回的用户总数 |
| `startAt` | 数字 | 分页起始索引 |
| `maxResults` | 数字 | 每页最大结果数 |
## 备注
- 分类:`tools`
- 类型:`jira`
- 类别: `tools`
- 类型: `jira`

View File

@@ -2521,9 +2521,9 @@ checksums:
content/22: ef92d95455e378abe4d27a1cdc5e1aed
content/23: febd6019055f3754953fd93395d0dbf2
content/24: 371d0e46b4bd2c23f559b8bc112f6955
content/25: caf6acbe2a4495ca055cb9006ce47250
content/25: 7ef3f388e5ee9346bac54c771d825f40
content/26: bcadfc362b69078beee0088e5936c98b
content/27: 57662dd91f8d1d807377fd48fa0e9142
content/27: e0fa91c45aa780fc03e91df77417f893
content/28: b463f54cd5fe2458b5842549fbb5e1ce
content/29: 55f8c724e1a2463bc29a32518a512c73
content/30: 371d0e46b4bd2c23f559b8bc112f6955
@@ -2638,14 +2638,8 @@ checksums:
content/139: 33fde4c3da4584b51f06183b7b192a78
content/140: bcadfc362b69078beee0088e5936c98b
content/141: b7451190f100388d999c183958d787a7
content/142: d0f9e799e2e5cc62de60668d35fd846f
content/143: b19069ff19899fe202217e06e002c447
content/144: 371d0e46b4bd2c23f559b8bc112f6955
content/145: 480fd62f8d9cc18467e82f4c3f70beea
content/146: bcadfc362b69078beee0088e5936c98b
content/147: 4e73a65d3b873f3979587e10a0f39e72
content/148: b3f310d5ef115bea5a8b75bf25d7ea9a
content/149: 4930918f803340baa861bed9cdf789de
content/142: b3f310d5ef115bea5a8b75bf25d7ea9a
content/143: 4930918f803340baa861bed9cdf789de
8f76e389f6226f608571622b015ca6a1:
meta/title: ddfe2191ea61b34d8b7cc1d7c19b94ac
meta/description: 049ff551f2ebabb15cdea0c71bd8e4eb

View File

@@ -20,12 +20,6 @@ export async function POST(request: Request) {
cloudId: providedCloudId,
issueType,
parent,
labels,
duedate,
reporter,
environment,
customFieldId,
customFieldValue,
} = await request.json()
if (!domain) {
@@ -100,57 +94,17 @@ export async function POST(request: Request) {
}
if (priority !== undefined && priority !== null && priority !== '') {
const isNumericId = /^\d+$/.test(priority)
fields.priority = isNumericId ? { id: priority } : { name: priority }
}
if (labels !== undefined && labels !== null && Array.isArray(labels) && labels.length > 0) {
fields.labels = labels
}
if (duedate !== undefined && duedate !== null && duedate !== '') {
fields.duedate = duedate
}
if (reporter !== undefined && reporter !== null && reporter !== '') {
fields.reporter = {
id: reporter,
fields.priority = {
name: priority,
}
}
if (environment !== undefined && environment !== null && environment !== '') {
fields.environment = {
type: 'doc',
version: 1,
content: [
{
type: 'paragraph',
content: [
{
type: 'text',
text: environment,
},
],
},
],
if (assignee !== undefined && assignee !== null && assignee !== '') {
fields.assignee = {
id: assignee,
}
}
if (
customFieldId !== undefined &&
customFieldId !== null &&
customFieldId !== '' &&
customFieldValue !== undefined &&
customFieldValue !== null &&
customFieldValue !== ''
) {
const fieldId = customFieldId.startsWith('customfield_')
? customFieldId
: `customfield_${customFieldId}`
fields[fieldId] = customFieldValue
}
const body = { fields }
const response = await fetch(url, {
@@ -178,47 +132,16 @@ export async function POST(request: Request) {
}
const responseData = await response.json()
const issueKey = responseData.key || 'unknown'
logger.info('Successfully created Jira issue:', issueKey)
let assigneeId: string | undefined
if (assignee !== undefined && assignee !== null && assignee !== '') {
const assignUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/issue/${issueKey}/assignee`
logger.info('Assigning issue to:', assignee)
const assignResponse = await fetch(assignUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
accountId: assignee,
}),
})
if (!assignResponse.ok) {
const assignErrorText = await assignResponse.text()
logger.warn('Failed to assign issue (issue was created successfully):', {
status: assignResponse.status,
error: assignErrorText,
})
} else {
assigneeId = assignee
logger.info('Successfully assigned issue to:', assignee)
}
}
logger.info('Successfully created Jira issue:', responseData.key)
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
issueKey: issueKey,
issueKey: responseData.key || 'unknown',
summary: responseData.fields?.summary || 'Issue created',
success: true,
url: `https://${domain}/browse/${issueKey}`,
...(assigneeId && { assigneeId }),
url: `https://${domain}/browse/${responseData.key}`,
},
})
} catch (error: any) {

View File

@@ -37,7 +37,6 @@ import { useWand } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-
import type { GenerationType } from '@/blocks/types'
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
import { useTagSelection } from '@/hooks/use-tag-selection'
import { useTextHistory } from '@/hooks/use-text-history'
import { normalizeBlockName } from '@/stores/workflows/utils'
const logger = createLogger('Code')
@@ -306,20 +305,6 @@ export function Code({
},
})
// Text history for undo/redo with debouncing
const textHistory = useTextHistory({
blockId,
subBlockId,
value: code,
onChange: (newValue) => {
setCode(newValue)
if (!isPreview && !disabled) {
setStoreValue(newValue)
}
},
disabled: isPreview || disabled || readOnly || isAiStreaming,
})
const getDefaultValueString = () => {
if (defaultValue === undefined || defaultValue === null) return ''
if (typeof defaultValue === 'string') return defaultValue
@@ -363,12 +348,10 @@ export function Code({
useEffect(() => {
handleStreamStartRef.current = () => {
setCode('')
lastInternalValueRef.current = ''
}
handleGeneratedContentRef.current = (generatedCode: string) => {
setCode(generatedCode)
lastInternalValueRef.current = generatedCode
if (!isPreview && !disabled) {
setStoreValue(generatedCode)
}
@@ -404,21 +387,14 @@ export function Code({
}
}, [readOnly])
// Ref to track the last value we set internally (to avoid sync loops)
const lastInternalValueRef = useRef<string>('')
// Effects: Sync code with external value (only for truly external changes)
// Effects: Sync code with external value
useEffect(() => {
if (isAiStreaming) return
const valueString = value?.toString() ?? ''
// Only sync if this is a genuine external change, not our own update
// This prevents resetting the undo history when we update the store
if (valueString !== code && valueString !== lastInternalValueRef.current) {
if (valueString !== code) {
setCode(valueString)
lastInternalValueRef.current = valueString
}
}, [value, isAiStreaming]) // Removed 'code' from dependencies to prevent sync loops
}, [value, code, isAiStreaming])
// Effects: Track active line number for cursor position
useEffect(() => {
@@ -526,9 +502,8 @@ export function Code({
const dropPosition = textarea?.selectionStart ?? code.length
const newValue = `${code.slice(0, dropPosition)}<${code.slice(dropPosition)}`
// Use textHistory for proper undo tracking
textHistory.handleChange(newValue)
lastInternalValueRef.current = newValue
setCode(newValue)
setStoreValue(newValue)
const newCursorPosition = dropPosition + 1
setCursorPosition(newCursorPosition)
@@ -556,9 +531,7 @@ export function Code({
*/
const handleTagSelect = (newValue: string) => {
if (!isPreview && !readOnly) {
// Use textHistory for proper undo tracking
textHistory.handleChange(newValue)
lastInternalValueRef.current = newValue
setCode(newValue)
emitTagSelection(newValue)
}
setShowTags(false)
@@ -575,9 +548,7 @@ export function Code({
*/
const handleEnvVarSelect = (newValue: string) => {
if (!isPreview && !readOnly) {
// Use textHistory for proper undo tracking
textHistory.handleChange(newValue)
lastInternalValueRef.current = newValue
setCode(newValue)
emitTagSelection(newValue)
}
setShowEnvVars(false)
@@ -770,10 +741,8 @@ export function Code({
value={code}
onValueChange={(newCode) => {
if (!isAiStreaming && !isPreview && !disabled && !readOnly) {
// Use textHistory for debounced undo/redo tracking
textHistory.handleChange(newCode)
// Track this as an internal change to prevent sync loops
lastInternalValueRef.current = newCode
setCode(newCode)
setStoreValue(newCode)
const textarea = editorRef.current?.querySelector('textarea')
if (textarea) {
@@ -793,10 +762,6 @@ export function Code({
}
}}
onKeyDown={(e) => {
// Let text history handle undo/redo first
if (textHistory.handleKeyDown(e)) {
return
}
if (e.key === 'Escape') {
setShowTags(false)
setShowEnvVars(false)
@@ -805,10 +770,6 @@ export function Code({
e.preventDefault()
}
}}
onBlur={() => {
// Commit any pending text history changes on blur
textHistory.handleBlur()
}}
highlight={createHighlightFunction(effectiveLanguage, shouldHighlightReference)}
{...getCodeEditorProps({ isStreaming: isAiStreaming, isPreview, disabled })}
/>

View File

@@ -844,13 +844,8 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
if (!accessibleBlock) continue
// Skip the current block - blocks cannot reference their own outputs
// Exception: approval and human_in_the_loop blocks can reference their own outputs
if (
accessibleBlockId === blockId &&
accessibleBlock.type !== 'approval' &&
accessibleBlock.type !== 'human_in_the_loop'
)
continue
// Exception: approval blocks can reference their own outputs
if (accessibleBlockId === blockId && accessibleBlock.type !== 'approval') continue
const blockConfig = getBlock(accessibleBlock.type)
@@ -977,8 +972,6 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
const allTags = outputPaths.map((path) => `${normalizedBlockName}.${path}`)
blockTags = isSelfReference ? allTags.filter((tag) => tag.endsWith('.url')) : allTags
}
} else if (accessibleBlock.type === 'human_in_the_loop') {
blockTags = [`${normalizedBlockName}.url`]
} else {
const operationValue =
mergedSubBlocks?.operation?.value ?? getSubBlockValue(accessibleBlockId, 'operation')
@@ -1221,25 +1214,31 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
let processedTag = tag
// Check if this is a file property and add [0] automatically
// Only include user-accessible fields (matches UserFile interface)
const fileProperties = ['id', 'name', 'url', 'size', 'type']
const parts = tag.split('.')
if (parts.length >= 3 && blockGroup) {
const arrayFieldName = parts[1] // e.g., "channels", "files", "users"
const block = useWorkflowStore.getState().blocks[blockGroup.blockId]
const blockConfig = block ? (getBlock(block.type) ?? null) : null
const mergedSubBlocks = getMergedSubBlocks(blockGroup.blockId)
if (parts.length >= 2 && fileProperties.includes(parts[parts.length - 1])) {
const fieldName = parts[parts.length - 2]
const fieldType = getOutputTypeForPath(
block,
blockConfig,
blockGroup.blockId,
arrayFieldName,
mergedSubBlocks
)
if (blockGroup) {
const block = useWorkflowStore.getState().blocks[blockGroup.blockId]
const blockConfig = block ? (getBlock(block.type) ?? null) : null
const mergedSubBlocks = getMergedSubBlocks(blockGroup.blockId)
if (fieldType === 'files' || fieldType === 'array') {
const blockName = parts[0]
const remainingPath = parts.slice(2).join('.')
processedTag = `${blockName}.${arrayFieldName}[0].${remainingPath}`
const fieldType = getOutputTypeForPath(
block,
blockConfig,
blockGroup.blockId,
fieldName,
mergedSubBlocks
)
if (fieldType === 'files') {
const blockAndField = parts.slice(0, -1).join('.')
const property = parts[parts.length - 1]
processedTag = `${blockAndField}[0].${property}`
}
}
}

View File

@@ -1,5 +1,5 @@
import type { ReactElement } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import 'prismjs/components/prism-json'
import { Wand2 } from 'lucide-react'
import Editor from 'react-simple-code-editor'
@@ -17,7 +17,6 @@ import {
createEnvVarPattern,
createWorkflowVariablePattern,
} from '@/executor/utils/reference-validation'
import { useTextHistoryStore } from '@/stores/text-history'
interface CodeEditorProps {
value: string
@@ -34,11 +33,6 @@ interface CodeEditorProps {
showWandButton?: boolean
onWandClick?: () => void
wandButtonDisabled?: boolean
/**
* Unique identifier for text history. When provided, enables undo/redo functionality.
* Format: "blockId:fieldName" e.g. "block-123:schema" or "block-123:code"
*/
historyId?: string
}
export function CodeEditor({
@@ -56,125 +50,16 @@ export function CodeEditor({
showWandButton = false,
onWandClick,
wandButtonDisabled = false,
historyId,
}: CodeEditorProps) {
const [code, setCode] = useState(value)
const [visualLineHeights, setVisualLineHeights] = useState<number[]>([])
const editorRef = useRef<HTMLDivElement>(null)
const lastInternalValueRef = useRef<string>(value)
const initializedRef = useRef(false)
// Text history store for undo/redo
const textHistoryStore = useTextHistoryStore()
// Parse historyId into blockId and subBlockId for the store
const [historyBlockId, historySubBlockId] = historyId?.split(':') ?? ['', '']
const hasHistory = Boolean(historyId && historyBlockId && historySubBlockId)
// Initialize history on mount
useEffect(() => {
if (hasHistory && !initializedRef.current) {
textHistoryStore.initHistory(historyBlockId, historySubBlockId, value)
initializedRef.current = true
}
}, [hasHistory, historyBlockId, historySubBlockId, value, textHistoryStore])
// Sync external value changes (but avoid resetting undo history for internal changes)
useEffect(() => {
if (value !== code && value !== lastInternalValueRef.current) {
setCode(value)
lastInternalValueRef.current = value
}
setCode(value)
}, [value])
// Handle value change with history tracking
const handleValueChange = useCallback(
(newCode: string) => {
setCode(newCode)
lastInternalValueRef.current = newCode
onChange(newCode)
// Record to history if enabled
if (hasHistory) {
textHistoryStore.recordChange(historyBlockId, historySubBlockId, newCode)
}
},
[onChange, hasHistory, historyBlockId, historySubBlockId, textHistoryStore]
)
// Handle undo
const handleUndo = useCallback(() => {
if (!hasHistory) return false
const previousValue = textHistoryStore.undo(historyBlockId, historySubBlockId)
if (previousValue !== null) {
setCode(previousValue)
lastInternalValueRef.current = previousValue
onChange(previousValue)
return true
}
return false
}, [hasHistory, historyBlockId, historySubBlockId, textHistoryStore, onChange])
// Handle redo
const handleRedo = useCallback(() => {
if (!hasHistory) return false
const nextValue = textHistoryStore.redo(historyBlockId, historySubBlockId)
if (nextValue !== null) {
setCode(nextValue)
lastInternalValueRef.current = nextValue
onChange(nextValue)
return true
}
return false
}, [hasHistory, historyBlockId, historySubBlockId, textHistoryStore, onChange])
// Handle keyboard events for undo/redo
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (disabled) return
const isMod = e.metaKey || e.ctrlKey
// Undo: Cmd+Z / Ctrl+Z
if (isMod && e.key === 'z' && !e.shiftKey && hasHistory) {
if (handleUndo()) {
e.preventDefault()
e.stopPropagation()
return
}
}
// Redo: Cmd+Shift+Z / Ctrl+Shift+Z / Ctrl+Y
if (hasHistory) {
if (
(isMod && e.key === 'z' && e.shiftKey) ||
(isMod && e.key === 'Z') ||
(e.ctrlKey && e.key === 'y')
) {
if (handleRedo()) {
e.preventDefault()
e.stopPropagation()
return
}
}
}
// Call parent's onKeyDown if provided
onKeyDown?.(e)
},
[disabled, hasHistory, handleUndo, handleRedo, onKeyDown]
)
// Handle blur - commit pending history
const handleBlur = useCallback(() => {
if (hasHistory) {
textHistoryStore.commitPending(historyBlockId, historySubBlockId)
}
}, [hasHistory, historyBlockId, historySubBlockId, textHistoryStore])
useEffect(() => {
if (!editorRef.current) return
@@ -326,9 +211,11 @@ export function CodeEditor({
<Editor
value={code}
onValueChange={handleValueChange}
onKeyDown={handleKeyDown}
onBlur={handleBlur}
onValueChange={(newCode) => {
setCode(newCode)
onChange(newCode)
}}
onKeyDown={onKeyDown}
highlight={(code) => customHighlight(code)}
disabled={disabled}
{...getCodeEditorProps({ disabled })}

View File

@@ -936,7 +936,6 @@ try {
gutterClassName='bg-[var(--bg)]'
disabled={schemaGeneration.isLoading || schemaGeneration.isStreaming}
onKeyDown={handleKeyDown}
historyId={`${blockId}:tool-schema`}
/>
</ModalTabsContent>
@@ -1019,7 +1018,6 @@ try {
disabled={codeGeneration.isLoading || codeGeneration.isStreaming}
onKeyDown={handleKeyDown}
schemaParameters={schemaParameters}
historyId={`${blockId}:tool-code`}
/>
{showEnvVars && (

View File

@@ -43,7 +43,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
{ label: 'Delete Issue Link', id: 'delete_link' },
{ label: 'Add Watcher', id: 'add_watcher' },
{ label: 'Remove Watcher', id: 'remove_watcher' },
{ label: 'Get Users', id: 'get_users' },
],
value: () => 'read',
},
@@ -195,71 +194,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
dependsOn: ['projectId'],
condition: { field: 'operation', value: ['update', 'write'] },
},
// Write Issue additional fields
{
id: 'assignee',
title: 'Assignee Account ID',
type: 'short-input',
placeholder: 'Assignee account ID (e.g., 5b109f2e9729b51b54dc274d)',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
},
{
id: 'priority',
title: 'Priority',
type: 'short-input',
placeholder: 'Priority ID or name (e.g., "10000" or "High")',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
},
{
id: 'labels',
title: 'Labels',
type: 'short-input',
placeholder: 'Comma-separated labels (e.g., bug, urgent)',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
},
{
id: 'duedate',
title: 'Due Date',
type: 'short-input',
placeholder: 'YYYY-MM-DD (e.g., 2024-12-31)',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
},
{
id: 'reporter',
title: 'Reporter Account ID',
type: 'short-input',
placeholder: 'Reporter account ID',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
},
{
id: 'environment',
title: 'Environment',
type: 'long-input',
placeholder: 'Environment information (e.g., Production, Staging)',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
},
{
id: 'customFieldId',
title: 'Custom Field ID',
type: 'short-input',
placeholder: 'e.g., customfield_10001 or 10001',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
},
{
id: 'teamUuid',
title: 'Team UUID',
type: 'short-input',
placeholder: 'e.g., b3aa307a-76ea-462d-b6f1-a6e89ce9858a',
dependsOn: ['projectId'],
condition: { field: 'operation', value: 'write' },
},
// Delete Issue fields
{
id: 'deleteSubtasks',
@@ -417,28 +351,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
placeholder: 'Enter link ID to delete',
condition: { field: 'operation', value: 'delete_link' },
},
// Get Users fields
{
id: 'userAccountId',
title: 'Account ID',
type: 'short-input',
placeholder: 'Enter account ID for specific user',
condition: { field: 'operation', value: 'get_users' },
},
{
id: 'usersStartAt',
title: 'Start At',
type: 'short-input',
placeholder: 'Pagination start index (default: 0)',
condition: { field: 'operation', value: 'get_users' },
},
{
id: 'usersMaxResults',
title: 'Max Results',
type: 'short-input',
placeholder: 'Maximum users to return (default: 50)',
condition: { field: 'operation', value: 'get_users' },
},
// Trigger SubBlocks
...getTrigger('jira_issue_created').subBlocks,
...getTrigger('jira_issue_updated').subBlocks,
@@ -471,7 +383,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
'jira_delete_issue_link',
'jira_add_watcher',
'jira_remove_watcher',
'jira_get_users',
],
config: {
tool: (params) => {
@@ -527,8 +438,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
return 'jira_add_watcher'
case 'remove_watcher':
return 'jira_remove_watcher'
case 'get_users':
return 'jira_get_users'
default:
return 'jira_retrieve'
}
@@ -552,29 +461,12 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
'Project ID is required. Please select a project or enter a project ID manually.'
)
}
// Parse comma-separated strings into arrays
const parseCommaSeparated = (value: string | undefined): string[] | undefined => {
if (!value || value.trim() === '') return undefined
return value
.split(',')
.map((item) => item.trim())
.filter((item) => item !== '')
}
const writeParams = {
projectId: effectiveProjectId,
summary: params.summary || '',
description: params.description || '',
issueType: params.issueType || 'Task',
parent: params.parentIssue ? { key: params.parentIssue } : undefined,
assignee: params.assignee || undefined,
priority: params.priority || undefined,
labels: parseCommaSeparated(params.labels),
duedate: params.duedate || undefined,
reporter: params.reporter || undefined,
environment: params.environment || undefined,
customFieldId: params.customFieldId || undefined,
customFieldValue: params.customFieldValue || undefined,
}
return {
...baseParams,
@@ -812,16 +704,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
accountId: params.accountId,
}
}
case 'get_users': {
return {
...baseParams,
accountId: params.userAccountId || undefined,
startAt: params.usersStartAt ? Number.parseInt(params.usersStartAt) : undefined,
maxResults: params.usersMaxResults
? Number.parseInt(params.usersMaxResults)
: undefined,
}
}
default:
return baseParams
}
@@ -840,15 +722,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
summary: { type: 'string', description: 'Issue summary' },
description: { type: 'string', description: 'Issue description' },
issueType: { type: 'string', description: 'Issue type' },
// Write operation additional inputs
assignee: { type: 'string', description: 'Assignee account ID' },
priority: { type: 'string', description: 'Priority ID or name' },
labels: { type: 'string', description: 'Comma-separated labels for the issue' },
duedate: { type: 'string', description: 'Due date in YYYY-MM-DD format' },
reporter: { type: 'string', description: 'Reporter account ID' },
environment: { type: 'string', description: 'Environment information' },
customFieldId: { type: 'string', description: 'Custom field ID (e.g., customfield_10001)' },
customFieldValue: { type: 'string', description: 'Value for the custom field' },
// Delete operation inputs
deleteSubtasks: { type: 'string', description: 'Whether to delete subtasks (true/false)' },
// Assign/Watcher operation inputs
@@ -885,13 +758,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
linkType: { type: 'string', description: 'Type of link (e.g., "Blocks", "Relates")' },
linkComment: { type: 'string', description: 'Optional comment for issue link' },
linkId: { type: 'string', description: 'Link ID for delete operation' },
// Get Users operation inputs
userAccountId: {
type: 'string',
description: 'Account ID for specific user lookup (optional)',
},
usersStartAt: { type: 'string', description: 'Pagination start index for users' },
usersMaxResults: { type: 'string', description: 'Maximum users to return' },
},
outputs: {
// Common outputs across all Jira operations
@@ -968,12 +834,6 @@ export const JiraBlock: BlockConfig<JiraResponse> = {
// jira_add_watcher, jira_remove_watcher outputs
watcherAccountId: { type: 'string', description: 'Watcher account ID' },
// jira_get_users outputs
users: {
type: 'json',
description: 'Array of users with accountId, displayName, emailAddress, active status',
},
// jira_bulk_read outputs
// Note: bulk_read returns an array in the output field, each item contains:
// ts, issueKey, summary, description, status, assignee, created, updated

View File

@@ -128,8 +128,6 @@ export const DEFAULTS = {
BLOCK_TITLE: 'Untitled Block',
WORKFLOW_NAME: 'Workflow',
MAX_LOOP_ITERATIONS: 1000,
MAX_FOREACH_ITEMS: 1000,
MAX_PARALLEL_BRANCHES: 20,
MAX_WORKFLOW_DEPTH: 10,
EXECUTION_TIME: 0,
TOKENS: {

View File

@@ -4,7 +4,6 @@ import { LoopConstructor } from '@/executor/dag/construction/loops'
import { NodeConstructor } from '@/executor/dag/construction/nodes'
import { PathConstructor } from '@/executor/dag/construction/paths'
import type { DAGEdge, NodeMetadata } from '@/executor/dag/types'
import { buildSentinelStartId, extractBaseBlockId } from '@/executor/utils/subflow-utils'
import type {
SerializedBlock,
SerializedLoop,
@@ -80,9 +79,6 @@ export class DAGBuilder {
}
}
// Validate loop and parallel structure
this.validateSubflowStructure(dag)
logger.info('DAG built', {
totalNodes: dag.nodes.size,
loopCount: dag.loopConfigs.size,
@@ -109,43 +105,4 @@ export class DAGBuilder {
}
}
}
/**
* Validates that loops and parallels have proper internal structure.
* Throws an error if a loop/parallel has no blocks inside or no connections from start.
*/
private validateSubflowStructure(dag: DAG): void {
for (const [id, config] of dag.loopConfigs) {
this.validateSubflow(dag, id, config.nodes, 'Loop')
}
for (const [id, config] of dag.parallelConfigs) {
this.validateSubflow(dag, id, config.nodes, 'Parallel')
}
}
private validateSubflow(
dag: DAG,
id: string,
nodes: string[] | undefined,
type: 'Loop' | 'Parallel'
): void {
if (!nodes || nodes.length === 0) {
throw new Error(
`${type} has no blocks inside. Add at least one block to the ${type.toLowerCase()}.`
)
}
const sentinelStartNode = dag.nodes.get(buildSentinelStartId(id))
if (!sentinelStartNode) return
const hasConnections = Array.from(sentinelStartNode.outgoingEdges.values()).some((edge) =>
nodes.includes(extractBaseBlockId(edge.target))
)
if (!hasConnections) {
throw new Error(
`${type} start is not connected to any blocks. Connect a block to the ${type.toLowerCase()} start.`
)
}
}
}

View File

@@ -63,10 +63,8 @@ export class DAGExecutor {
const resolver = new VariableResolver(this.workflow, this.workflowVariables, state)
const loopOrchestrator = new LoopOrchestrator(dag, state, resolver)
loopOrchestrator.setContextExtensions(this.contextExtensions)
const parallelOrchestrator = new ParallelOrchestrator(dag, state)
parallelOrchestrator.setResolver(resolver)
parallelOrchestrator.setContextExtensions(this.contextExtensions)
const allHandlers = createBlockHandlers()
const blockExecutor = new BlockExecutor(allHandlers, resolver, this.contextExtensions, state)
const edgeManager = new EdgeManager(dag)

View File

@@ -14,8 +14,6 @@ export interface LoopScope {
condition?: string
loopType?: 'for' | 'forEach' | 'while' | 'doWhile'
skipFirstConditionCheck?: boolean
/** Error message if loop validation failed (e.g., exceeded max iterations) */
validationError?: string
}
export interface ParallelScope {
@@ -25,8 +23,6 @@ export interface ParallelScope {
completedCount: number
totalExpectedNodes: number
items?: any[]
/** Error message if parallel validation failed (e.g., exceeded max branches) */
validationError?: string
}
export class ExecutionState implements BlockStateController {

View File

@@ -5,17 +5,14 @@ import { buildLoopIndexCondition, DEFAULTS, EDGE } from '@/executor/constants'
import type { DAG } from '@/executor/dag/builder'
import type { EdgeManager } from '@/executor/execution/edge-manager'
import type { LoopScope } from '@/executor/execution/state'
import type { BlockStateController, ContextExtensions } from '@/executor/execution/types'
import type { BlockStateController } from '@/executor/execution/types'
import type { ExecutionContext, NormalizedBlockOutput } from '@/executor/types'
import type { LoopConfigWithNodes } from '@/executor/types/loop'
import { replaceValidReferences } from '@/executor/utils/reference-validation'
import {
addSubflowErrorLog,
buildSentinelEndId,
buildSentinelStartId,
extractBaseBlockId,
resolveArrayInput,
validateMaxCount,
} from '@/executor/utils/subflow-utils'
import type { VariableResolver } from '@/executor/variables/resolver'
import type { SerializedLoop } from '@/serializer/types'
@@ -35,7 +32,6 @@ export interface LoopContinuationResult {
export class LoopOrchestrator {
private edgeManager: EdgeManager | null = null
private contextExtensions: ContextExtensions | null = null
constructor(
private dag: DAG,
@@ -43,10 +39,6 @@ export class LoopOrchestrator {
private resolver: VariableResolver
) {}
setContextExtensions(contextExtensions: ContextExtensions): void {
this.contextExtensions = contextExtensions
}
setEdgeManager(edgeManager: EdgeManager): void {
this.edgeManager = edgeManager
}
@@ -56,6 +48,7 @@ export class LoopOrchestrator {
if (!loopConfig) {
throw new Error(`Loop config not found: ${loopId}`)
}
const scope: LoopScope = {
iteration: 0,
currentIterationOutputs: new Map(),
@@ -65,70 +58,15 @@ export class LoopOrchestrator {
const loopType = loopConfig.loopType
switch (loopType) {
case 'for': {
case 'for':
scope.loopType = 'for'
const requestedIterations = loopConfig.iterations || DEFAULTS.MAX_LOOP_ITERATIONS
const iterationError = validateMaxCount(
requestedIterations,
DEFAULTS.MAX_LOOP_ITERATIONS,
'For loop iterations'
)
if (iterationError) {
logger.error(iterationError, { loopId, requestedIterations })
this.addLoopErrorLog(ctx, loopId, loopType, iterationError, {
iterations: requestedIterations,
})
scope.maxIterations = 0
scope.validationError = iterationError
scope.condition = buildLoopIndexCondition(0)
ctx.loopExecutions?.set(loopId, scope)
throw new Error(iterationError)
}
scope.maxIterations = requestedIterations
scope.maxIterations = loopConfig.iterations || DEFAULTS.MAX_LOOP_ITERATIONS
scope.condition = buildLoopIndexCondition(scope.maxIterations)
break
}
case 'forEach': {
scope.loopType = 'forEach'
let items: any[]
try {
items = this.resolveForEachItems(ctx, loopConfig.forEachItems)
} catch (error) {
const errorMessage = `ForEach loop resolution failed: ${error instanceof Error ? error.message : String(error)}`
logger.error(errorMessage, { loopId, forEachItems: loopConfig.forEachItems })
this.addLoopErrorLog(ctx, loopId, loopType, errorMessage, {
forEachItems: loopConfig.forEachItems,
})
scope.items = []
scope.maxIterations = 0
scope.validationError = errorMessage
scope.condition = buildLoopIndexCondition(0)
ctx.loopExecutions?.set(loopId, scope)
throw new Error(errorMessage)
}
const sizeError = validateMaxCount(
items.length,
DEFAULTS.MAX_FOREACH_ITEMS,
'ForEach loop collection size'
)
if (sizeError) {
logger.error(sizeError, { loopId, collectionSize: items.length })
this.addLoopErrorLog(ctx, loopId, loopType, sizeError, {
forEachItems: loopConfig.forEachItems,
collectionSize: items.length,
})
scope.items = []
scope.maxIterations = 0
scope.validationError = sizeError
scope.condition = buildLoopIndexCondition(0)
ctx.loopExecutions?.set(loopId, scope)
throw new Error(sizeError)
}
const items = this.resolveForEachItems(ctx, loopConfig.forEachItems)
scope.items = items
scope.maxIterations = items.length
scope.item = items[0]
@@ -141,35 +79,15 @@ export class LoopOrchestrator {
scope.condition = loopConfig.whileCondition
break
case 'doWhile': {
case 'doWhile':
scope.loopType = 'doWhile'
if (loopConfig.doWhileCondition) {
scope.condition = loopConfig.doWhileCondition
} else {
const requestedIterations = loopConfig.iterations || DEFAULTS.MAX_LOOP_ITERATIONS
const iterationError = validateMaxCount(
requestedIterations,
DEFAULTS.MAX_LOOP_ITERATIONS,
'Do-While loop iterations'
)
if (iterationError) {
logger.error(iterationError, { loopId, requestedIterations })
this.addLoopErrorLog(ctx, loopId, loopType, iterationError, {
iterations: requestedIterations,
})
scope.maxIterations = 0
scope.validationError = iterationError
scope.condition = buildLoopIndexCondition(0)
ctx.loopExecutions?.set(loopId, scope)
throw new Error(iterationError)
}
scope.maxIterations = requestedIterations
scope.maxIterations = loopConfig.iterations || DEFAULTS.MAX_LOOP_ITERATIONS
scope.condition = buildLoopIndexCondition(scope.maxIterations)
}
break
}
default:
throw new Error(`Unknown loop type: ${loopType}`)
@@ -182,23 +100,6 @@ export class LoopOrchestrator {
return scope
}
private addLoopErrorLog(
ctx: ExecutionContext,
loopId: string,
loopType: string,
errorMessage: string,
inputData?: any
): void {
addSubflowErrorLog(
ctx,
loopId,
'loop',
errorMessage,
{ loopType, ...inputData },
this.contextExtensions
)
}
storeLoopNodeOutput(
ctx: ExecutionContext,
loopId: string,
@@ -511,6 +412,54 @@ export class LoopOrchestrator {
}
private resolveForEachItems(ctx: ExecutionContext, items: any): any[] {
return resolveArrayInput(ctx, items, this.resolver)
if (Array.isArray(items)) {
return items
}
if (typeof items === 'object' && items !== null) {
return Object.entries(items)
}
if (typeof items === 'string') {
if (items.startsWith('<') && items.endsWith('>')) {
const resolved = this.resolver.resolveSingleReference(ctx, '', items)
if (Array.isArray(resolved)) {
return resolved
}
return []
}
try {
const normalized = items.replace(/'/g, '"')
const parsed = JSON.parse(normalized)
if (Array.isArray(parsed)) {
return parsed
}
return []
} catch (error) {
logger.error('Failed to parse forEach items', { items, error })
return []
}
}
try {
const resolved = this.resolver.resolveInputs(ctx, 'loop_foreach_items', { items }).items
if (Array.isArray(resolved)) {
return resolved
}
logger.warn('ForEach items did not resolve to array', {
items,
resolved,
})
return []
} catch (error: any) {
logger.error('Error resolving forEach items, returning empty array:', {
error: error.message,
})
return []
}
}
}

View File

@@ -1,19 +1,15 @@
import { createLogger } from '@/lib/logs/console/logger'
import { DEFAULTS } from '@/executor/constants'
import type { DAG, DAGNode } from '@/executor/dag/builder'
import type { ParallelScope } from '@/executor/execution/state'
import type { BlockStateWriter, ContextExtensions } from '@/executor/execution/types'
import type { BlockStateWriter } from '@/executor/execution/types'
import type { ExecutionContext, NormalizedBlockOutput } from '@/executor/types'
import type { ParallelConfigWithNodes } from '@/executor/types/parallel'
import {
addSubflowErrorLog,
buildBranchNodeId,
calculateBranchCount,
extractBaseBlockId,
extractBranchIndex,
parseDistributionItems,
resolveArrayInput,
validateMaxCount,
} from '@/executor/utils/subflow-utils'
import type { VariableResolver } from '@/executor/variables/resolver'
import type { SerializedParallel } from '@/serializer/types'
@@ -36,7 +32,6 @@ export interface ParallelAggregationResult {
export class ParallelOrchestrator {
private resolver: VariableResolver | null = null
private contextExtensions: ContextExtensions | null = null
constructor(
private dag: DAG,
@@ -47,10 +42,6 @@ export class ParallelOrchestrator {
this.resolver = resolver
}
setContextExtensions(contextExtensions: ContextExtensions): void {
this.contextExtensions = contextExtensions
}
initializeParallelScope(
ctx: ExecutionContext,
parallelId: string,
@@ -58,42 +49,11 @@ export class ParallelOrchestrator {
terminalNodesCount = 1
): ParallelScope {
const parallelConfig = this.dag.parallelConfigs.get(parallelId)
const items = parallelConfig ? this.resolveDistributionItems(ctx, parallelConfig) : undefined
let items: any[] | undefined
if (parallelConfig) {
try {
items = this.resolveDistributionItems(ctx, parallelConfig)
} catch (error) {
const errorMessage = `Parallel distribution resolution failed: ${error instanceof Error ? error.message : String(error)}`
logger.error(errorMessage, {
parallelId,
distribution: parallelConfig.distribution,
})
this.addParallelErrorLog(ctx, parallelId, errorMessage, {
distribution: parallelConfig.distribution,
})
this.setErrorScope(ctx, parallelId, errorMessage)
throw new Error(errorMessage)
}
}
// If we have more items than pre-built branches, expand the DAG
const actualBranchCount = items && items.length > totalBranches ? items.length : totalBranches
const branchError = validateMaxCount(
actualBranchCount,
DEFAULTS.MAX_PARALLEL_BRANCHES,
'Parallel branch count'
)
if (branchError) {
logger.error(branchError, { parallelId, actualBranchCount })
this.addParallelErrorLog(ctx, parallelId, branchError, {
distribution: parallelConfig?.distribution,
branchCount: actualBranchCount,
})
this.setErrorScope(ctx, parallelId, branchError)
throw new Error(branchError)
}
const scope: ParallelScope = {
parallelId,
totalBranches: actualBranchCount,
@@ -148,38 +108,6 @@ export class ParallelOrchestrator {
return scope
}
private addParallelErrorLog(
ctx: ExecutionContext,
parallelId: string,
errorMessage: string,
inputData?: any
): void {
addSubflowErrorLog(
ctx,
parallelId,
'parallel',
errorMessage,
inputData || {},
this.contextExtensions
)
}
private setErrorScope(ctx: ExecutionContext, parallelId: string, errorMessage: string): void {
const scope: ParallelScope = {
parallelId,
totalBranches: 0,
branchOutputs: new Map(),
completedCount: 0,
totalExpectedNodes: 0,
items: [],
validationError: errorMessage,
}
if (!ctx.parallelExecutions) {
ctx.parallelExecutions = new Map()
}
ctx.parallelExecutions.set(parallelId, scope)
}
/**
* Dynamically expand the DAG to include additional branch nodes when
* the resolved item count exceeds the pre-built branch count.
@@ -363,11 +291,63 @@ export class ParallelOrchestrator {
}
}
/**
* Resolve distribution items at runtime, handling references like <previousBlock.items>
* This mirrors how LoopOrchestrator.resolveForEachItems works.
*/
private resolveDistributionItems(ctx: ExecutionContext, config: SerializedParallel): any[] {
if (config.distribution === undefined || config.distribution === null) {
const rawItems = config.distribution
if (rawItems === undefined || rawItems === null) {
return []
}
return resolveArrayInput(ctx, config.distribution, this.resolver)
// Already an array - return as-is
if (Array.isArray(rawItems)) {
return rawItems
}
// Object - convert to entries array (consistent with loop forEach behavior)
if (typeof rawItems === 'object') {
return Object.entries(rawItems)
}
// String handling
if (typeof rawItems === 'string') {
// Resolve references at runtime using the variable resolver
if (rawItems.startsWith('<') && rawItems.endsWith('>') && this.resolver) {
const resolved = this.resolver.resolveSingleReference(ctx, '', rawItems)
if (Array.isArray(resolved)) {
return resolved
}
if (typeof resolved === 'object' && resolved !== null) {
return Object.entries(resolved)
}
logger.warn('Distribution reference did not resolve to array or object', {
rawItems,
resolved,
})
return []
}
// Try to parse as JSON
try {
const normalized = rawItems.replace(/'/g, '"')
const parsed = JSON.parse(normalized)
if (Array.isArray(parsed)) {
return parsed
}
if (typeof parsed === 'object' && parsed !== null) {
return Object.entries(parsed)
}
return []
} catch (error) {
logger.error('Failed to parse distribution items', { rawItems, error })
return []
}
}
return []
}
handleParallelBranchCompletion(

View File

@@ -1,8 +1,5 @@
import { createLogger } from '@/lib/logs/console/logger'
import { LOOP, PARALLEL, PARSING, REFERENCE } from '@/executor/constants'
import type { ContextExtensions } from '@/executor/execution/types'
import type { BlockLog, ExecutionContext } from '@/executor/types'
import type { VariableResolver } from '@/executor/variables/resolver'
import type { SerializedParallel } from '@/serializer/types'
const logger = createLogger('SubflowUtils')
@@ -135,131 +132,3 @@ export function normalizeNodeId(nodeId: string): string {
}
return nodeId
}
/**
* Validates that a count doesn't exceed a maximum limit.
* Returns an error message if validation fails, undefined otherwise.
*/
export function validateMaxCount(count: number, max: number, itemType: string): string | undefined {
if (count > max) {
return `${itemType} (${count}) exceeds maximum allowed (${max}). Execution blocked.`
}
return undefined
}
/**
* Resolves array input at runtime. Handles arrays, objects, references, and JSON strings.
* Used by both loop forEach and parallel distribution resolution.
* Throws an error if resolution fails.
*/
export function resolveArrayInput(
ctx: ExecutionContext,
items: any,
resolver: VariableResolver | null
): any[] {
if (Array.isArray(items)) {
return items
}
if (typeof items === 'object' && items !== null) {
return Object.entries(items)
}
if (typeof items === 'string') {
if (items.startsWith(REFERENCE.START) && items.endsWith(REFERENCE.END) && resolver) {
try {
const resolved = resolver.resolveSingleReference(ctx, '', items)
if (Array.isArray(resolved)) {
return resolved
}
if (typeof resolved === 'object' && resolved !== null) {
return Object.entries(resolved)
}
throw new Error(`Reference "${items}" did not resolve to an array or object`)
} catch (error) {
if (error instanceof Error && error.message.startsWith('Reference "')) {
throw error
}
throw new Error(
`Failed to resolve reference "${items}": ${error instanceof Error ? error.message : String(error)}`
)
}
}
try {
const normalized = items.replace(/'/g, '"')
const parsed = JSON.parse(normalized)
if (Array.isArray(parsed)) {
return parsed
}
if (typeof parsed === 'object' && parsed !== null) {
return Object.entries(parsed)
}
throw new Error(`Parsed value is not an array or object`)
} catch (error) {
if (error instanceof Error && error.message.startsWith('Parsed value')) {
throw error
}
throw new Error(`Failed to parse items as JSON: "${items}"`)
}
}
if (resolver) {
try {
const resolved = resolver.resolveInputs(ctx, 'subflow_items', { items }).items
if (Array.isArray(resolved)) {
return resolved
}
throw new Error(`Resolved items is not an array`)
} catch (error) {
if (error instanceof Error && error.message.startsWith('Resolved items')) {
throw error
}
throw new Error(
`Failed to resolve items: ${error instanceof Error ? error.message : String(error)}`
)
}
}
return []
}
/**
* Creates and logs an error for a subflow (loop or parallel).
*/
export function addSubflowErrorLog(
ctx: ExecutionContext,
blockId: string,
blockType: 'loop' | 'parallel',
errorMessage: string,
inputData: Record<string, any>,
contextExtensions: ContextExtensions | null
): void {
const now = new Date().toISOString()
const block = ctx.workflow?.blocks?.find((b) => b.id === blockId)
const blockName = block?.metadata?.name || (blockType === 'loop' ? 'Loop' : 'Parallel')
const blockLog: BlockLog = {
blockId,
blockName,
blockType,
startedAt: now,
endedAt: now,
durationMs: 0,
success: false,
error: errorMessage,
input: inputData,
output: { error: errorMessage },
...(blockType === 'loop' ? { loopId: blockId } : { parallelId: blockId }),
}
ctx.blockLogs.push(blockLog)
if (contextExtensions?.onBlockComplete) {
contextExtensions.onBlockComplete(blockId, blockName, blockType, {
input: inputData,
output: { error: errorMessage },
executionTime: 0,
})
}
}

View File

@@ -1,196 +0,0 @@
import { useCallback, useEffect, useRef } from 'react'
import { useTextHistoryStore } from '@/stores/text-history'
interface UseTextHistoryOptions {
/** Block ID for the text field */
blockId: string
/** Sub-block ID for the text field */
subBlockId: string
/** Current value of the text field */
value: string
/** Callback to update the value */
onChange: (value: string) => void
/** Whether the field is disabled/readonly */
disabled?: boolean
}
interface UseTextHistoryResult {
/**
* Handle text change - records to history with debouncing
*/
handleChange: (newValue: string) => void
/**
* Handle keyboard events for undo/redo
* Returns true if the event was handled
*/
handleKeyDown: (e: React.KeyboardEvent) => boolean
/**
* Handle blur - commits any pending changes
*/
handleBlur: () => void
/**
* Undo the last change
*/
undo: () => void
/**
* Redo the last undone change
*/
redo: () => void
/**
* Whether undo is available
*/
canUndo: boolean
/**
* Whether redo is available
*/
canRedo: boolean
}
/**
* Hook for managing text undo/redo history for a specific text field.
*
* @remarks
* - Provides debounced history recording (coalesces rapid changes)
* - Handles Cmd+Z/Ctrl+Z for undo, Cmd+Shift+Z/Ctrl+Y for redo
* - Commits pending changes on blur to preserve history
* - Each blockId:subBlockId pair has its own independent history
*
* @example
* ```tsx
* const { handleChange, handleKeyDown, handleBlur } = useTextHistory({
* blockId,
* subBlockId,
* value: code,
* onChange: (newCode) => {
* setCode(newCode)
* setStoreValue(newCode)
* },
* })
*
* <textarea
* value={code}
* onChange={(e) => handleChange(e.target.value)}
* onKeyDown={handleKeyDown}
* onBlur={handleBlur}
* />
* ```
*/
export function useTextHistory({
blockId,
subBlockId,
value,
onChange,
disabled = false,
}: UseTextHistoryOptions): UseTextHistoryResult {
const store = useTextHistoryStore()
const initializedRef = useRef(false)
const lastExternalValueRef = useRef(value)
// Initialize history on mount
useEffect(() => {
if (!initializedRef.current && blockId && subBlockId) {
store.initHistory(blockId, subBlockId, value)
initializedRef.current = true
}
}, [blockId, subBlockId, value, store])
// Handle external value changes (e.g., from AI generation or store sync)
useEffect(() => {
if (value !== lastExternalValueRef.current) {
// This is an external change, commit any pending and record the new value
store.commitPending(blockId, subBlockId)
store.recordChange(blockId, subBlockId, value)
store.commitPending(blockId, subBlockId)
lastExternalValueRef.current = value
}
}, [value, blockId, subBlockId, store])
const handleChange = useCallback(
(newValue: string) => {
if (disabled) return
// Update the external value immediately
onChange(newValue)
lastExternalValueRef.current = newValue
// Record to history with debouncing
store.recordChange(blockId, subBlockId, newValue)
},
[blockId, subBlockId, onChange, disabled, store]
)
const undo = useCallback(() => {
if (disabled) return
const previousValue = store.undo(blockId, subBlockId)
if (previousValue !== null) {
onChange(previousValue)
lastExternalValueRef.current = previousValue
}
}, [blockId, subBlockId, onChange, disabled, store])
const redo = useCallback(() => {
if (disabled) return
const nextValue = store.redo(blockId, subBlockId)
if (nextValue !== null) {
onChange(nextValue)
lastExternalValueRef.current = nextValue
}
}, [blockId, subBlockId, onChange, disabled, store])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent): boolean => {
if (disabled) return false
const isMod = e.metaKey || e.ctrlKey
// Undo: Cmd+Z / Ctrl+Z
if (isMod && e.key === 'z' && !e.shiftKey) {
e.preventDefault()
e.stopPropagation()
undo()
return true
}
// Redo: Cmd+Shift+Z / Ctrl+Shift+Z / Ctrl+Y
if (
(isMod && e.key === 'z' && e.shiftKey) ||
(isMod && e.key === 'Z') ||
(e.ctrlKey && e.key === 'y')
) {
e.preventDefault()
e.stopPropagation()
redo()
return true
}
return false
},
[disabled, undo, redo]
)
const handleBlur = useCallback(() => {
// Commit any pending changes when the field loses focus
store.commitPending(blockId, subBlockId)
}, [blockId, subBlockId, store])
const canUndo = store.canUndo(blockId, subBlockId)
const canRedo = store.canRedo(blockId, subBlockId)
return {
handleChange,
handleKeyDown,
handleBlur,
undo,
redo,
canUndo,
canRedo,
}
}

View File

@@ -471,10 +471,8 @@ function groupIterationBlocks(spans: TraceSpan[]): TraceSpan[] {
}
})
// Include loop/parallel spans that have errors (e.g., validation errors that blocked execution)
// These won't have iteration children, so they should appear directly in results
const nonIterationContainerSpans = normalSpans.filter(
(span) => (span.type !== 'parallel' && span.type !== 'loop') || span.status === 'error'
(span) => span.type !== 'parallel' && span.type !== 'loop'
)
if (iterationSpans.length > 0) {

View File

@@ -225,13 +225,6 @@ export function getBlockOutputs(
return getUnifiedStartOutputs(subBlocks)
}
if (blockType === 'human_in_the_loop') {
// For human_in_the_loop, only expose url (inputFormat fields are only available after resume)
return {
url: { type: 'string', description: 'Resume UI URL' },
}
}
if (blockType === 'approval') {
// Start with only url (apiUrl commented out - not accessible as output)
const pauseResumeOutputs: Record<string, any> = {

View File

@@ -1 +0,0 @@
export { useTextHistoryStore } from './store'

View File

@@ -1,339 +0,0 @@
import { create } from 'zustand'
import { createLogger } from '@/lib/logs/console/logger'
const logger = createLogger('TextHistoryStore')
/**
* Default debounce delay in milliseconds.
* Changes within this window are coalesced into a single history entry.
*/
const DEBOUNCE_DELAY_MS = 500
/**
* Maximum number of history entries per text field.
*/
const MAX_HISTORY_SIZE = 10
interface TextHistoryEntry {
/** The undo/redo stack of text values */
stack: string[]
/** Current position in the stack (0 = oldest) */
index: number
/** Pending value that hasn't been committed to history yet */
pending: string | null
/** Timer ID for debounced commit */
debounceTimer: ReturnType<typeof setTimeout> | null
/** Timestamp of last change (for coalescing logic) */
lastChangeAt: number
}
interface TextHistoryState {
/** Map of "blockId:subBlockId" to history entry */
histories: Record<string, TextHistoryEntry>
/**
* Records a text change with debouncing.
* Multiple rapid changes are coalesced into a single history entry.
*/
recordChange: (blockId: string, subBlockId: string, value: string) => void
/**
* Immediately commits any pending changes to history.
* Call this on blur or before navigation.
*/
commitPending: (blockId: string, subBlockId: string) => void
/**
* Undo the last text change for a specific field.
* @returns The previous value, or null if at the beginning of history
*/
undo: (blockId: string, subBlockId: string) => string | null
/**
* Redo the last undone text change for a specific field.
* @returns The next value, or null if at the end of history
*/
redo: (blockId: string, subBlockId: string) => string | null
/**
* Check if undo is available for a field.
*/
canUndo: (blockId: string, subBlockId: string) => boolean
/**
* Check if redo is available for a field.
*/
canRedo: (blockId: string, subBlockId: string) => boolean
/**
* Initialize history for a field with an initial value.
* Called when a text field first mounts.
*/
initHistory: (blockId: string, subBlockId: string, initialValue: string) => void
/**
* Clear history for a specific field.
*/
clearHistory: (blockId: string, subBlockId: string) => void
/**
* Clear all history for a block (when block is deleted).
*/
clearBlockHistory: (blockId: string) => void
}
function getKey(blockId: string, subBlockId: string): string {
return `${blockId}:${subBlockId}`
}
function createEmptyEntry(initialValue: string): TextHistoryEntry {
return {
stack: [initialValue],
index: 0,
pending: null,
debounceTimer: null,
lastChangeAt: 0,
}
}
export const useTextHistoryStore = create<TextHistoryState>((set, get) => ({
histories: {},
initHistory: (blockId: string, subBlockId: string, initialValue: string) => {
const key = getKey(blockId, subBlockId)
const state = get()
// Only initialize if not already present
if (!state.histories[key]) {
set({
histories: {
...state.histories,
[key]: createEmptyEntry(initialValue),
},
})
logger.debug('Initialized text history', { blockId, subBlockId })
}
},
recordChange: (blockId: string, subBlockId: string, value: string) => {
const key = getKey(blockId, subBlockId)
const state = get()
let entry = state.histories[key]
// Initialize if needed
if (!entry) {
entry = createEmptyEntry('')
}
// Clear any existing debounce timer
if (entry.debounceTimer) {
clearTimeout(entry.debounceTimer)
}
// Set up new debounce timer
const timer = setTimeout(() => {
get().commitPending(blockId, subBlockId)
}, DEBOUNCE_DELAY_MS)
// Update entry with pending value
set({
histories: {
...get().histories,
[key]: {
...entry,
pending: value,
debounceTimer: timer,
lastChangeAt: Date.now(),
},
},
})
},
commitPending: (blockId: string, subBlockId: string) => {
const key = getKey(blockId, subBlockId)
const state = get()
const entry = state.histories[key]
if (!entry || entry.pending === null) {
return
}
// Clear the timer
if (entry.debounceTimer) {
clearTimeout(entry.debounceTimer)
}
const currentValue = entry.stack[entry.index]
// Don't commit if value hasn't changed
if (entry.pending === currentValue) {
set({
histories: {
...state.histories,
[key]: {
...entry,
pending: null,
debounceTimer: null,
},
},
})
return
}
// Truncate any redo history (we're branching)
const newStack = entry.stack.slice(0, entry.index + 1)
// Add the new value
newStack.push(entry.pending)
// Enforce max size (remove oldest entries)
while (newStack.length > MAX_HISTORY_SIZE) {
newStack.shift()
}
const newIndex = newStack.length - 1
set({
histories: {
...state.histories,
[key]: {
stack: newStack,
index: newIndex,
pending: null,
debounceTimer: null,
lastChangeAt: entry.lastChangeAt,
},
},
})
logger.debug('Committed text change to history', {
blockId,
subBlockId,
stackSize: newStack.length,
index: newIndex,
})
},
undo: (blockId: string, subBlockId: string) => {
const key = getKey(blockId, subBlockId)
const state = get()
const entry = state.histories[key]
if (!entry) {
return null
}
// Commit any pending changes first
if (entry.pending !== null) {
get().commitPending(blockId, subBlockId)
// Re-fetch after commit
const updatedEntry = get().histories[key]
if (!updatedEntry || updatedEntry.index <= 0) {
return null
}
const newIndex = updatedEntry.index - 1
set({
histories: {
...get().histories,
[key]: {
...updatedEntry,
index: newIndex,
},
},
})
logger.debug('Text undo', { blockId, subBlockId, newIndex })
return updatedEntry.stack[newIndex]
}
if (entry.index <= 0) {
return null
}
const newIndex = entry.index - 1
set({
histories: {
...state.histories,
[key]: {
...entry,
index: newIndex,
},
},
})
logger.debug('Text undo', { blockId, subBlockId, newIndex })
return entry.stack[newIndex]
},
redo: (blockId: string, subBlockId: string) => {
const key = getKey(blockId, subBlockId)
const state = get()
const entry = state.histories[key]
if (!entry || entry.index >= entry.stack.length - 1) {
return null
}
const newIndex = entry.index + 1
set({
histories: {
...state.histories,
[key]: {
...entry,
index: newIndex,
},
},
})
logger.debug('Text redo', { blockId, subBlockId, newIndex })
return entry.stack[newIndex]
},
canUndo: (blockId: string, subBlockId: string) => {
const key = getKey(blockId, subBlockId)
const entry = get().histories[key]
if (!entry) return false
// Can undo if we have pending changes or index > 0
return entry.pending !== null || entry.index > 0
},
canRedo: (blockId: string, subBlockId: string) => {
const key = getKey(blockId, subBlockId)
const entry = get().histories[key]
if (!entry) return false
return entry.index < entry.stack.length - 1
},
clearHistory: (blockId: string, subBlockId: string) => {
const key = getKey(blockId, subBlockId)
const state = get()
const entry = state.histories[key]
if (entry?.debounceTimer) {
clearTimeout(entry.debounceTimer)
}
const { [key]: _, ...rest } = state.histories
set({ histories: rest })
logger.debug('Cleared text history', { blockId, subBlockId })
},
clearBlockHistory: (blockId: string) => {
const state = get()
const prefix = `${blockId}:`
const newHistories: Record<string, TextHistoryEntry> = {}
for (const [key, entry] of Object.entries(state.histories)) {
if (key.startsWith(prefix)) {
if (entry.debounceTimer) {
clearTimeout(entry.debounceTimer)
}
} else {
newHistories[key] = entry
}
}
set({ histories: newHistories })
logger.debug('Cleared all text history for block', { blockId })
},
}))

View File

@@ -1,217 +0,0 @@
import { getJiraCloudId } from '@/tools/jira/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
export interface JiraGetUsersParams {
accessToken: string
domain: string
accountId?: string
startAt?: number
maxResults?: number
cloudId?: string
}
export interface JiraUser {
accountId: string
accountType?: string
active: boolean
displayName: string
emailAddress?: string
avatarUrls?: {
'16x16'?: string
'24x24'?: string
'32x32'?: string
'48x48'?: string
}
timeZone?: string
self?: string
}
export interface JiraGetUsersResponse extends ToolResponse {
output: {
ts: string
users: JiraUser[]
total?: number
startAt?: number
maxResults?: number
}
}
export const jiraGetUsersTool: ToolConfig<JiraGetUsersParams, JiraGetUsersResponse> = {
id: 'jira_get_users',
name: 'Jira Get Users',
description:
'Get Jira users. If an account ID is provided, returns a single user. Otherwise, returns a list of all users.',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira',
},
domain: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
accountId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Optional account ID to get a specific user. If not provided, returns all users.',
},
startAt: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'The index of the first user to return (for pagination, default: 0)',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum number of users to return (default: 50)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description:
'Jira Cloud ID for the instance. If not provided, it will be fetched using the domain.',
},
},
request: {
url: (params: JiraGetUsersParams) => {
if (params.cloudId) {
if (params.accountId) {
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/user?accountId=${encodeURIComponent(params.accountId)}`
}
const queryParams = new URLSearchParams()
if (params.startAt !== undefined) queryParams.append('startAt', String(params.startAt))
if (params.maxResults !== undefined)
queryParams.append('maxResults', String(params.maxResults))
const queryString = queryParams.toString()
return `https://api.atlassian.com/ex/jira/${params.cloudId}/rest/api/3/users/search${queryString ? `?${queryString}` : ''}`
}
return 'https://api.atlassian.com/oauth/token/accessible-resources'
},
method: 'GET',
headers: (params: JiraGetUsersParams) => ({
Accept: 'application/json',
Authorization: `Bearer ${params.accessToken}`,
}),
},
transformResponse: async (response: Response, params?: JiraGetUsersParams) => {
if (!params?.cloudId) {
const cloudId = await getJiraCloudId(params!.domain, params!.accessToken)
let usersUrl: string
if (params!.accountId) {
usersUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/user?accountId=${encodeURIComponent(params!.accountId)}`
} else {
const queryParams = new URLSearchParams()
if (params!.startAt !== undefined) queryParams.append('startAt', String(params!.startAt))
if (params!.maxResults !== undefined)
queryParams.append('maxResults', String(params!.maxResults))
const queryString = queryParams.toString()
usersUrl = `https://api.atlassian.com/ex/jira/${cloudId}/rest/api/3/users/search${queryString ? `?${queryString}` : ''}`
}
const usersResponse = await fetch(usersUrl, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${params!.accessToken}`,
},
})
if (!usersResponse.ok) {
let message = `Failed to get Jira users (${usersResponse.status})`
try {
const err = await usersResponse.json()
message = err?.errorMessages?.join(', ') || err?.message || message
} catch (_e) {}
throw new Error(message)
}
const data = await usersResponse.json()
const users = params!.accountId ? [data] : data
return {
success: true,
output: {
ts: new Date().toISOString(),
users: users.map((user: any) => ({
accountId: user.accountId,
accountType: user.accountType,
active: user.active,
displayName: user.displayName,
emailAddress: user.emailAddress,
avatarUrls: user.avatarUrls,
timeZone: user.timeZone,
self: user.self,
})),
total: params!.accountId ? 1 : users.length,
startAt: params!.startAt || 0,
maxResults: params!.maxResults || 50,
},
}
}
if (!response.ok) {
let message = `Failed to get Jira users (${response.status})`
try {
const err = await response.json()
message = err?.errorMessages?.join(', ') || err?.message || message
} catch (_e) {}
throw new Error(message)
}
const data = await response.json()
const users = params?.accountId ? [data] : data
return {
success: true,
output: {
ts: new Date().toISOString(),
users: users.map((user: any) => ({
accountId: user.accountId,
accountType: user.accountType,
active: user.active,
displayName: user.displayName,
emailAddress: user.emailAddress,
avatarUrls: user.avatarUrls,
timeZone: user.timeZone,
self: user.self,
})),
total: params?.accountId ? 1 : users.length,
startAt: params?.startAt || 0,
maxResults: params?.maxResults || 50,
},
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
users: {
type: 'json',
description:
'Array of users with accountId, displayName, emailAddress, active status, and avatarUrls',
},
total: { type: 'number', description: 'Total number of users returned' },
startAt: { type: 'number', description: 'Pagination start index' },
maxResults: { type: 'number', description: 'Maximum results per page' },
},
}

View File

@@ -11,7 +11,6 @@ import { jiraDeleteIssueLinkTool } from '@/tools/jira/delete_issue_link'
import { jiraDeleteWorklogTool } from '@/tools/jira/delete_worklog'
import { jiraGetAttachmentsTool } from '@/tools/jira/get_attachments'
import { jiraGetCommentsTool } from '@/tools/jira/get_comments'
import { jiraGetUsersTool } from '@/tools/jira/get_users'
import { jiraGetWorklogsTool } from '@/tools/jira/get_worklogs'
import { jiraRemoveWatcherTool } from '@/tools/jira/remove_watcher'
import { jiraRetrieveTool } from '@/tools/jira/retrieve'
@@ -45,5 +44,4 @@ export {
jiraDeleteIssueLinkTool,
jiraAddWatcherTool,
jiraRemoveWatcherTool,
jiraGetUsersTool,
}

View File

@@ -69,12 +69,6 @@ export interface JiraWriteParams {
cloudId?: string
issueType: string
parent?: { key: string }
labels?: string[]
duedate?: string
reporter?: string
environment?: string
customFieldId?: string
customFieldValue?: string
}
export interface JiraWriteResponse extends ToolResponse {

View File

@@ -46,14 +46,14 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
priority: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Priority ID or name for the issue (e.g., "10000" or "High")',
visibility: 'hidden',
description: 'Priority for the issue',
},
assignee: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Assignee account ID for the issue',
visibility: 'hidden',
description: 'Assignee for the issue',
},
cloudId: {
type: 'string',
@@ -68,42 +68,6 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
visibility: 'hidden',
description: 'Type of issue to create (e.g., Task, Story)',
},
labels: {
type: 'array',
required: false,
visibility: 'user-or-llm',
description: 'Labels for the issue (array of label names)',
},
duedate: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Due date for the issue (format: YYYY-MM-DD)',
},
reporter: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Reporter account ID for the issue',
},
environment: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Environment information for the issue',
},
customFieldId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Custom field ID (e.g., customfield_10001)',
},
customFieldValue: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Value for the custom field',
},
},
request: {
@@ -125,12 +89,6 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
cloudId: params.cloudId,
issueType: params.issueType,
parent: params.parent,
labels: params.labels,
duedate: params.duedate,
reporter: params.reporter,
environment: params.environment,
customFieldId: params.customFieldId,
customFieldValue: params.customFieldValue,
}
},
},
@@ -176,6 +134,5 @@ export const jiraWriteTool: ToolConfig<JiraWriteParams, JiraWriteResponse> = {
issueKey: { type: 'string', description: 'Created issue key (e.g., PROJ-123)' },
summary: { type: 'string', description: 'Issue summary' },
url: { type: 'string', description: 'URL to the created issue' },
assigneeId: { type: 'string', description: 'Account ID of the assigned user (if assigned)' },
},
}

View File

@@ -463,7 +463,6 @@ import {
jiraDeleteWorklogTool,
jiraGetAttachmentsTool,
jiraGetCommentsTool,
jiraGetUsersTool,
jiraGetWorklogsTool,
jiraRemoveWatcherTool,
jiraRetrieveTool,
@@ -1479,7 +1478,6 @@ export const tools: Record<string, ToolConfig> = {
jira_delete_issue_link: jiraDeleteIssueLinkTool,
jira_add_watcher: jiraAddWatcherTool,
jira_remove_watcher: jiraRemoveWatcherTool,
jira_get_users: jiraGetUsersTool,
kalshi_get_markets: kalshiGetMarketsTool,
kalshi_get_market: kalshiGetMarketTool,
kalshi_get_events: kalshiGetEventsTool,

View File

@@ -110,15 +110,10 @@ export const slackListChannelsTool: ToolConfig<SlackListChannelsParams, SlackLis
creator: channel.creator,
}))
const ids = channels.map((channel: { id: string }) => channel.id)
const names = channels.map((channel: { name: string }) => channel.name)
return {
success: true,
output: {
channels,
ids,
names,
count: channels.length,
},
}
@@ -147,14 +142,6 @@ export const slackListChannelsTool: ToolConfig<SlackListChannelsParams, SlackLis
},
},
},
ids: {
type: 'array',
description: 'Array of channel IDs for easy access',
},
names: {
type: 'array',
description: 'Array of channel names for easy access',
},
count: {
type: 'number',
description: 'Total number of channels returned',

View File

@@ -102,15 +102,10 @@ export const slackListUsersTool: ToolConfig<SlackListUsersParams, SlackListUsers
status_emoji: user.profile?.status_emoji || '',
}))
const ids = users.map((user: { id: string }) => user.id)
const names = users.map((user: { name: string }) => user.name)
return {
success: true,
output: {
users,
ids,
names,
count: users.length,
},
}
@@ -138,14 +133,6 @@ export const slackListUsersTool: ToolConfig<SlackListUsersParams, SlackListUsers
},
},
},
ids: {
type: 'array',
description: 'Array of user IDs for easy access',
},
names: {
type: 'array',
description: 'Array of usernames for easy access',
},
count: {
type: 'number',
description: 'Total number of users returned',

View File

@@ -245,8 +245,6 @@ export interface SlackChannel {
export interface SlackListChannelsResponse extends ToolResponse {
output: {
channels: SlackChannel[]
ids: string[]
names: string[]
count: number
}
}
@@ -293,8 +291,6 @@ export interface SlackUser {
export interface SlackListUsersResponse extends ToolResponse {
output: {
users: SlackUser[]
ids: string[]
names: string[]
count: number
}
}