diff --git a/assets/design-system/src/utils/urls.ts b/assets/design-system/src/utils/urls.ts index 00454fef22..f517a0b820 100644 --- a/assets/design-system/src/utils/urls.ts +++ b/assets/design-system/src/utils/urls.ts @@ -59,15 +59,19 @@ export function isValidRepoUrl(url: string): boolean { return false } -export function prettifyRepoUrl(repoUrl: string) { - const [owner, name] = repoUrl +export function prettifyRepoUrl( + repoUrl: string, + showFullPath: boolean = false +) { + const fullPath = repoUrl .trim() .replace(/^git@[^:]+:/, '') .replace(/^ssh:\/\/[^/]+\/?/, '') .replace(/^https?:\/\/[^/]+\/?/, '') .replace(/^\/+/, '') .replace(/\.git$/, '') - .split('/') - return owner && name ? `${owner}/${name}` : repoUrl + const [owner, name] = fullPath.split('/') + + return showFullPath ? fullPath : owner && name ? `${owner}/${name}` : repoUrl } diff --git a/assets/public/setup-guides/webhooks/asana.md b/assets/public/setup-guides/webhooks/asana.md new file mode 100644 index 0000000000..8be2f9b1b6 --- /dev/null +++ b/assets/public/setup-guides/webhooks/asana.md @@ -0,0 +1,54 @@ +# Asana Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in Asana against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts, new tickets, and workflow updates. + +## 1. Create the webhook in Plural + +In Plural, go to the webhook creation flow and choose: + +- Type: Ticketing +- Provider: ASANA + +Fill in: + +- Name: a clear name like Asana Production Tickets +- URL: the callback URL Plural gives you +- Secret: a strong shared secret + +Save the webhook in Plural. + +## 2. Register URL and secret in Asana + +In Asana, open your app or integration settings and create a webhook subscription: + +- Target URL: paste the Plural webhook URL +- Secret/token field: paste the same secret from Plural + +If Asana requires signing secrets per endpoint, ensure the secret is bound to this exact URL. + +## 3. Configure triggers to send events to Plural + +Enable triggers that map to operational ticketing activity: + +- New task/ticket created +- Task moved to incident/alerts project +- Critical priority changes +- Status transitions for incident-related work + +Prefer project-level or workspace-level filters so only relevant events are sent. + +## 4. Validate delivery + +Trigger a test event in Asana (for example create a test ticket) and verify in Plural: + +- request is received +- signature/secret validation succeeds +- event appears in webhook activity + +## 5. Troubleshooting + +If events fail: + +- confirm URL exactly matches the Plural endpoint +- rotate and re-enter the secret on both sides +- ensure the selected project/workspace scope includes the event source diff --git a/assets/public/setup-guides/webhooks/datadog.md b/assets/public/setup-guides/webhooks/datadog.md new file mode 100644 index 0000000000..ee3cc50282 --- /dev/null +++ b/assets/public/setup-guides/webhooks/datadog.md @@ -0,0 +1,42 @@ +# Datadog Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in Datadog against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts and monitor state changes. + +## 1. Create the webhook in Plural + +In Plural configure: + +- Type: Observability +- Provider: DATADOG +- Name: for example Datadog Production Alerts +- Secret: shared secret used for verification + +Plural stores the secret and gives you the target URL (if required by your flow). + +## 2. Register URL and secret in Datadog + +In Datadog integrations/webhooks: + +- Target URL: set to the Plural webhook URL +- Secret/signing token: set to the same secret from Plural + +If custom headers are required, keep them aligned with your Plural validation policy. + +## 3. Configure alert triggers + +Enable triggers for relevant alert lifecycle events: + +- monitor alert triggered +- monitor recovered +- incident state changes +- high-priority signal transitions + +Scope notifications to production monitors or incident-critical tags. + +## 4. Validate delivery + +Trigger a test monitor alert in Datadog and verify in Plural: + +- event received and authenticated +- payload parsed +- trigger evaluation sees alert state correctly diff --git a/assets/public/setup-guides/webhooks/github.md b/assets/public/setup-guides/webhooks/github.md new file mode 100644 index 0000000000..d4b26e2387 --- /dev/null +++ b/assets/public/setup-guides/webhooks/github.md @@ -0,0 +1,47 @@ +# GitHub Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in GitHub against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts, new tickets, and issue lifecycle updates. + +## 1. Create the webhook in Plural + +Create a new webhook in Plural with: + +- Type: Ticketing +- Provider: GITHUB +- URL: Plural callback URL +- Secret: shared secret used for signature verification + +## 2. Register URL and secret in GitHub + +In GitHub repository or organization settings: + +- go to Webhooks +- click Add webhook +- Payload URL: paste the Plural URL +- Secret: paste the same secret configured in Plural +- Content type: application/json + +## 3. Configure GitHub events + +Select events relevant to ticketing and alerts, for example: + +- Issues (opened, edited, closed, reopened) +- Issue comments +- Label changes +- Projects if your team routes alert tickets through boards + +Use either individual events or Send me everything only in non-production environments. + +## 4. Test and verify + +Use GitHub's ping/test delivery and also create a test issue. In Plural, verify: + +- delivery succeeded (2xx) +- event parsed successfully +- issue/ticket event appears in webhook history + +## 5. Security recommendations + +- use a unique secret per webhook +- rotate periodically +- restrict admin rights for webhook management diff --git a/assets/public/setup-guides/webhooks/gitlab.md b/assets/public/setup-guides/webhooks/gitlab.md new file mode 100644 index 0000000000..800118c8b8 --- /dev/null +++ b/assets/public/setup-guides/webhooks/gitlab.md @@ -0,0 +1,46 @@ +# GitLab Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in GitLab against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts, new tickets, and incident issue updates. + +## 1. Create the webhook in Plural + +In Plural create: + +- Type: Ticketing +- Provider: GITLAB +- URL: incoming webhook URL from Plural +- Secret: shared secret + +## 2. Register URL and secret in GitLab + +In GitLab project/group settings under Webhooks: + +- URL: paste the Plural URL +- Secret token: paste the same secret from Plural +- SSL verification: keep enabled unless your environment requires otherwise + +## 3. Configure GitLab triggers + +Enable events that correspond to ticket lifecycle: + +- Issues events +- Notes/comments events (optional) +- Labels changes (if used for severity) +- Confidential issue updates where applicable + +For alert-driven workflows, scope to projects where alert tickets are created. + +## 4. Test the integration + +Use GitLab Test webhook for Issues events and then open a real test issue. + +Check Plural for: + +- successful request validation +- processed event payload +- activity logs with expected event type + +## 5. Operational tips + +- keep separate webhooks for staging and production projects +- apply narrow event filters to reduce noise diff --git a/assets/public/setup-guides/webhooks/grafana.md b/assets/public/setup-guides/webhooks/grafana.md new file mode 100644 index 0000000000..2360bca725 --- /dev/null +++ b/assets/public/setup-guides/webhooks/grafana.md @@ -0,0 +1,41 @@ +# Grafana Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in Grafana against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts and alert rule state transitions. + +## 1. Create the webhook in Plural + +Create a webhook in Plural with: + +- Type: Observability +- Provider: GRAFANA +- Secret: shared webhook secret + +Use the URL provided by Plural for incoming events. + +## 2. Register URL and secret in Grafana + +In Grafana Alerting contact points: + +- Contact point type: Webhook +- URL: paste the Plural webhook URL +- Secret/signature key: paste the Plural secret + +Set optional fields (headers, auth) only if your policy requires them. + +## 3. Configure Grafana triggers + +Bind the contact point to notification policies and routes for: + +- firing alerts +- resolved alerts +- selected folders/labels for incident-relevant rules + +Route only high-signal alerts to avoid noisy webhook traffic. + +## 4. Validate integration + +Use Grafana test notification and a real alert simulation. In Plural verify: + +- request accepted +- signature check passed +- firing and resolved events are visible diff --git a/assets/public/setup-guides/webhooks/jira.md b/assets/public/setup-guides/webhooks/jira.md new file mode 100644 index 0000000000..cbfc2857d8 --- /dev/null +++ b/assets/public/setup-guides/webhooks/jira.md @@ -0,0 +1,45 @@ +# Jira Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in Jira against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts, new tickets, and issue transitions. + +## 1. Create the webhook in Plural + +Configure a webhook in Plural with: + +- Type: Ticketing +- Provider: JIRA +- URL: Plural callback endpoint +- Secret: shared signing secret + +## 2. Register URL and secret in Jira + +In Jira admin settings for webhooks: + +- Endpoint URL: paste Plural URL +- Secret or authentication field: paste the Plural secret (if available in your Jira deployment) +- Authentication: configure additional auth headers only if your policy requires it + +## 3. Configure Jira trigger events + +Enable events commonly used for alert workflows: + +- Issue created +- Issue updated +- Issue transitioned +- Issue reopened/resolved + +If supported, apply JQL filter to include only incident/alert projects and severities. + +## 4. Validate end-to-end + +Create and transition a test issue in Jira. In Plural confirm: + +- webhook accepted +- signature/secret check passed +- trigger appears with expected metadata + +## 5. Best practices + +- keep dedicated Jira webhook per environment +- document JQL filters with your on-call process +- rotate secret after team ownership changes diff --git a/assets/public/setup-guides/webhooks/linear.md b/assets/public/setup-guides/webhooks/linear.md new file mode 100644 index 0000000000..2d817a5d7d --- /dev/null +++ b/assets/public/setup-guides/webhooks/linear.md @@ -0,0 +1,45 @@ +# Linear Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in Linear against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts, new tickets, and issue status updates. + +## 1. Create the webhook in Plural + +In Plural, create webhook: + +- Type: Ticketing +- Provider: LINEAR +- URL: Plural endpoint URL +- Secret: shared secret for request verification + +## 2. Register URL and secret in Linear + +In Linear workspace settings under API/Webhooks: + +- Webhook URL: paste Plural URL +- Secret/signing key: paste same secret from Plural + +If Linear signs payloads, make sure Plural validates against this exact secret. + +## 3. Configure event subscriptions + +Enable the events that should drive Plural automation: + +- Issue created (new tickets) +- Issue updated +- Priority changed +- State changed (triaged/in progress/done) + +Use team-based filters if only certain teams should send webhook data. + +## 4. Verify integration + +Create a test issue and transition its state. In Plural verify: + +- request accepted +- event recorded +- downstream automation or trigger evaluation runs correctly + +## 5. Maintenance + +- keep webhook ownership documented in your runbook +- rotate secrets on schedule diff --git a/assets/public/setup-guides/webhooks/newrelic.md b/assets/public/setup-guides/webhooks/newrelic.md new file mode 100644 index 0000000000..f638f55b17 --- /dev/null +++ b/assets/public/setup-guides/webhooks/newrelic.md @@ -0,0 +1,42 @@ +# New Relic Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in New Relic against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts and incident lifecycle changes. + +## 1. Create the webhook in Plural + +Create webhook details in Plural: + +- Type: Observability +- Provider: NEWRELIC +- Secret: shared secret for verification + +Copy the Plural webhook URL. + +## 2. Register URL and secret in New Relic + +In New Relic workflows/destinations: + +- Destination type: webhook +- URL: Plural webhook URL +- Secret/signing credential: same secret as in Plural + +Ensure destination is enabled and reachable from New Relic. + +## 3. Configure trigger workflows + +Attach destination to workflows with conditions such as: + +- incident created/opened +- incident acknowledged +- incident closed +- policy/condition severity changes + +Filter by account, policy, or tags to keep routing precise. + +## 4. Test and verify + +Send a test payload from New Relic and open a test incident. In Plural verify: + +- authenticated delivery +- expected incident events captured +- trigger automation reacts to open/close events diff --git a/assets/public/setup-guides/webhooks/pagerduty.md b/assets/public/setup-guides/webhooks/pagerduty.md new file mode 100644 index 0000000000..f31928e0e0 --- /dev/null +++ b/assets/public/setup-guides/webhooks/pagerduty.md @@ -0,0 +1,41 @@ +# PagerDuty Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in PagerDuty against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts, incidents, and escalation lifecycle changes. + +## 1. Create the webhook in Plural + +Configure webhook in Plural: + +- Type: Observability +- Provider: PAGERDUTY +- Secret: shared secret + +Copy the Plural webhook URL. + +## 2. Register URL and secret in PagerDuty + +In PagerDuty extensions/webhooks or event orchestration actions: + +- URL: Plural webhook endpoint +- Secret/signing token: same value as Plural secret + +Choose the service(s) tied to your incident flow. + +## 3. Configure incident triggers + +Enable/route events such as: + +- incident triggered +- incident acknowledged +- incident resolved +- escalation events (if needed) + +Prefer service-level filtering so only relevant incidents are sent. + +## 4. Validate end-to-end + +Create a test incident in PagerDuty. Confirm in Plural: + +- event accepted +- secret validated +- incident state updates reflected diff --git a/assets/public/setup-guides/webhooks/plural.md b/assets/public/setup-guides/webhooks/plural.md new file mode 100644 index 0000000000..0e7e44a5eb --- /dev/null +++ b/assets/public/setup-guides/webhooks/plural.md @@ -0,0 +1,40 @@ +# Plural-to-Plural Webhook Setup + +Generate markdown documentation for creating a webhook in Plural against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts and internal event streams. + +## 1. Create the receiving webhook in target Plural + +In the target Plural environment create: + +- Type: Observability +- Provider: PLURAL +- Secret: shared secret + +Copy the receiving URL. + +## 2. Register destination URL and secret in source Plural + +In the source Plural environment configure outbound webhook destination: + +- URL: target Plural webhook URL +- Secret: same secret as target receiver + +This ensures the receiving environment can validate authenticity. + +## 3. Configure source triggers + +Enable event routing for the signals you want to federate: + +- alert raised +- alert resolved +- workflow or policy events relevant to operations + +Apply project/environment filters to avoid duplicate or noisy events. + +## 4. Validate + +Send a controlled test event from source Plural and verify target Plural: + +- receives event +- validates secret +- records event under webhook activity diff --git a/assets/public/setup-guides/webhooks/sentry.md b/assets/public/setup-guides/webhooks/sentry.md new file mode 100644 index 0000000000..96f4f5b462 --- /dev/null +++ b/assets/public/setup-guides/webhooks/sentry.md @@ -0,0 +1,41 @@ +# Sentry Webhook Setup for Plural + +Generate markdown documentation for creating a webhook in Sentry against plural. Plural allows a webhook to have a url and secret, you need to explain to the user how to register those, and how to configure the appropriate triggers so plural can receive events from alerts, issue lifecycle updates, and regressions. + +## 1. Create the webhook in Plural + +Create webhook details in Plural: + +- Type: Observability +- Provider: SENTRY +- Secret: shared secret used for request verification + +Copy the Plural webhook URL. + +## 2. Register URL and secret in Sentry + +In Sentry project/organization integrations for webhooks: + +- URL: Plural webhook endpoint +- Secret/signing key: same secret from Plural + +Bind the integration to the right projects. + +## 3. Configure Sentry triggers + +Enable event categories for operational signal flow: + +- issue created +- issue regressed +- issue resolved +- alert rule fired/resolved (if configured) + +Filter by project and severity to keep event volume relevant. + +## 4. Validate and monitor + +Trigger a test error and resolve/regress it. Confirm in Plural: + +- events arrive with valid signature +- lifecycle changes are represented correctly +- downstream automations can key off these events diff --git a/assets/src/components/ai/AIContext.tsx b/assets/src/components/ai/AIContext.tsx index fb3c6bc43d..b7e4b5ebe2 100644 --- a/assets/src/components/ai/AIContext.tsx +++ b/assets/src/components/ai/AIContext.tsx @@ -28,6 +28,10 @@ import { useTheme } from 'styled-components' import usePersistedState from '../hooks/usePersistedState.tsx' import { SidebarContext } from 'components/layout/Sidebar.tsx' import { POLL_INTERVAL } from 'components/cd/ContinuousDeployment.tsx' +import { + type SidePanel, + useTopLevelSidePanel, +} from 'components/layout/TopLevelSidePanel.tsx' export enum AIVerbosityLevel { High = 'High', @@ -121,10 +125,13 @@ export function AIContextProvider({ children }: { children: ReactNode }) { ) } +const SIDE_PANEL_TYPE: SidePanel = 'ai-chat' + function ChatbotContextProvider({ children }: { children: ReactNode }) { const { spacing } = useTheme() const { setIsExpanded: setSidebarExpanded } = use(SidebarContext) - const [open, setOpenState] = usePersistedState('plural-ai-chat-open', false) + const { sidePanel, setSidePanel } = useTopLevelSidePanel() + const open = sidePanel === SIDE_PANEL_TYPE const [viewType, setViewType] = usePersistedState( 'plural-ai-view-type', AIViewTypes.ChatThread @@ -198,8 +205,10 @@ function ChatbotContextProvider({ children }: { children: ReactNode }) { value={{ open, setOpen: (open) => { - setOpenState(open) - if (open) setSidebarExpanded(false) + if (open) { + setSidePanel(SIDE_PANEL_TYPE) + setSidebarExpanded(false) + } else setSidePanel(null) }, currentThread, currentThreadLoading, diff --git a/assets/src/components/ai/agent-runs/AgentRunFixButton.tsx b/assets/src/components/ai/agent-runs/AgentRunFixButton.tsx index ad9b0d4119..b7e515926c 100644 --- a/assets/src/components/ai/agent-runs/AgentRunFixButton.tsx +++ b/assets/src/components/ai/agent-runs/AgentRunFixButton.tsx @@ -22,7 +22,7 @@ import { FillLevelDiv } from 'components/utils/FillLevelDiv' import { StretchedFlex } from 'components/utils/StretchedFlex' import { StackedText } from 'components/utils/table/StackedText' import { InlineLink } from 'components/utils/typography/InlineLink' -import { Body2BoldP } from 'components/utils/typography/Text' +import { Body2BoldP, CaptionP } from 'components/utils/typography/Text' import { AgentRunFragment, AgentRunMode, @@ -37,6 +37,8 @@ import { AI_SETTINGS_AGENT_RUNTIMES_ABS_PATH } from 'routes/settingsRoutesConst' import styled, { useTheme } from 'styled-components' import { AIAgentRuntimesSelector } from './AIAgentRuntimesSelector' import { AgentRunRepoSelector } from './AgentRunRepoSelector' +import { TRUNCATE } from 'components/utils/truncate' +import { formatDateTime } from 'utils/datetime' export function AgentRunFixButton({ headerTitle, @@ -209,14 +211,15 @@ function AgentRunForm({ } export function AgentRunInfoCard({ - agentRun: { id, status }, + agentRun, showLinkButton = false, ...props }: { - agentRun: AgentRunFragment + agentRun: Nullable showLinkButton?: boolean } & CardProps) { const { colors } = useTheme() + const { id = '', status, prompt, insertedAt, updatedAt } = agentRun ?? {} const isRunning = status === AgentRunStatus.Running || status === AgentRunStatus.Pending const { data } = useAgentRunTinyQuery({ @@ -225,37 +228,69 @@ export function AgentRunInfoCard({ fetchPolicy: 'cache-and-network', pollInterval: 5000, }) + if (!agentRun) return null return ( - - + + {status === AgentRunStatus.Successful + ? 'Run complete' + : 'Started agent run'} + + } + icon={ + + } + /> + } /> - - {status === AgentRunStatus.Successful - ? 'Run complete' - : 'Started agent run'} - + + {showLinkButton && ( + + )} - - {showLinkButton && ( - - )} + + {prompt} + + + + Start time{' '} + + {formatDateTime(insertedAt)} + + + {!isRunning && ( + + End time{' '} + + {formatDateTime(updatedAt)} + + + )} + ) } @@ -279,8 +314,8 @@ export const PromptInputBoxSC = styled(Card)(({ theme }) => ({ const AgentRunStatusBoxSC = styled(Card)(({ theme }) => ({ display: 'flex', - alignItems: 'center', - gap: theme.spacing.small, + flexDirection: 'column', + gap: theme.spacing.medium, justifyContent: 'space-between', padding: theme.spacing.medium, width: '100%', diff --git a/assets/src/components/ai/chatbot/Chatbot.tsx b/assets/src/components/ai/chatbot/Chatbot.tsx index ce40e48060..c6a119d528 100644 --- a/assets/src/components/ai/chatbot/Chatbot.tsx +++ b/assets/src/components/ai/chatbot/Chatbot.tsx @@ -1,9 +1,8 @@ -import { Accordion, AccordionItem, usePrevious } from '@pluralsh/design-system' +import { usePrevious } from '@pluralsh/design-system' import { useDeploymentSettings } from 'components/contexts/DeploymentSettingsContext.tsx' import { isEmpty } from 'lodash' import { useEffect, useMemo } from 'react' -import styled from 'styled-components' import { useChatThreadMessagesQuery } from '../../../generated/graphql.ts' import { mapExistingNodes } from '../../../utils/graphql.ts' import { useFetchPaginatedData } from '../../utils/table/useFetchPaginatedData.tsx' @@ -15,13 +14,8 @@ import { MainChatbotButton } from './ChatbotButton.tsx' import { ChatbotHeader } from './ChatbotHeader.tsx' import { ChatbotPanelThread } from './ChatbotPanelThread.tsx' import { McpServerShelf } from './tools/McpServerShelf.tsx' -import { useResizablePane } from './useResizeableChatPane.tsx' import { ChatbotPanelInfraResearch } from './ChatbotPanelInfraResearch.tsx' - -const MIN_WIDTH = 500 -const MAX_WIDTH_VW = 40 -const HANDLE_THICKNESS = 20 -export const CHATBOT_HEADER_HEIGHT = 57 +import { SidePanelContent } from './SidePanelShared.tsx' export function ChatbotLauncher() { const { open, setOpen } = useChatbotContext() @@ -32,30 +26,7 @@ export function ChatbotLauncher() { return setOpen(true)} /> } -export function ChatbotPanel() { - const { open } = useChatbotContext() - return ( - - - - - - ) -} - -function ChatbotPanelInner() { +export function ChatbotPanelContent() { const { currentThread, currentThreadId, agentInitMode, viewType } = useChatbot() const { data, loading, error, fetchNextPage, pageInfo, refetch } = @@ -83,14 +54,8 @@ function ChatbotPanelInner() { [currentThreadId, data?.chatThread?.chats] ) - const { calculatedPanelWidth, dragHandleProps, isDragging } = - useResizablePane(MIN_WIDTH, MAX_WIDTH_VW) - return ( -
+ <> {!isEmpty(currentThread?.tools) && } {currentThread?.session && !agentInitMode && ( )} - - + {!!agentInitMode ? ( @@ -117,66 +81,7 @@ function ChatbotPanelInner() { hasNextPage={pageInfo?.hasNextPage} /> )} - - -
+ + ) } - -const MainContentWrapperSC = styled.div(({ theme }) => ({ - position: 'relative', - zIndex: theme.zIndexes.modal, - display: 'flex', - flexDirection: 'column', - height: '100%', - width: 'var(--chatbot-panel-width)', - borderLeft: theme.borders.default, - background: theme.colors['fill-accent'], -})) - -const ResizeGripSC = styled.div(({ theme }) => ({ - borderLeft: theme.borders.default, - height: 40, - left: 2, - position: 'absolute', - top: 'calc(50% - 20px)', - width: 5, - - '&:after': { - borderLeft: theme.borders.default, - content: '""', - height: 30, - left: 2, - position: 'absolute', - top: 'calc(50% - 15px)', - width: 5, - }, -})) - -const DragHandleSC = styled.div<{ $isDragging: boolean }>( - ({ theme, $isDragging }) => ({ - position: 'absolute', - zIndex: theme.zIndexes.modal, - left: -HANDLE_THICKNESS / 2, - top: 0, - width: HANDLE_THICKNESS, - height: '100%', - cursor: 'ew-resize', - background: 'transparent', - display: 'flex', - justifyContent: 'center', - '&:focus-visible': { outline: theme.borders['outline-focused'] }, - // make the part the highlights while dragging a little thinner than full drag area - '&::before': { - content: '""', - pointerEvents: 'none', - width: HANDLE_THICKNESS / 4, - background: $isDragging ? theme.colors['icon-primary'] : 'transparent', - transition: 'background 0.2s ease-in-out', - }, - }) -) diff --git a/assets/src/components/ai/chatbot/ChatbotHeader.tsx b/assets/src/components/ai/chatbot/ChatbotHeader.tsx index 3e1cc55ab1..066f823a12 100644 --- a/assets/src/components/ai/chatbot/ChatbotHeader.tsx +++ b/assets/src/components/ai/chatbot/ChatbotHeader.tsx @@ -27,7 +27,7 @@ import { getResourceLinkPath, TableEntryResourceLink } from '../AITableEntry' import { RunStatusChip } from '../infra-research/details/InfraResearch.tsx' import { AgentSelect } from './AgentSelect.tsx' import { AgentSessionTypeSelect } from './AgentSessionTypeSelect.tsx' -import { CHATBOT_HEADER_HEIGHT } from './Chatbot.tsx' +import { SIDE_PANEL_HEADER_HEIGHT } from './SidePanelShared.tsx' import { ChatbotThreadMoreMenu } from './ChatbotThreadMoreMenu' export function ChatbotHeader() { @@ -180,5 +180,5 @@ const MainHeaderSC = styled.div(({ theme }) => ({ alignItems: 'center', padding: theme.spacing.medium, borderBottom: theme.borders.default, - maxHeight: CHATBOT_HEADER_HEIGHT, + maxHeight: SIDE_PANEL_HEADER_HEIGHT, })) diff --git a/assets/src/components/ai/chatbot/SidePanelShared.tsx b/assets/src/components/ai/chatbot/SidePanelShared.tsx new file mode 100644 index 0000000000..d623ab549e --- /dev/null +++ b/assets/src/components/ai/chatbot/SidePanelShared.tsx @@ -0,0 +1,67 @@ +import { ReactNode } from 'react' +import styled from 'styled-components' + +const HANDLE_THICKNESS = 20 +export const SIDE_PANEL_HEADER_HEIGHT = 57 + +export function SidePanelContent({ children }: { children: ReactNode }) { + return ( + + + {children} + + ) +} + +const SidePanelWrapperSC = styled.div(({ theme }) => ({ + position: 'relative', + zIndex: theme.zIndexes.modal, + display: 'flex', + flexDirection: 'column', + height: '100%', + width: 'var(--side-panel-width)', + borderLeft: theme.borders.default, + background: theme.colors['fill-accent'], +})) + +export const ResizeGripSC = styled.div(({ theme }) => ({ + borderLeft: theme.borders.default, + height: 40, + left: 2, + position: 'absolute', + top: 'calc(50% - 20px)', + width: 5, + + '&:after': { + borderLeft: theme.borders.default, + content: '""', + height: 30, + left: 2, + position: 'absolute', + top: 'calc(50% - 15px)', + width: 5, + }, +})) + +export const DragHandleSC = styled.div<{ $isDragging: boolean }>( + ({ theme, $isDragging }) => ({ + position: 'absolute', + zIndex: theme.zIndexes.modal, + left: -HANDLE_THICKNESS / 2, + top: 0, + width: HANDLE_THICKNESS, + height: '100%', + cursor: 'ew-resize', + background: 'transparent', + display: 'flex', + justifyContent: 'center', + '&:focus-visible': { outline: theme.borders['outline-focused'] }, + '&::before': { + content: '""', + pointerEvents: 'none', + width: HANDLE_THICKNESS / 4, + background: $isDragging ? theme.colors['icon-primary'] : 'transparent', + transition: 'background 0.2s ease-in-out', + }, + }) +) diff --git a/assets/src/components/ai/chatbot/actions-panel/ChatbotActionsPanel.tsx b/assets/src/components/ai/chatbot/actions-panel/ChatbotActionsPanel.tsx index 4684729e4d..af604e75f1 100644 --- a/assets/src/components/ai/chatbot/actions-panel/ChatbotActionsPanel.tsx +++ b/assets/src/components/ai/chatbot/actions-panel/ChatbotActionsPanel.tsx @@ -7,7 +7,7 @@ import { } from 'generated/graphql' import styled, { useTheme } from 'styled-components' import { useChatbot } from '../../AIContext.tsx' -import { CHATBOT_HEADER_HEIGHT } from '../Chatbot' +import { SIDE_PANEL_HEADER_HEIGHT } from '../SidePanelShared' import { ArrowTopRightIcon, @@ -293,7 +293,7 @@ const HeaderSC = styled.div(({ theme }) => ({ justifyContent: 'space-between', borderBottom: theme.borders.default, padding: `0 ${theme.spacing.medium}px`, - minHeight: CHATBOT_HEADER_HEIGHT, + minHeight: SIDE_PANEL_HEADER_HEIGHT, })) export const ActionItemSC = styled.div(({ theme }) => ({ diff --git a/assets/src/components/ai/chatbot/multithread/MultiThreadViewerMessage.tsx b/assets/src/components/ai/chatbot/multithread/MultiThreadViewerMessage.tsx index c3e51d4297..01562e0f2e 100644 --- a/assets/src/components/ai/chatbot/multithread/MultiThreadViewerMessage.tsx +++ b/assets/src/components/ai/chatbot/multithread/MultiThreadViewerMessage.tsx @@ -10,7 +10,7 @@ import { ChatFragment, ChatType } from 'generated/graphql' import { ReactElement, ReactNode, useState } from 'react' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' -import styled from 'styled-components' +import styled, { useTheme } from 'styled-components' import { ToolCallContent } from '../ToolCallContent' export function MultiThreadViewerMessage({ @@ -41,6 +41,7 @@ export function SimpleToolCall({ attributes: ChatFragment['attributes'] isPending?: boolean }) { + const { colors } = useTheme() const [isOpen, setIsOpen] = useState(false) const toolName = attributes?.tool?.name ?? '' @@ -51,7 +52,8 @@ export function SimpleToolCall({ $shimmer={isPending} $color="text-xlight" > - {isPending ? 'CALLING' : 'CALLED'} TOOL {toolName} + {isPending ? 'Calling' : 'Called'} tool{' '} + {toolName} - {label}} - padding="none" - caret="none" + + - - {content} - - - + {content} + + ) return ( @@ -177,6 +169,13 @@ export function SimplifiedMarkdown({ text }: { text: string }) { ol: ({ children }) => {children}, li: ({ children }) =>
  • {children}
  • , hr: () => , + table: ({ children }) => ( + + {children} + + ), + th: ({ children }) => {children}, + td: ({ children }) => {children}, }} > {text} @@ -185,7 +184,34 @@ export function SimplifiedMarkdown({ text }: { text: string }) { ) } -const ClickableLabelSC = styled.button(({ theme }) => ({ +export function SimpleAccordion({ + label, + defaultOpen = false, + children, +}: { + label: ReactNode + defaultOpen?: boolean + children: ReactNode +}) { + return ( + + {label}} + padding="none" + caret="none" + > + {children} + + + ) +} + +export const ClickableLabelSC = styled.button(({ theme }) => ({ background: 'none', border: 'none', padding: 0, @@ -215,6 +241,7 @@ const InlineCodeSC = styled.code(({ theme }) => ({ backgroundColor: theme.colors['fill-two'], padding: `0 ${theme.spacing.xxsmall}px`, borderRadius: theme.borderRadiuses.medium, + wordBreak: 'break-word', })) const ListSC = styled.ul(({ theme }) => ({ @@ -229,3 +256,49 @@ const HrSC = styled.hr(({ theme }) => ({ margin: `${theme.spacing.xsmall}px 0`, width: '100%', })) + +const TableWrapperSC = styled.div(({ theme }) => ({ + paddingTop: theme.spacing.medium, + overflowX: 'auto', + maxWidth: '100%', +})) + +const TableSC = styled.table(() => ({ + borderCollapse: 'separate', + borderSpacing: 0, + minWidth: '100%', + width: 'max-content', +})) + +const ThSC = styled.th(({ theme }) => ({ + padding: theme.spacing.small, + height: 40, + textAlign: 'left', + backgroundColor: theme.colors['fill-one'], + border: theme.borders['fill-two'], + borderBottom: theme.borders.default, + 'tr:first-child &': { + '&:first-child': { borderTopLeftRadius: theme.borderRadiuses.large }, + '&:last-child': { borderTopRightRadius: theme.borderRadiuses.large }, + }, + '&:not(:last-child)': { borderRight: 'none' }, + '&:not(:first-child)': { borderLeft: 'none' }, +})) + +const TdSC = styled.td(({ theme }) => ({ + backgroundColor: theme.colors['fill-zero-selected'], + padding: `${theme.spacing.xsmall}px ${theme.spacing.small}px`, + color: theme.colors['text-light'], + height: 40, + border: theme.borders['fill-two'], + borderBottom: theme.borders.default, + borderTop: 'none', + textAlign: 'left', + 'tr:last-child &': { + borderBottom: theme.borders['fill-two'], + '&:first-child': { borderBottomLeftRadius: theme.borderRadiuses.large }, + '&:last-child': { borderBottomRightRadius: theme.borderRadiuses.large }, + }, + '&:not(:last-child)': { borderRight: 'none' }, + '&:not(:first-child)': { borderLeft: 'none' }, +})) diff --git a/assets/src/components/ai/chatbot/tools/McpServerShelf.tsx b/assets/src/components/ai/chatbot/tools/McpServerShelf.tsx index 29b650f2a3..ac89b9d1bf 100644 --- a/assets/src/components/ai/chatbot/tools/McpServerShelf.tsx +++ b/assets/src/components/ai/chatbot/tools/McpServerShelf.tsx @@ -21,7 +21,7 @@ import { Link } from 'react-router-dom' import { AI_MCP_SERVERS_ABS_PATH } from 'routes/aiRoutesConsts' import styled, { useTheme } from 'styled-components' import { isNonNullable } from 'utils/isNonNullable' -import { CHATBOT_HEADER_HEIGHT } from '../Chatbot' +import { SIDE_PANEL_HEADER_HEIGHT } from '../SidePanelShared' import { ToolDetailsModal } from './ToolDetailsModal' type ServerWithTools = { @@ -162,7 +162,7 @@ const HeaderSC = styled.div(({ theme }) => ({ justifyContent: 'space-between', borderBottom: theme.borders.default, padding: `${theme.spacing.small}px ${theme.spacing.medium}px`, - height: CHATBOT_HEADER_HEIGHT, + height: SIDE_PANEL_HEADER_HEIGHT, })) const ContentAccordionSC = styled(Accordion)(({ theme }) => ({ diff --git a/assets/src/components/layout/Console.tsx b/assets/src/components/layout/Console.tsx index b670dab320..f909f625f2 100644 --- a/assets/src/components/layout/Console.tsx +++ b/assets/src/components/layout/Console.tsx @@ -21,19 +21,22 @@ import { SelectedProjectProvider } from '../contexts/ProjectsContext' import { ShareSecretProvider } from '../sharesecret/ShareSecretContext' import { CLOSE_CHAT_ACTION_PANEL_EVENT } from 'components/ai/AIAgentSessions' -import { AIContextProvider, useChatbot } from 'components/ai/AIContext' -import { ChatbotPanel } from 'components/ai/chatbot/Chatbot' +import { useChatbot } from 'components/ai/AIContext' import { CommandPaletteProvider } from 'components/commandpalette/CommandPaletteContext' import { FeatureFlagProvider } from 'components/flows/FeatureFlagContext' import { useNativeDomEvent } from 'components/hooks/useNativeDomEvent' +import { AccessTokenProvider } from 'components/profile/access-tokens/AccessTokenContext' +import { SimpleToastProvider } from 'components/utils/SimpleToastContext' import { CloudConsoleWelcomeModal } from '../cloud-setup/CloudConsoleWelcomeModal' +import { SentryInitializer } from '../SentryInitializer' import { ApplicationUpdateToast } from './ApplicationUpdateToast' import Header from './Header' import { Sidebar, SidebarProvider } from './Sidebar' import Subheader from './Subheader' -import { SentryInitializer } from '../SentryInitializer' -import { AccessTokenProvider } from 'components/profile/access-tokens/AccessTokenContext' -import { SimpleToastProvider } from 'components/utils/SimpleToastContext' +import { + TopLevelSidePanel, + TopLevelSidePanelProviders, +} from './TopLevelSidePanel' export default function Console() { return ( @@ -50,7 +53,7 @@ export default function Console() { - + @@ -58,7 +61,7 @@ export default function Console() { - + @@ -127,7 +130,7 @@ function ConsoleContent() { - + ) } diff --git a/assets/src/components/layout/TopLevelSidePanel.tsx b/assets/src/components/layout/TopLevelSidePanel.tsx new file mode 100644 index 0000000000..3b2c69af96 --- /dev/null +++ b/assets/src/components/layout/TopLevelSidePanel.tsx @@ -0,0 +1,130 @@ +import { Accordion, AccordionItem } from '@pluralsh/design-system' +import { AIContextProvider } from 'components/ai/AIContext' +import { ChatbotPanelContent } from 'components/ai/chatbot/Chatbot' +import { DragHandleSC } from 'components/ai/chatbot/SidePanelShared' +import { useResizablePane } from 'components/ai/chatbot/useResizeableChatPane' +import { WorkbenchJobPanelContent } from 'components/workbenches/workbench/job/WorkbenchJobPanel' +import { + WebhookSetupGuidePanelContent, + WebhookSetupGuidePanelProvider, +} from 'components/workbenches/workbench/webhooks/WebhookSetupGuidePanel' +import { ReactNode, createContext, use, useMemo, useState } from 'react' +import { matchPath, useLocation } from 'react-router-dom' +import { + WORKBENCH_JOBS_PATH_MATCHER_ABS, + WORKBENCH_WEBHOOK_TRIGGERS_PATH_MATCHER_ABS, +} from 'routes/workbenchesRoutesConsts' + +export type SidePanel = 'ai-chat' | 'webhook-setup-guide' | 'workbench-job' + +const MIN_WIDTH = 500 +const MAX_WIDTH_VW = 40 + +const ALLOWED_ROUTES: Record, string[]> = { + 'webhook-setup-guide': [WORKBENCH_WEBHOOK_TRIGGERS_PATH_MATCHER_ABS], + 'workbench-job': [WORKBENCH_JOBS_PATH_MATCHER_ABS], +} + +const TopLevelSidePanelContext = createContext<{ + sidePanel: SidePanel | null + setSidePanel: (panel: SidePanel | null) => void +}>({ + sidePanel: null, + setSidePanel: () => {}, +}) + +export function useTopLevelSidePanel() { + return use(TopLevelSidePanelContext) +} + +export function TopLevelSidePanel() { + const { sidePanel } = useTopLevelSidePanel() + const { calculatedPanelWidth, dragHandleProps, isDragging } = + useResizablePane(MIN_WIDTH, MAX_WIDTH_VW) + + return ( + + +
    + + +
    +
    +
    + ) +} + +function TopLevelSidePanelContent({ + sidePanel, +}: { + sidePanel: SidePanel | null +}) { + switch (sidePanel) { + case 'webhook-setup-guide': + return + case 'workbench-job': + return + case 'ai-chat': + default: + return + } +} + +export function TopLevelSidePanelProviders({ + children, +}: { + children: ReactNode +}) { + return ( + + + + {children} + + + + ) +} + +function TopLevelSidePanelProvider({ children }: { children: ReactNode }) { + const [requested, setRequested] = useState(null) + const { pathname } = useLocation() + + const sidePanel: SidePanel | null = useMemo(() => { + if (!requested || requested === 'ai-chat') return requested + + return ALLOWED_ROUTES[requested]?.some((pattern) => + matchPath(pattern, pathname) + ) + ? requested + : null + }, [requested, pathname]) + + const ctx = useMemo( + () => ({ sidePanel, setSidePanel: setRequested }), + [sidePanel] + ) + + return ( + {children} + ) +} diff --git a/assets/src/components/settings/global/observability/EditObservabilityWebhook.tsx b/assets/src/components/settings/global/observability/EditObservabilityWebhook.tsx index 98fd6ec872..7068801105 100644 --- a/assets/src/components/settings/global/observability/EditObservabilityWebhook.tsx +++ b/assets/src/components/settings/global/observability/EditObservabilityWebhook.tsx @@ -9,6 +9,7 @@ import { Modal, NewrelicLogoIcon, PagerdutyLogoIcon, + PluralLogoIcon, Select, SentryLogoIcon, WebhooksIcon, @@ -161,7 +162,9 @@ const WrapperFormSC = styled.form(({ theme }) => ({ gap: theme.spacing.medium, })) -function getObservabilityWebhookTypeIcon(type: ObservabilityWebhookType) { +export function getObservabilityWebhookTypeIcon( + type: Nullable +) { switch (type) { case ObservabilityWebhookType.Grafana: return @@ -173,6 +176,8 @@ function getObservabilityWebhookTypeIcon(type: ObservabilityWebhookType) { return case ObservabilityWebhookType.Sentry: return + case ObservabilityWebhookType.Plural: + return default: return } diff --git a/assets/src/components/workbenches/WorkbenchesList.tsx b/assets/src/components/workbenches/WorkbenchesList.tsx index 3a03ec3af4..cd38139035 100644 --- a/assets/src/components/workbenches/WorkbenchesList.tsx +++ b/assets/src/components/workbenches/WorkbenchesList.tsx @@ -82,7 +82,7 @@ export function WorkbenchesList() { function WorkbenchCard({ workbench }: { workbench: WorkbenchTinyFragment }) { const { spacing } = useTheme() - const { id, name, description, tools: t, repository } = workbench + const { id, name, description, tools: t } = workbench const tools = t?.filter(isNonNullable) ?? [] return ( @@ -110,15 +112,23 @@ export function Workbench() { as={Link} to={WORKBENCHES_EDIT_REL_PATH} > - Edit workbench + Edit + + mapExistingNodes(workbench?.crons), [workbench]) + const webhooks = useMemo( + () => mapExistingNodes(workbench?.webhooks), + [workbench] + ) + + // TODO: Do it on the server-side to avoid bug when there is more than one page of crons. + const nextCron = useMemo( + () => + minBy(crons, (cron) => { + const nextRunAt = cron.nextRunAt ? Date.parse(cron.nextRunAt) : NaN + + return Number.isFinite(nextRunAt) ? nextRunAt : Number.POSITIVE_INFINITY + }) ?? null, + [crons] + ) + const nextRunTime = useMemo(() => { + if (!nextCron?.nextRunAt) return null + + const parsed = dayjs(nextCron.nextRunAt) + if (!parsed.isValid()) return nextCron.nextRunAt + + return parsed.utc().format('YYYY-MM-DD HH:mm:ss [UTC]') + }, [nextCron]) + + const nextRunTimeParts = useMemo( + () => (nextRunTime ? formatPreviewTimestamp(nextRunTime) : null), + [nextRunTime] + ) + + if (!nextCron && webhooks.length === 0) return null + + return ( + + {nextCron && ( + + {nextRunTimeParts ? ( + + next at + {nextRunTimeParts.datePart}{' '} + + {nextRunTimeParts.hourPart} + {' '} + {nextRunTimeParts.zonePart} + + ) : nextRunTime ? ( + + next at + {nextRunTime} + + ) : null} + + )} + {!isEmpty(webhooks) && ( + + + {webhooks.map((webhook) => ( + + + {webhook.name} + + ))} + + + )} + + ) +} diff --git a/assets/src/components/workbenches/workbench/create-edit/WorkbenchFormSteps.tsx b/assets/src/components/workbenches/workbench/create-edit/WorkbenchFormSteps.tsx index 87dd4afe64..e4a7887a43 100644 --- a/assets/src/components/workbenches/workbench/create-edit/WorkbenchFormSteps.tsx +++ b/assets/src/components/workbenches/workbench/create-edit/WorkbenchFormSteps.tsx @@ -10,7 +10,6 @@ import { Input2, isValidRepoUrl, ListBoxItem, - prettifyRepoUrl, Radio, RadioGroup, Select, @@ -356,7 +355,7 @@ export function WorkbenchCodingAgentStep({ {allowedRepos.map((url) => ( ))} @@ -410,7 +409,7 @@ export function WorkbenchCodingAgentStep({ clickable onClick={() => removeRepo(url)} > - {prettifyRepoUrl(url)} + {url} ))} diff --git a/assets/src/components/workbenches/workbench/triggers/WorkbenchScheduleDeleteModal.tsx b/assets/src/components/workbenches/workbench/crons/CronScheduleDeleteModal.tsx similarity index 87% rename from assets/src/components/workbenches/workbench/triggers/WorkbenchScheduleDeleteModal.tsx rename to assets/src/components/workbenches/workbench/crons/CronScheduleDeleteModal.tsx index 41362cfb18..01f807c415 100644 --- a/assets/src/components/workbenches/workbench/triggers/WorkbenchScheduleDeleteModal.tsx +++ b/assets/src/components/workbenches/workbench/crons/CronScheduleDeleteModal.tsx @@ -5,10 +5,9 @@ import { useDeleteWorkbenchCronMutation, WorkbenchCronFragment, } from 'generated/graphql' -import { SCHEDULE_TRIGGER_REFETCH_QUERIES } from './WorkbenchTriggers' import { truncate } from 'lodash' -export function WorkbenchScheduleDeleteModal({ +export function CronScheduleDeleteModal({ open, cron, onClose, @@ -28,7 +27,7 @@ export function WorkbenchScheduleDeleteModal({ }) onClose() }, - refetchQueries: SCHEDULE_TRIGGER_REFETCH_QUERIES, + refetchQueries: ['WorkbenchCrons', 'WorkbenchTriggersSummary'], awaitRefetchQueries: true, }) diff --git a/assets/src/components/workbenches/workbench/crons/CronScheduleForm.tsx b/assets/src/components/workbenches/workbench/crons/CronScheduleForm.tsx new file mode 100644 index 0000000000..182edae78a --- /dev/null +++ b/assets/src/components/workbenches/workbench/crons/CronScheduleForm.tsx @@ -0,0 +1,364 @@ +import { + Button, + Card, + EmptyState, + Flex, + FormField, + Input, + Input2, + ReturnIcon, + useSetBreadcrumbs, +} from '@pluralsh/design-system' +import { GqlError } from 'components/utils/Alert' +import { useSimpleToast } from 'components/utils/SimpleToastContext' +import { RectangleSkeleton } from 'components/utils/SkeletonLoaders' +import { StackedText } from 'components/utils/table/StackedText' +import { Body2P, CaptionP, InlineA } from 'components/utils/typography/Text' +import { + useCreateWorkbenchCronMutation, + useGetWorkbenchCronMutation, + useUpdateWorkbenchCronMutation, + useWorkbenchQuery, + WorkbenchCronFragment, +} from 'generated/graphql' +import { isEqual, truncate } from 'lodash' +import { useEffect, useMemo, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { + getWorkbenchCronSchedulesAbsPath, + WORKBENCH_PARAM_ID, + WORKBENCHES_CRON_PARAM_ID, +} from 'routes/workbenchesRoutesConsts' +import { useTheme } from 'styled-components' +import { getWorkbenchBreadcrumbs } from '../Workbench' +import { + FormCardSC, + StickyActionsFooterSC, +} from '../create-edit/WorkbenchCreateOrEdit' +import { + buildCronPreview, + CRON_PLACEHOLDER, + formatPreviewTimestamp, + validateCronExpression, +} from './utils' + +const CRON_SHORTCUTS_URL = + 'https://github.com/harrisiirak/cron-parser?tab=readme-ov-file#predefined-expressions' // TODO: Use our own docs once we have them. + +type CronScheduleFormState = { + prompt: string + crontab: string +} + +export function CronScheduleForm({ mode }: { mode: 'create' | 'edit' }) { + const navigate = useNavigate() + const theme = useTheme() + const workbenchId = useParams()[WORKBENCH_PARAM_ID] ?? '' + const cronId = useParams()[WORKBENCHES_CRON_PARAM_ID] + const [formState, setFormState] = useState( + getInitialFormState() + ) + const { popToast } = useSimpleToast() + + const { + data: workbenchData, + loading: workbenchLoading, + error: workbenchError, + } = useWorkbenchQuery({ + variables: { id: workbenchId }, + skip: !workbenchId, + }) + const workbench = workbenchData?.workbench + + const [ + fetchCron, + { data: cronData, loading: cronLoading, error: cronsError }, + ] = useGetWorkbenchCronMutation({ + onCompleted: (data) => { + const loadedCron = data?.workbenchCron + + if (!loadedCron) return + setFormState(getInitialFormState(loadedCron)) + }, + }) + + useEffect(() => { + if (mode === 'edit' && !!cronId) fetchCron({ variables: { id: cronId } }) + }, [cronId, fetchCron, mode]) + + const cron = mode === 'edit' ? (cronData?.workbenchCron ?? null) : null + + const preview = useMemo( + () => buildCronPreview(formState.crontab), + [formState.crontab] + ) + + const prompt = formState.prompt.trim() + const crontab = formState.crontab.trim() + const isCronValid = !!crontab && validateCronExpression(crontab) + const hasCronError = !!crontab && !isCronValid + + const canSave = + !!prompt && isCronValid && !isEqual(formState, getInitialFormState(cron)) + const attributes = { crontab, prompt } + + const handleCompleted = () => { + navigate(getWorkbenchCronSchedulesAbsPath(workbenchId)) + popToast({ + name: truncate(prompt, { length: 30 }), + action: cron ? 'updated' : 'created', + color: 'icon-success', + }) + } + const [createWorkbenchCron, createState] = useCreateWorkbenchCronMutation({ + variables: { workbenchId, attributes }, + onCompleted: handleCompleted, + refetchQueries: ['WorkbenchCrons', 'WorkbenchTriggersSummary'], + awaitRefetchQueries: true, + }) + const [updateWorkbenchCron, updateState] = useUpdateWorkbenchCronMutation({ + variables: { id: cron?.id ?? '', attributes }, + onCompleted: handleCompleted, + refetchQueries: ['WorkbenchCrons', 'WorkbenchTriggersSummary'], + awaitRefetchQueries: true, + }) + + const isSaving = createState.loading || updateState.loading + const mutationError = createState.error ?? updateState.error + + const handleSave = () => { + if (!canSave) return + if (cron) updateWorkbenchCron() + else createWorkbenchCron() + } + + useSetBreadcrumbs( + useMemo( + () => [ + ...getWorkbenchBreadcrumbs(workbench), + { + label: 'cron schedules', + url: getWorkbenchCronSchedulesAbsPath(workbenchId), + }, + { label: mode === 'create' ? 'create' : 'edit' }, + ], + [mode, workbench, workbenchId] + ) + ) + + if (workbenchError) return + + if (cronsError) return + + if (mode === 'edit' && !cronLoading && !cron) + return ( + + + + ) + + const isLoading = + (!workbenchData && workbenchLoading) || + (mode === 'edit' && !cronData && cronLoading) + + return ( + + + + {isLoading ? ( + + ) : ( + + {mutationError && } + + + { + const nextPrompt = e.target.value + + setFormState((prev) => ({ ...prev, prompt: nextPrompt })) + }} + placeholder="Ask the agent use an integrated tool or service on your cluster" + /> + + + + Enter a valid cron expression. See all{' '} + + shortcuts + + . + + ) : ( + + Enter a cron expression or use shortcuts like @hourly, + @daily, @weekdays. See all{' '} + shortcuts. + + ) + } + > + + setFormState((prev) => ({ + ...prev, + crontab: e.target.value, + })) + } + placeholder={CRON_PLACEHOLDER} + css={{ + color: theme.colors['code-block-purple'], + fontFamily: theme.fontFamilies.mono, + '&:focus-within': { + border: theme.borders['outline-focused'], + borderColor: hasCronError + ? theme.colors['border-danger'] + : theme.colors['code-block-purple'], + }, + '& input': { + minHeight: 54, + paddingLeft: 16, + paddingRight: 16, + fontFamily: theme.fontFamilies.mono, + }, + }} + /> + + + + {preview.description} + {preview.nextTimes.map((time, index) => { + const parsedTime = formatPreviewTimestamp(time) + + return ( + + + {index === 0 ? 'next at' : 'then at'}{' '} + + {parsedTime ? ( + <> + {parsedTime.datePart}{' '} + + {parsedTime.hourPart} + {' '} + {parsedTime.zonePart} + + ) : ( + time + )} + + ) + })} + + + + + + + + + + )} + + + ) +} + +function getInitialFormState( + cron?: Nullable +): CronScheduleFormState { + return { + prompt: cron?.prompt ?? '', + crontab: cron?.crontab ?? '', + } +} diff --git a/assets/src/components/workbenches/workbench/crons/CronSchedules.tsx b/assets/src/components/workbenches/workbench/crons/CronSchedules.tsx new file mode 100644 index 0000000000..1c14d766cd --- /dev/null +++ b/assets/src/components/workbenches/workbench/crons/CronSchedules.tsx @@ -0,0 +1,232 @@ +import { + Button, + Card, + EmptyState, + Flex, + IconFrame, + PencilIcon, + Table, + TrashCanIcon, + useSetBreadcrumbs, +} from '@pluralsh/design-system' +import { createColumnHelper } from '@tanstack/react-table' +import { GqlError } from 'components/utils/Alert' +import { StretchedFlex } from 'components/utils/StretchedFlex' +import { StackedText } from 'components/utils/table/StackedText' +import { useFetchPaginatedData } from 'components/utils/table/useFetchPaginatedData' +import { Body2P } from 'components/utils/typography/Text' +import { + useWorkbenchCronsQuery, + useWorkbenchQuery, + WorkbenchCronFragment, +} from 'generated/graphql' +import { useMemo, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { + getWorkbenchCronScheduleCreateAbsPath, + getWorkbenchCronScheduleEditAbsPath, + WORKBENCH_PARAM_ID, +} from 'routes/workbenchesRoutesConsts' +import { useTheme } from 'styled-components' +import { mapExistingNodes } from 'utils/graphql' +import { getWorkbenchBreadcrumbs } from '../Workbench' +import { CronScheduleDeleteModal } from './CronScheduleDeleteModal' +import { isEmpty } from 'lodash' +import { cronToExplanation } from './utils' + +export function CronSchedules() { + const navigate = useNavigate() + const theme = useTheme() + const workbenchId = useParams()[WORKBENCH_PARAM_ID] ?? '' + const [deletingCron, setDeletingCron] = + useState>(null) + + const { + data: workbenchData, + loading: workbenchLoading, + error: workbenchError, + } = useWorkbenchQuery({ + variables: { id: workbenchId }, + skip: !workbenchId, + }) + const workbench = workbenchData?.workbench + + const { data, loading, error, pageInfo, fetchNextPage, setVirtualSlice } = + useFetchPaginatedData( + { queryHook: useWorkbenchCronsQuery, keyPath: ['workbench', 'crons'] }, + { id: workbenchId } + ) + + const crons = useMemo(() => mapExistingNodes(data?.workbench?.crons), [data]) + + useSetBreadcrumbs( + useMemo( + () => [ + ...getWorkbenchBreadcrumbs(workbench), + { label: 'cron schedules' }, + ], + [workbench] + ) + ) + + const columns = useMemo( + () => + getColumns({ + onEdit: (cron) => + navigate( + getWorkbenchCronScheduleEditAbsPath({ + workbenchId, + cronId: cron.id, + }) + ), + onDelete: (cron) => setDeletingCron(cron), + }), + [navigate, workbenchId] + ) + + if (workbenchError) return + + if (error) return + + return ( + + + + {!!data && isEmpty(crons) ? ( + + + + + + ) : ( + + + + Add schedules to trigger this workbench. + + + + + + )} + + setDeletingCron(null)} + /> + + ) +} + +const columnHelper = createColumnHelper() +function getColumns({ + onEdit, + onDelete, +}: { + onEdit: (cron: WorkbenchCronFragment) => void + onDelete: (cron: WorkbenchCronFragment) => void +}) { + return [ + columnHelper.accessor((cron) => cron, { + id: 'details', + meta: { truncate: true, gridTemplate: 'minmax(0, 1fr)' }, + cell: ({ getValue }) => { + const cron = getValue() + return ( + + ) + }, + }), + columnHelper.display({ + id: 'actions', + meta: { gridTemplate: '100px' }, + cell: ({ row }) => ( + + } + onClick={() => onEdit(row.original)} + /> + } + onClick={() => onDelete(row.original)} + /> + + ), + }), + ] +} diff --git a/assets/src/components/workbenches/workbench/crons/utils.ts b/assets/src/components/workbenches/workbench/crons/utils.ts new file mode 100644 index 0000000000..32f3e3ed5f --- /dev/null +++ b/assets/src/components/workbenches/workbench/crons/utils.ts @@ -0,0 +1,90 @@ +import CronExpressionParser from 'cron-parser' +import cronstrue from 'cronstrue' +import { dayjsExtended as dayjs, formatDateTime } from 'utils/datetime' + +export const CRON_PLACEHOLDER = '*/5 * * * *' + +export function cronToExplanation({ + crontab, + nextRunAt, +}: { + crontab?: string | null + nextRunAt?: string | null +}) { + const nextRunText = nextRunAt + ? formatDateTime(nextRunAt, 'MMM D, YYYY [at] h:mm A') + : null + const fallback = `Next ${nextRunText ? `at ${nextRunText}` : 'run not scheduled yet'}` + + if (!crontab) return fallback + + try { + const description = cronstrue.toString(crontab.trim(), { + throwExceptionOnParseError: true, + }) + + return nextRunText ? `${description}, next at ${nextRunText}` : description + } catch { + return fallback + } +} + +export function buildCronPreview(expressionInput: string) { + const expression = expressionInput.trim() || CRON_PLACEHOLDER + + try { + const description = cronstrue.toString(expression, { + throwExceptionOnParseError: true, + }) + const nextTimes = getNextTriggerTimesUtc(expression, 3) + + return { description, nextTimes } + } catch { + return { + description: 'Invalid cron expression', + nextTimes: [] as string[], + } + } +} + +function getNextTriggerTimesUtc(expression: string, count: number): string[] { + const iterator = CronExpressionParser.parse(expression, { + currentDate: new Date(), + tz: 'UTC', + }) + + return Array.from({ length: count }, () => formatCronDateUtc(iterator.next())) +} + +function formatCronDateUtc(value: { toISOString: () => string | null }) { + const iso = value.toISOString() + if (!iso) return '' + return dayjs(iso).utc().format('YYYY-MM-DD HH:mm:ss [UTC]') +} + +export function formatPreviewTimestamp(time: string): { + datePart: string + hourPart: string + zonePart: string +} | null { + const parts = time.split(' ') + if (parts.length !== 3) return null + + const [datePart, hourPart, zonePart] = parts + if (!datePart || !hourPart || !zonePart) return null + + return { datePart, hourPart, zonePart } +} + +export function validateCronExpression(expression: string) { + try { + CronExpressionParser.parse(expression, { + currentDate: new Date(), + tz: 'UTC', + }) + + return true + } catch { + return false + } +} diff --git a/assets/src/components/workbenches/workbench/job/WorkbenchJob.tsx b/assets/src/components/workbenches/workbench/job/WorkbenchJob.tsx index 17b9ee2733..70c76fb3f8 100644 --- a/assets/src/components/workbenches/workbench/job/WorkbenchJob.tsx +++ b/assets/src/components/workbenches/workbench/job/WorkbenchJob.tsx @@ -2,12 +2,11 @@ import { EmptyState, Flex, useSetBreadcrumbs } from '@pluralsh/design-system' import { RunStatusChip } from 'components/ai/infra-research/details/InfraResearch' import { POLL_INTERVAL } from 'components/cd/ContinuousDeployment' import { GqlError } from 'components/utils/Alert' -import { RectangleSkeleton } from 'components/utils/SkeletonLoaders' import { StretchedFlex } from 'components/utils/StretchedFlex' import { StackedText } from 'components/utils/table/StackedText' import { useWorkbenchJobQuery } from 'generated/graphql' import { truncate } from 'lodash' -import { Suspense, useMemo } from 'react' +import { useMemo } from 'react' import { useParams } from 'react-router-dom' import { getWorkbenchAbsPath, @@ -16,10 +15,11 @@ import { WORKBENCHES_ABS_PATH, } from 'routes/workbenchesRoutesConsts' import styled from 'styled-components' +import { isNonNullable } from 'utils/isNonNullable' import { WorkbenchJobActivities } from './WorkbenchJobActivities' +import { WorkbenchJobPrs } from './WorkbenchJobPrs' import { WorkbenchJobResult } from './WorkbenchJobResult' import { WorkbenchJobTodos } from './WorkbenchJobTodos' -import { PluralErrorBoundary } from 'components/cd/PluralErrorBoundary' import { WorkbenchJobTriggerAlert } from './WorkbenchJobTriggerAlert' import { WorkbenchJobTriggerIssue } from './WorkbenchJobTriggerIssue' @@ -103,26 +103,19 @@ export function WorkbenchJob() { status={job?.status} /> - - - } - > - - - + + | null>(null) + if (closedIds === null && !!data) setClosedIds(defaultClosedIds(activities)) + + const openIds = useMemo( + () => activities.filter((a) => !closedIds?.has(a.id)).map((a) => a.id), + [activities, closedIds] + ) + useWorkbenchJobActivityDeltaSubscription({ variables: { jobId }, onData: ({ data: { data } }) => { + const id = data?.workbenchJobActivityDelta?.payload?.id + if ( + id && + isActivityTerminal(data?.workbenchJobActivityDelta?.payload?.status) + ) + setClosedIds(new Set(closedIds ? closedIds.add(id) : new Set([id]))) + updateCache(client.cache, { query: WorkbenchJobActivitiesDocument, variables: { id: jobId }, @@ -47,17 +75,30 @@ export function WorkbenchJobActivities({ jobId }: { jobId: string }) { }, }) - const [openIds, setOpenIds] = useState(() => - activities - .filter((activity) => !isActivityRunning(activity.status)) - .map((activity) => activity.id) - ) + if (!data && loading) + return ( + + ) + + if (error) return + return ( { + setClosedIds( + new Set( + activities + .filter((a) => !newOpenIds.includes(a.id)) + .map((a) => a.id) + ) + ) + }} > ( @@ -90,7 +132,7 @@ const ActivitiesPanelSC = styled.div(({ theme }) => ({ border: theme.borders.default, borderRadius: theme.borderRadiuses.medium, padding: `${theme.spacing.xlarge}px ${theme.spacing.large}px`, - background: AI_GRADIENT_BG, + background: theme.colors['fill-zero'], flex: 1, display: 'flex', flexDirection: 'column', @@ -106,3 +148,25 @@ const JobPromptCardSC = styled(Card)(({ theme }) => ({ wordBreak: 'break-word', marginBottom: theme.spacing.small, })) + +const lastActivityId = ( + activities: WorkbenchJobActivityFragment[] +): string | null => { + const last = activities.findLast( + (a) => a.type !== WorkbenchJobActivityType.Memo + ) + if (last) return last.id + return null +} + +const defaultClosedIds = ( + activities: WorkbenchJobActivityFragment[] +): Set => { + const lastId = lastActivityId(activities) + + return new Set( + activities + .filter((a) => a.id !== lastId && isActivityTerminal(a.status)) + .map((a) => a.id) + ) +} diff --git a/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx b/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx index 65c81b243c..c713829564 100644 --- a/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx +++ b/assets/src/components/workbenches/workbench/job/WorkbenchJobActivity.tsx @@ -4,8 +4,10 @@ import { ChecklistCheckedIcon, CheckOutlineIcon, CloudLoggingIcon, + DiscoverIcon, FailedFilledIcon, Flex, + IconFrame, IconProps, InfrastructureIcon, NotebookIcon, @@ -15,35 +17,54 @@ import { UnknownIcon, VisualInspectionIcon, } from '@pluralsh/design-system' -import { SimplifiedMarkdown } from 'components/ai/chatbot/multithread/MultiThreadViewerMessage' +import { + ClickableLabelSC, + SimpleToolCall, + SimplifiedMarkdown, +} from 'components/ai/chatbot/multithread/MultiThreadViewerMessage' import { StretchedFlex } from 'components/utils/StretchedFlex' -import { Body2P, OverlineH3 } from 'components/utils/typography/Text' +import { Body2P, CaptionP, OverlineH3 } from 'components/utils/typography/Text' import { WorkbenchJobActivityFragment, WorkbenchJobActivityStatus, WorkbenchJobActivityType, WorkbenchJobProgressFragment, + WorkbenchJobThoughtFragment, } from 'generated/graphql' -import { ComponentType } from 'react' +import { ComponentType, useState } from 'react' import { useTheme } from 'styled-components' import { isNonNullable } from 'utils/isNonNullable' import { JobActivityLogs, JobActivityMetrics, + JobActivityPrompt, MemoActivityResult, } from './WorkbenchJobActivityResults' +import { GqlError } from 'components/utils/Alert' +import { isEmpty } from 'lodash' +import { AgentRunInfoCard } from 'components/ai/agent-runs/AgentRunFixButton' +import { Link } from 'react-router-dom' +import { getAgentRunAbsPath } from 'routes/aiRoutesConsts' export function WorkbenchJobActivity({ + isOpen, activity, progress, }: { + isOpen: boolean activity: WorkbenchJobActivityFragment progress: WorkbenchJobProgressFragment[] }) { const { spacing } = useTheme() const isRunning = isActivityRunning(activity.status) + + if (activity.type === WorkbenchJobActivityType.Conclusion) { + return + } + const TypeIcon = activityTypeToIcon[activity.type ?? WorkbenchJobActivityType.Integration] + const { agentRun } = activity return ( {activity.type?.toLowerCase() ?? 'activity'} + {agentRun && !isOpen && ( + } + tooltip="Go to agent run details" + /> + )} } @@ -71,7 +106,6 @@ export function WorkbenchJobActivity({ @@ -79,6 +113,12 @@ export function WorkbenchJobActivity({ {progress.map((p, i) => ( {p.text} ))} + {activity.prompt && activity.type !== WorkbenchJobActivityType.Memo && ( + + )} + @@ -90,8 +130,7 @@ function WorkbenchJobActivityResult({ }: { activity: WorkbenchJobActivityFragment }) { - const { type, result } = activity - if (!result) return null + const { type, result, agentRun } = activity switch (type) { case WorkbenchJobActivityType.Memo: return @@ -101,16 +140,58 @@ function WorkbenchJobActivityResult({ direction="column" gap="medium" > - + {result?.error && } + + + - ) } } +function WorkbenchJobActivityThoughts({ + thoughts, +}: { + thoughts: WorkbenchJobThoughtFragment[] +}) { + const { spacing } = useTheme() + const [isExpanded, setIsExpanded] = useState(false) + const isExpandable = thoughts.length > 3 + const visibleThoughts = isExpanded ? thoughts : thoughts.slice(0, 3) + if (isEmpty(thoughts)) return null + return ( + + {visibleThoughts.map((t, i) => ( + + ))} + {isExpandable && ( + setIsExpanded(!isExpanded)}> + + {isExpanded + ? 'View less' + : `View more +${thoughts.length - visibleThoughts.length}`} + + + )} + + ) +} + const activityTypeToIcon: Record< WorkbenchJobActivityType, ComponentType @@ -124,6 +205,7 @@ const activityTypeToIcon: Record< [WorkbenchJobActivityType.Integration]: ToolKitIcon, [WorkbenchJobActivityType.Memory]: BrainIcon, [WorkbenchJobActivityType.User]: BrainIcon, + [WorkbenchJobActivityType.Conclusion]: CheckOutlineIcon, } as const satisfies Record> function ActivityStatusIcon({ diff --git a/assets/src/components/workbenches/workbench/job/WorkbenchJobActivityResults.tsx b/assets/src/components/workbenches/workbench/job/WorkbenchJobActivityResults.tsx index 8e808013e0..38fafa7d39 100644 --- a/assets/src/components/workbenches/workbench/job/WorkbenchJobActivityResults.tsx +++ b/assets/src/components/workbenches/workbench/job/WorkbenchJobActivityResults.tsx @@ -1,11 +1,16 @@ import { ResponsiveLine } from '@nivo/line' import { Button, + Card, FileDiffIcon, Flex, IconFrame, Modal, } from '@pluralsh/design-system' +import { + SimpleAccordion, + SimplifiedMarkdown, +} from 'components/ai/chatbot/multithread/MultiThreadViewerMessage' import { LogLine } from 'components/cd/logs/LogLine' import { SliceTooltip } from 'components/utils/ChartTooltip' import DiffViewer from 'components/utils/DiffViewer' @@ -17,10 +22,10 @@ import { WorkbenchJobActivityLogFragment, WorkbenchJobActivityMetricFragment, } from 'generated/graphql' -import { groupBy, isEmpty, isNil } from 'lodash' +import { groupBy, isEmpty, isNil, truncate } from 'lodash' import { useMemo, useState } from 'react' import { DiffMethod } from 'react-diff-viewer' -import styled from 'styled-components' +import styled, { useTheme } from 'styled-components' import { COLORS } from 'utils/color' import { toDateOrUndef } from 'utils/datetime' import { getOldContentFromTextDiff } from 'utils/textDiff' @@ -28,10 +33,11 @@ import { getOldContentFromTextDiff } from 'utils/textDiff' type ActivityResult = NonNullable export function MemoActivityResult({ - result: { jobUpdate, output }, + result, }: { - result: ActivityResult + result: Nullable }) { + const { jobUpdate, output } = result ?? {} const [showDiff, setShowDiff] = useState(false) const newValue = jobUpdate?.workingTheory ?? jobUpdate?.conclusion ?? '' @@ -139,6 +145,25 @@ export function JobActivityMetrics({ ) } +export function JobActivityPrompt({ prompt }: { prompt: Nullable }) { + const { spacing } = useTheme() + if (!prompt) return null + return ( + + Prompt: + {truncate(prompt, { length: 40 })} + + } + > + + + + + ) +} + const MetricsChartSC = styled.div(() => ({ height: 160, width: '100%', diff --git a/assets/src/components/workbenches/workbench/job/WorkbenchJobPanel.tsx b/assets/src/components/workbenches/workbench/job/WorkbenchJobPanel.tsx new file mode 100644 index 0000000000..e53f6cb60c --- /dev/null +++ b/assets/src/components/workbenches/workbench/job/WorkbenchJobPanel.tsx @@ -0,0 +1,23 @@ +import { SidePanelContent } from 'components/ai/chatbot/SidePanelShared' +import { + SidePanel, + useTopLevelSidePanel, +} from 'components/layout/TopLevelSidePanel' +import { useParams } from 'react-router-dom' +import { WORKBENCH_JOBS_PARAM_JOB } from 'routes/workbenchesRoutesConsts' + +const SIDE_PANEL_TYPE: SidePanel = 'workbench-job' + +export function WorkbenchJobPanelContent() { + const jobId = useParams()[WORKBENCH_JOBS_PARAM_JOB] + return TODO {jobId} +} + +export function useWorkbenchJobPanel() { + const { sidePanel, setSidePanel } = useTopLevelSidePanel() + const isOpen = sidePanel === SIDE_PANEL_TYPE + return { + isOpen, + setOpen: (open: boolean) => setSidePanel(open ? SIDE_PANEL_TYPE : null), + } +} diff --git a/assets/src/components/workbenches/workbench/job/WorkbenchJobPrs.tsx b/assets/src/components/workbenches/workbench/job/WorkbenchJobPrs.tsx new file mode 100644 index 0000000000..dd3c121f0d --- /dev/null +++ b/assets/src/components/workbenches/workbench/job/WorkbenchJobPrs.tsx @@ -0,0 +1,72 @@ +import { + Accordion, + AccordionItem, + ArrowTopRightIcon, + Flex, + IconFrame, + prettifyRepoUrl, + PrOpenIcon, +} from '@pluralsh/design-system' +import { StretchedFlex } from 'components/utils/StretchedFlex' +import { StackedText } from 'components/utils/table/StackedText' +import { Body2BoldP } from 'components/utils/typography/Text' +import { PullRequestBasicFragment } from 'generated/graphql' +import { isEmpty } from 'lodash' + +import { Link } from 'react-router-dom' + +export function WorkbenchJobPrs({ prs }: { prs: PullRequestBasicFragment[] }) { + if (isEmpty(prs)) return null + return ( + + } + /> + } + first={Generated pull requests} + /> + } + > + + {prs.map((pr) => ( + + + } + /> + + ))} + + + + ) +} diff --git a/assets/src/components/workbenches/workbench/job/WorkbenchJobResult.tsx b/assets/src/components/workbenches/workbench/job/WorkbenchJobResult.tsx index 9e629ac803..f20c71931c 100644 --- a/assets/src/components/workbenches/workbench/job/WorkbenchJobResult.tsx +++ b/assets/src/components/workbenches/workbench/job/WorkbenchJobResult.tsx @@ -45,6 +45,7 @@ const ResultPanelSC = styled.div(({ theme }) => ({ position: 'relative', overflow: 'hidden', padding: theme.spacing.large, + minHeight: 400, '&::before': { borderRadius: theme.borderRadiuses.large, content: '""', diff --git a/assets/src/components/workbenches/workbench/job/WorkbenchJobTriggerAlert.tsx b/assets/src/components/workbenches/workbench/job/WorkbenchJobTriggerAlert.tsx index ceece27b5c..6be35e3de3 100644 --- a/assets/src/components/workbenches/workbench/job/WorkbenchJobTriggerAlert.tsx +++ b/assets/src/components/workbenches/workbench/job/WorkbenchJobTriggerAlert.tsx @@ -6,16 +6,16 @@ import { MegaphoneIcon, Prop, } from '@pluralsh/design-system' +import { alertSeverityToChipSeverity } from 'components/utils/alerts/AlertsTable' +import { AlertStateChip } from 'components/utils/alerts/AlertStateChip' +import { Body2BoldP, CaptionP, InlineA } from 'components/utils/typography/Text' import { TriggerAccordionSC, - TriggerCardSC, TriggerCardIconWrapperSC, + TriggerCardSC, TriggerPropsRowSC, } from 'components/workbenches/common/WorkbenchTriggerCard' -import { alertSeverityToChipSeverity } from 'components/utils/alerts/AlertsTable' -import { AlertStateChip } from 'components/utils/alerts/AlertStateChip' -import { Body2BoldP, CaptionP, InlineA } from 'components/utils/typography/Text' -import { WorkbenchJobFragment } from 'generated/graphql' +import { AlertFragment } from 'generated/graphql' import { startCase } from 'lodash' import styled from 'styled-components' import { formatDateTime } from 'utils/datetime' @@ -23,7 +23,7 @@ import { formatDateTime } from 'utils/datetime' export function WorkbenchJobTriggerAlert({ alert, }: { - alert?: Nullable + alert?: Nullable }) { if (!alert) return null diff --git a/assets/src/components/workbenches/workbench/triggers/WorkbenchScheduleTrigger.tsx b/assets/src/components/workbenches/workbench/triggers/WorkbenchScheduleTrigger.tsx deleted file mode 100644 index ef6dae8684..0000000000 --- a/assets/src/components/workbenches/workbench/triggers/WorkbenchScheduleTrigger.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import { - Button, - Flex, - IconFrame, - PencilIcon, - Table, - TrashCanIcon, -} from '@pluralsh/design-system' -import { createColumnHelper } from '@tanstack/react-table' -import cronstrue from 'cronstrue' -import { GqlError } from 'components/utils/Alert' -import { StretchedFlex } from 'components/utils/StretchedFlex' -import { StackedText } from 'components/utils/table/StackedText' -import { useFetchPaginatedData } from 'components/utils/table/useFetchPaginatedData' -import { Body2P } from 'components/utils/typography/Text' -import { - useWorkbenchCronsQuery, - WorkbenchCronFragment, -} from 'generated/graphql' -import { useMemo, useState } from 'react' -import { useParams, useSearchParams } from 'react-router-dom' -import { - WORKBENCH_PARAM_ID, - WORKBENCHES_TRIGGERS_CREATE_QUERY_PARAM, -} from 'routes/workbenchesRoutesConsts' -import { useTheme } from 'styled-components' -import { formatDateTime } from 'utils/datetime' -import { mapExistingNodes } from 'utils/graphql' -import { FormCardSC } from '../create-edit/WorkbenchCreateOrEdit' -import { WorkbenchScheduleDeleteModal } from './WorkbenchScheduleDeleteModal' -import { WorkbenchScheduleTriggerForm } from './WorkbenchScheduleTriggerForm' - -export function WorkbenchScheduleTrigger() { - const theme = useTheme() - const workbenchId = useParams()[WORKBENCH_PARAM_ID] ?? '' - const [searchParams, setSearchParams] = useSearchParams() - const isCreating = - searchParams.get(WORKBENCHES_TRIGGERS_CREATE_QUERY_PARAM) === 'true' - const [editingCronId, setEditingCronId] = useState>(null) - const [deletingCron, setDeletingCron] = - useState>(null) - - const { data, loading, error, pageInfo, fetchNextPage, setVirtualSlice } = - useFetchPaginatedData( - { queryHook: useWorkbenchCronsQuery, keyPath: ['workbench', 'crons'] }, - { id: workbenchId } - ) - - const crons = useMemo(() => mapExistingNodes(data?.workbench?.crons), [data]) - - const editingCron = useMemo( - () => crons.find((cron) => cron.id === editingCronId), - [crons, editingCronId] - ) - - const columns = useMemo( - () => - getColumns({ - onEdit: (cron) => setEditingCronId(cron.id), - onDelete: (cron) => setDeletingCron(cron), - }), - [] - ) - const clearForm = () => { - setEditingCronId(null) - setSearchParams({}, { replace: true }) - } - - if (error) return - if (isCreating || editingCron) - return ( - - - - ) - - return ( - - - - - Add schedules to trigger this workbench. - - - -
    - - setDeletingCron(null)} - /> - - ) -} - -const columnHelper = createColumnHelper() -function getColumns({ - onEdit, - onDelete, -}: { - onEdit: (cron: WorkbenchCronFragment) => void - onDelete: (cron: WorkbenchCronFragment) => void -}) { - return [ - columnHelper.accessor((cron) => cron, { - id: 'details', - meta: { truncate: true, gridTemplate: 'minmax(0, 1fr)' }, - cell: ({ getValue }) => { - const cron = getValue() - return ( - - ) - }, - }), - columnHelper.display({ - id: 'actions', - meta: { gridTemplate: '100px' }, - cell: ({ row }) => ( - - } - onClick={() => onEdit(row.original)} - /> - } - onClick={() => onDelete(row.original)} - /> - - ), - }), - ] -} - -function cronToExplanation({ - crontab, - nextRunAt, -}: Pick) { - const nextRunText = nextRunAt - ? formatDateTime(nextRunAt, 'MMM D, YYYY [at] h:mm A') - : null - const fallback = `Next ${nextRunText ? `at ${nextRunText}` : 'run not scheduled yet'}` - - if (!crontab) return fallback - - try { - const description = cronstrue.toString(crontab.trim(), { - throwExceptionOnParseError: true, - }) - - return nextRunText ? `${description}, next at ${nextRunText}` : description - } catch { - return fallback - } -} diff --git a/assets/src/components/workbenches/workbench/triggers/WorkbenchScheduleTriggerForm.tsx b/assets/src/components/workbenches/workbench/triggers/WorkbenchScheduleTriggerForm.tsx deleted file mode 100644 index 7a59946e91..0000000000 --- a/assets/src/components/workbenches/workbench/triggers/WorkbenchScheduleTriggerForm.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import { - Button, - Card, - Flex, - FormField, - Input, - Input2, - ReturnIcon, -} from '@pluralsh/design-system' -import { GqlError } from 'components/utils/Alert' -import { useSimpleToast } from 'components/utils/SimpleToastContext' -import { Body2P, CaptionP, InlineA } from 'components/utils/typography/Text' -import CronExpressionParser from 'cron-parser' -import cronstrue from 'cronstrue' -import { - useCreateWorkbenchCronMutation, - useUpdateWorkbenchCronMutation, - WorkbenchCronFragment, -} from 'generated/graphql' -import { isEqual, truncate } from 'lodash' -import { useMemo, useState } from 'react' -import { useTheme } from 'styled-components' -import { dayjsExtended as dayjs } from 'utils/datetime' -import { StickyActionsFooterSC } from '../create-edit/WorkbenchCreateOrEdit' -import { SCHEDULE_TRIGGER_REFETCH_QUERIES } from './WorkbenchTriggers' - -const CRON_SHORTCUTS_URL = - 'https://github.com/harrisiirak/cron-parser?tab=readme-ov-file#predefined-expressions' -const CRON_PLACEHOLDER = '*/5 * * * *' - -type ScheduleTriggerFormState = { - prompt: string - crontab: string -} - -export function WorkbenchScheduleTriggerForm({ - workbenchId, - cron, - onCancel, - onCompleted, -}: { - workbenchId: string - cron?: Nullable - onCancel: () => void - onCompleted?: Nullable<() => void> -}) { - const theme = useTheme() - const editing = !!cron - - const [formState, setFormState] = useState(() => - getInitialFormState(cron) - ) - const { popToast } = useSimpleToast() - - const preview = useMemo( - () => buildCronPreview(formState.crontab), - [formState.crontab] - ) - - const prompt = formState.prompt.trim() - const crontab = formState.crontab.trim() - const isCronValid = !!crontab && validateCronExpression(crontab) - const hasCronError = !!crontab && !isCronValid - - const canSave = - !!prompt && isCronValid && !isEqual(formState, getInitialFormState(cron)) - const attributes = { crontab, prompt } - - const handleCompleted = () => { - onCompleted?.() - popToast({ - name: truncate(prompt, { length: 30 }), - action: editing ? 'updated' : 'created', - color: 'icon-success', - }) - } - const [createWorkbenchCron, createState] = useCreateWorkbenchCronMutation({ - variables: { workbenchId, attributes }, - onCompleted: handleCompleted, - refetchQueries: SCHEDULE_TRIGGER_REFETCH_QUERIES, - awaitRefetchQueries: true, - }) - const [updateWorkbenchCron, updateState] = useUpdateWorkbenchCronMutation({ - variables: { id: cron?.id ?? '', attributes }, - onCompleted: handleCompleted, - refetchQueries: SCHEDULE_TRIGGER_REFETCH_QUERIES, - awaitRefetchQueries: true, - }) - - const isSaving = createState.loading || updateState.loading - const error = createState.error ?? updateState.error - - const handleSave = () => { - if (!canSave) return - if (editing && cron) updateWorkbenchCron() - else createWorkbenchCron() - } - - return ( - - {error && } - - { - const prompt = e.target.value - - setFormState((prev) => ({ ...prev, prompt })) - }} - placeholder="Ask the agent use an integrated tool or service on your cluster" - /> - - - - Enter a valid cron expression. See all{' '} - - shortcuts - - . - - ) : ( - - Enter a cron expression or use shortcuts like @hourly, @daily, - @weekdays. See all{' '} - shortcuts. - - ) - } - > - - setFormState((prev) => ({ ...prev, crontab: e.target.value })) - } - placeholder={CRON_PLACEHOLDER} - css={{ - color: theme.colors['code-block-purple'], - fontFamily: theme.fontFamilies.mono, - '&:focus-within': { - border: theme.borders['outline-focused'], - borderColor: hasCronError - ? theme.colors['border-danger'] - : theme.colors['code-block-purple'], - }, - '& input': { - minHeight: 54, - paddingLeft: 16, - paddingRight: 16, - fontFamily: theme.fontFamilies.mono, - }, - }} - /> - - - - {preview.description} - {preview.nextTimes.map((time, index) => { - const parsedTime = formatPreviewTimestamp(time) - - return ( - - - {index === 0 ? 'next at' : 'then at'}{' '} - - {parsedTime ? ( - <> - {parsedTime.datePart}{' '} - - {parsedTime.hourPart} - {' '} - {parsedTime.zonePart} - - ) : ( - time - )} - - ) - })} - - - - - - - - - ) -} - -function validateCronExpression(expression: string) { - try { - CronExpressionParser.parse(expression, { - currentDate: new Date(), - tz: 'UTC', - }) - - return true - } catch { - return false - } -} - -function getInitialFormState( - cron?: Nullable -): ScheduleTriggerFormState { - return { - prompt: cron?.prompt ?? '', - crontab: cron?.crontab ?? '', - } -} - -function buildCronPreview(expressionInput: string) { - const expression = expressionInput.trim() || CRON_PLACEHOLDER - - try { - const description = cronstrue.toString(expression, { - throwExceptionOnParseError: true, - }) - const nextTimes = getNextTriggerTimesUtc(expression, 3) - - return { description, nextTimes } - } catch { - return { - description: 'Invalid cron expression', - nextTimes: [] as string[], - } - } -} - -function getNextTriggerTimesUtc(expression: string, count: number): string[] { - const iterator = CronExpressionParser.parse(expression, { - currentDate: new Date(), - tz: 'UTC', - }) - - return Array.from({ length: count }, () => formatCronDateUtc(iterator.next())) -} - -function formatCronDateUtc(value: { toISOString: () => string | null }) { - const iso = value.toISOString() - if (!iso) return '' - return dayjs(iso).utc().format('YYYY-MM-DD HH:mm:ss [UTC]') -} - -function formatPreviewTimestamp(time: string): Nullable<{ - datePart: string - hourPart: string - zonePart: string -}> { - const parts = time.split(' ') - if (parts.length !== 3) return null - - const [datePart, hourPart, zonePart] = parts - if (!datePart || !hourPart || !zonePart) return null - - return { datePart, hourPart, zonePart } -} diff --git a/assets/src/components/workbenches/workbench/triggers/WorkbenchTriggers.tsx b/assets/src/components/workbenches/workbench/triggers/WorkbenchTriggers.tsx deleted file mode 100644 index 32423f0bc7..0000000000 --- a/assets/src/components/workbenches/workbench/triggers/WorkbenchTriggers.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { Flex, useSetBreadcrumbs } from '@pluralsh/design-system' -import { GqlError } from 'components/utils/Alert' -import { RectangleSkeleton } from 'components/utils/SkeletonLoaders' -import { StackedText } from 'components/utils/table/StackedText' -import { useWorkbenchTriggersSummaryQuery } from 'generated/graphql' -import { useMemo } from 'react' -import { - Link, - Outlet, - useMatch, - useParams, - useSearchParams, -} from 'react-router-dom' -import { - getWorkbenchAbsPath, - WORKBENCH_PARAM_ID, - WORKBENCHES_TRIGGERS_CREATE_QUERY_PARAM, - WORKBENCHES_TRIGGERS_REL_PATH, - WORKBENCHES_TRIGGERS_SCHEDULE_REL_PATH, - WORKBENCHES_TRIGGERS_WEBHOOK_REL_PATH, -} from 'routes/workbenchesRoutesConsts' -import { mapExistingNodes } from 'utils/graphql' - -import { getWorkbenchBreadcrumbs } from '../Workbench' -import { - SidebarBtnSC, - WorkbenchSplitLayoutSC, -} from '../create-edit/WorkbenchCreateOrEdit' -import { - WorkbenchScheduleEmptyState, - WorkbenchWebhookEmptyState, -} from './WorkbenchTriggersEmptyStates' - -export const WEBHOOK_TRIGGER_REFETCH_QUERIES = [ - 'WorkbenchTriggersSummary', - 'WorkbenchWebhooks', -] - -export const SCHEDULE_TRIGGER_REFETCH_QUERIES = [ - 'WorkbenchTriggersSummary', - 'WorkbenchCrons', -] - -const DIRECTORY = [ - { path: WORKBENCHES_TRIGGERS_SCHEDULE_REL_PATH, label: 'Schedule trigger' }, - { path: WORKBENCHES_TRIGGERS_WEBHOOK_REL_PATH, label: 'Webhook trigger' }, -] - -export function WorkbenchTriggers() { - const id = useParams()[WORKBENCH_PARAM_ID] - const pathPrefix = `${getWorkbenchAbsPath(id)}/${WORKBENCHES_TRIGGERS_REL_PATH}` - const tab = - useMatch(`${pathPrefix}/:tab`)?.params.tab ?? - WORKBENCHES_TRIGGERS_SCHEDULE_REL_PATH - const [searchParams] = useSearchParams() - const isCreating = - searchParams.get(WORKBENCHES_TRIGGERS_CREATE_QUERY_PARAM) === 'true' - - const { data, loading, error } = useWorkbenchTriggersSummaryQuery({ - variables: { id: id ?? '' }, - skip: !id, - }) - - const workbench = data?.workbench - const hasSchedules = mapExistingNodes(workbench?.crons).length > 0 - const hasWebhooks = mapExistingNodes(workbench?.webhooks).length > 0 - const showEmptyState = - !isCreating && - ((tab === WORKBENCHES_TRIGGERS_SCHEDULE_REL_PATH && !hasSchedules) || - (tab === WORKBENCHES_TRIGGERS_WEBHOOK_REL_PATH && !hasWebhooks)) - - useSetBreadcrumbs( - useMemo( - () => [...getWorkbenchBreadcrumbs(workbench), { label: 'triggers' }], - [workbench] - ) - ) - - if (error) - return ( - - ) - - return ( - - - - - {DIRECTORY.map(({ path, label }) => ( - - {label} - - ))} - - {!data && loading ? ( - - ) : showEmptyState ? ( - - {tab === WORKBENCHES_TRIGGERS_SCHEDULE_REL_PATH ? ( - <> - {!hasSchedules && } - {!hasWebhooks && } - - ) : ( - <> - {!hasWebhooks && } - {!hasSchedules && } - - )} - - ) : ( - - )} - - - ) -} diff --git a/assets/src/components/workbenches/workbench/triggers/WorkbenchTriggersEmptyStates.tsx b/assets/src/components/workbenches/workbench/triggers/WorkbenchTriggersEmptyStates.tsx deleted file mode 100644 index 11f6982a04..0000000000 --- a/assets/src/components/workbenches/workbench/triggers/WorkbenchTriggersEmptyStates.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Button, Card, EmptyState, Flex } from '@pluralsh/design-system' -import { Body2BoldP } from 'components/utils/typography/Text' -import { useNavigate, useParams } from 'react-router-dom' -import { - getWorkbenchAbsPath, - WORKBENCH_PARAM_ID, - WORKBENCHES_TRIGGERS_CREATE_QUERY_PARAM, - WORKBENCHES_TRIGGERS_REL_PATH, - WORKBENCHES_TRIGGERS_SCHEDULE_REL_PATH, - WORKBENCHES_TRIGGERS_WEBHOOK_REL_PATH, -} from 'routes/workbenchesRoutesConsts' -import styled from 'styled-components' - -const OuterCardSC = styled(Card)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - width: '100%', - gap: theme.spacing.small, - padding: theme.spacing.xlarge, -})) - -const InnerCardSC = styled(Card)(() => ({ - border: 'none', - width: '100%', - padding: 0, -})) - -export function WorkbenchScheduleEmptyState() { - const navigate = useNavigate() - const workbenchId = useParams()[WORKBENCH_PARAM_ID] - - return ( - - Schedules - - - - - - - ) -} - -export function WorkbenchWebhookEmptyState() { - const navigate = useNavigate() - const workbenchId = useParams()[WORKBENCH_PARAM_ID] - - return ( - - Webhooks - - - - - - - - - ) -} diff --git a/assets/src/components/workbenches/workbench/triggers/WorkbenchWebhookTrigger.tsx b/assets/src/components/workbenches/workbench/triggers/WorkbenchWebhookTrigger.tsx deleted file mode 100644 index bc3d699948..0000000000 --- a/assets/src/components/workbenches/workbench/triggers/WorkbenchWebhookTrigger.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import { - Button, - Flex, - IconFrame, - PencilIcon, - Table, - TrashCanIcon, -} from '@pluralsh/design-system' -import { createColumnHelper } from '@tanstack/react-table' -import { GqlError } from 'components/utils/Alert' -import { StretchedFlex } from 'components/utils/StretchedFlex' -import { StackedText } from 'components/utils/table/StackedText' -import { useFetchPaginatedData } from 'components/utils/table/useFetchPaginatedData' -import { Body2P } from 'components/utils/typography/Text' -import { - useWorkbenchWebhooksQuery, - WorkbenchWebhookFragment, -} from 'generated/graphql' -import { useMemo, useState } from 'react' -import { useParams, useSearchParams } from 'react-router-dom' -import { - WORKBENCH_PARAM_ID, - WORKBENCHES_TRIGGERS_CREATE_QUERY_PARAM, -} from 'routes/workbenchesRoutesConsts' -import { useTheme } from 'styled-components' -import { mapExistingNodes } from 'utils/graphql' -import { FormCardSC } from '../create-edit/WorkbenchCreateOrEdit' -import { WorkbenchWebhookDeleteModal } from './WorkbenchWebhookDeleteModal' -import { WorkbenchWebhookTriggerForm } from './WorkbenchWebhookTriggerForm' - -export function WorkbenchWebhookTrigger() { - const theme = useTheme() - const workbenchId = useParams()[WORKBENCH_PARAM_ID] ?? '' - const [searchParams, setSearchParams] = useSearchParams() - const isCreating = - searchParams.get(WORKBENCHES_TRIGGERS_CREATE_QUERY_PARAM) === 'true' - const [editingWebhookId, setEditingWebhookId] = - useState>(null) - const [deletingWebhook, setDeletingWebhook] = - useState>(null) - - const { data, loading, error, pageInfo, fetchNextPage, setVirtualSlice } = - useFetchPaginatedData( - { - queryHook: useWorkbenchWebhooksQuery, - keyPath: ['workbench', 'webhooks'], - }, - { id: workbenchId } - ) - const webhooks = useMemo( - () => mapExistingNodes(data?.workbench?.webhooks), - [data] - ) - - const editingWebhook = useMemo( - () => webhooks.find((webhook) => webhook.id === editingWebhookId), - [webhooks, editingWebhookId] - ) - - const columns = useMemo( - () => - getColumns({ - onEdit: (webhook) => setEditingWebhookId(webhook.id), - onDelete: (webhook) => setDeletingWebhook(webhook), - }), - [] - ) - const clearForm = () => { - setEditingWebhookId(null) - setSearchParams({}, { replace: true }) - } - - if (error) return - if (isCreating || editingWebhook) - return ( - - - - ) - - return ( - - - - - Add webhooks to trigger this workbench. - - - -
    - - setDeletingWebhook(null)} - /> - - ) -} - -const columnHelper = createColumnHelper() -function getColumns({ - onEdit, - onDelete, -}: { - onEdit: (webhook: WorkbenchWebhookFragment) => void - onDelete: (webhook: WorkbenchWebhookFragment) => void -}) { - return [ - columnHelper.accessor((webhook) => webhook, { - id: 'details', - meta: { truncate: true, gridTemplate: 'minmax(0, 1fr)' }, - cell: ({ getValue }) => { - const webhook = getValue() - - return ( - - ) - }, - }), - columnHelper.display({ - id: 'actions', - meta: { gridTemplate: '100px' }, - cell: ({ row }) => ( - - } - onClick={() => onEdit(row.original)} - /> - } - onClick={() => onDelete(row.original)} - /> - - ), - }), - ] -} - -function webhookURL(webhook: WorkbenchWebhookFragment) { - if (webhook.issueWebhook) return webhook.issueWebhook.url - - if (webhook.webhook) return webhook.webhook.url - - return undefined -} diff --git a/assets/src/components/workbenches/workbench/triggers/WorkbenchWebhookTriggerForm.tsx b/assets/src/components/workbenches/workbench/triggers/WorkbenchWebhookTriggerForm.tsx deleted file mode 100644 index 1d5d045a8f..0000000000 --- a/assets/src/components/workbenches/workbench/triggers/WorkbenchWebhookTriggerForm.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { - Button, - Flex, - FormField, - Input2, - ListBoxItem, - ReturnIcon, - Select, -} from '@pluralsh/design-system' -import { GqlError } from 'components/utils/Alert' -import { useSimpleToast } from 'components/utils/SimpleToastContext' -import { - useCreateWorkbenchWebhookMutation, - useObservabilityWebhooksQuery, - useUpdateWorkbenchWebhookMutation, - WorkbenchWebhookFragment, -} from 'generated/graphql' -import { useMemo, useState } from 'react' -import { mapExistingNodes } from 'utils/graphql' -import { StickyActionsFooterSC } from '../create-edit/WorkbenchCreateOrEdit' -import { WEBHOOK_TRIGGER_REFETCH_QUERIES } from './WorkbenchTriggers' -import { isEqual } from 'lodash' - -type WebhookTriggerFormState = { - name: string - webhookId: string -} - -export function WorkbenchWebhookTriggerForm({ - workbenchId, - webhook, - onCancel, - onCompleted, -}: { - workbenchId: string - webhook?: Nullable - onCancel: () => void - onCompleted?: Nullable<() => void> -}) { - const editing = !!webhook - const [formState, setFormState] = useState(() => - getInitialFormState(webhook) - ) - const { popToast } = useSimpleToast() - - const { - data, - loading: webhooksLoading, - error: webhooksError, - } = useObservabilityWebhooksQuery({ variables: { first: 100 } }) - - const webhooks = useMemo( - () => mapExistingNodes(data?.observabilityWebhooks), - [data] - ) - - const label = formState.name.trim() - const webhookId = formState.webhookId - - const canSave = - !!label && !!webhookId && !isEqual(formState, getInitialFormState(webhook)) - const attributes = { name: label, webhookId } - - const handleCompleted = () => { - onCompleted?.() - popToast({ - name: label, - action: editing ? 'updated' : 'created', - color: 'icon-success', - }) - } - const [createWorkbenchWebhook, createState] = - useCreateWorkbenchWebhookMutation({ - variables: { workbenchId, attributes }, - onCompleted: handleCompleted, - refetchQueries: WEBHOOK_TRIGGER_REFETCH_QUERIES, - awaitRefetchQueries: true, - }) - const [updateWorkbenchWebhook, updateState] = - useUpdateWorkbenchWebhookMutation({ - variables: { id: webhook?.id ?? '', attributes }, - onCompleted: handleCompleted, - refetchQueries: WEBHOOK_TRIGGER_REFETCH_QUERIES, - awaitRefetchQueries: true, - }) - - const isSaving = createState.loading || updateState.loading - const error = webhooksError ?? createState.error ?? updateState.error - - const handleSave = () => { - if (!canSave) return - if (editing && webhook) updateWorkbenchWebhook() - else createWorkbenchWebhook() - } - - return ( - - {error && } - - - setFormState((prev) => ({ ...prev, name: e.target.value })) - } - placeholder="Webhook trigger name" - /> - - - - - - - - - - ) -} - -function getInitialFormState( - webhook?: Nullable -): WebhookTriggerFormState { - return { - name: webhook?.name ?? '', - webhookId: webhook?.webhook?.id ?? '', - } -} diff --git a/assets/src/components/workbenches/workbench/webhooks/WebhookForm.tsx b/assets/src/components/workbenches/workbench/webhooks/WebhookForm.tsx new file mode 100644 index 0000000000..45b73cbb05 --- /dev/null +++ b/assets/src/components/workbenches/workbench/webhooks/WebhookForm.tsx @@ -0,0 +1,610 @@ +import { + Button, + Flex, + FormField, + Input2, + ListBoxItem, + ReturnIcon, + Select, + SidePanelOpenIcon, + TicketIcon, + VisualInspectionIcon, + useSetBreadcrumbs, +} from '@pluralsh/design-system' +import { InputRevealer } from 'components/cd/providers/InputRevealer' +import { GqlError } from 'components/utils/Alert' +import { RectangleSkeleton } from 'components/utils/SkeletonLoaders' +import { useSimpleToast } from 'components/utils/SimpleToastContext' +import { StackedText } from 'components/utils/table/StackedText' +import { + IssueWebhookProvider, + ObservabilityWebhookType, + useCreateIssueWebhookMutation, + useUpsertObservabilityWebhookMutation, + useWorkbenchQuery, +} from 'generated/graphql' +import { capitalize } from 'lodash' +import queryString from 'query-string' +import { useEffect, useEffectEvent, useMemo, useState } from 'react' +import { useLocation, useNavigate, useParams } from 'react-router-dom' +import { + getWorkbenchWebhookTriggersAbsPath, + WORKBENCH_PARAM_ID, + WORKBENCHES_WEBHOOK_SELECTED_QUERY_PARAM, +} from 'routes/workbenchesRoutesConsts' +import { getWorkbenchBreadcrumbs } from '../Workbench' +import { + FormCardSC, + StickyActionsFooterSC, +} from '../create-edit/WorkbenchCreateOrEdit' +import { WebhookTriggerFormState } from './WebhookTriggerForm' +import { getObservabilityWebhookTypeIcon } from '../../../settings/global/observability/EditObservabilityWebhook' +import { getIssueWebhookProviderIcon } from './utils' +import { useWebhookSetupGuidePanel } from './WebhookSetupGuidePanel' + +type CreateWebhookType = 'observability' | 'issue' + +type CreateWebhookFormState = { + webhookType: CreateWebhookType + observabilityType: Nullable + observabilityName: string + observabilitySecret: string + issueProvider: Nullable + issueName: string + issueUrl: string + issueSecret: string +} + +type RouteState = { + returnPath?: string + draftState?: WebhookTriggerFormState +} + +type SetupGuideSelection = { + webhookType: CreateWebhookType + observabilityType: Nullable + issueProvider: Nullable +} + +const OBSERVABILITY_SETUP_GUIDE_PATHS: Record< + ObservabilityWebhookType, + string +> = { + [ObservabilityWebhookType.Datadog]: '/setup-guides/webhooks/datadog.md', + [ObservabilityWebhookType.Grafana]: '/setup-guides/webhooks/grafana.md', + [ObservabilityWebhookType.Newrelic]: '/setup-guides/webhooks/newrelic.md', + [ObservabilityWebhookType.Pagerduty]: '/setup-guides/webhooks/pagerduty.md', + [ObservabilityWebhookType.Plural]: '/setup-guides/webhooks/plural.md', + [ObservabilityWebhookType.Sentry]: '/setup-guides/webhooks/sentry.md', +} + +const ISSUE_SETUP_GUIDE_PATHS: Record = { + [IssueWebhookProvider.Asana]: '/setup-guides/webhooks/asana.md', + [IssueWebhookProvider.Github]: '/setup-guides/webhooks/github.md', + [IssueWebhookProvider.Gitlab]: '/setup-guides/webhooks/gitlab.md', + [IssueWebhookProvider.Jira]: '/setup-guides/webhooks/jira.md', + [IssueWebhookProvider.Linear]: '/setup-guides/webhooks/linear.md', +} + +const OBSERVABILITY_SETUP_GUIDE_DOCUMENTATION_URLS: Partial< + Record +> = { + [ObservabilityWebhookType.Datadog]: + 'https://docs.plural.sh/plural-features/observability/observability-webhooks/datadog', + [ObservabilityWebhookType.Grafana]: + 'https://docs.plural.sh/plural-features/observability/observability-webhooks/grafana', +} + +function getSetupGuideMarkdownPath({ + webhookType, + observabilityType, + issueProvider, +}: SetupGuideSelection): Nullable { + if (webhookType === 'observability') { + if (!observabilityType) return null + + return OBSERVABILITY_SETUP_GUIDE_PATHS[observabilityType] ?? null + } + + if (!issueProvider) return null + + return ISSUE_SETUP_GUIDE_PATHS[issueProvider] ?? null +} + +function getSetupGuideDocumentationUrl({ + webhookType, + observabilityType, +}: SetupGuideSelection): string | undefined { + if (webhookType !== 'observability' || !observabilityType) return undefined + + return OBSERVABILITY_SETUP_GUIDE_DOCUMENTATION_URLS[observabilityType] +} + +function getInitialCreateWebhookFormState(): CreateWebhookFormState { + return { + webhookType: 'observability', + observabilityType: null, + observabilityName: '', + observabilitySecret: '', + issueProvider: null, + issueName: '', + issueUrl: '', + issueSecret: '', + } +} + +function getWebhookTypeIcon(type: CreateWebhookType) { + if (type === 'issue') return + + return +} + +function buildReturnPath({ + returnPath, + selectedWebhook, +}: { + returnPath: string + selectedWebhook?: string +}) { + if (!selectedWebhook) return returnPath + + const { url, query, fragmentIdentifier } = queryString.parseUrl(returnPath) + + return queryString.stringifyUrl({ + url, + query: { + ...query, + [WORKBENCHES_WEBHOOK_SELECTED_QUERY_PARAM]: selectedWebhook, + }, + fragmentIdentifier, + }) +} + +export function WebhookForm() { + const navigate = useNavigate() + const location = useLocation() + const workbenchId = useParams()[WORKBENCH_PARAM_ID] ?? '' + const routeState = location.state as Nullable + const { isOpen, openSetupGuidePanel, closeSetupGuidePanel } = + useWebhookSetupGuidePanel() + const [setupGuideSelection, setSetupGuideSelection] = + useState({ + webhookType: 'observability', + observabilityType: null, + issueProvider: null, + }) + const setupGuideMarkdownPath = getSetupGuideMarkdownPath(setupGuideSelection) + const setupGuideDocumentationUrl = + getSetupGuideDocumentationUrl(setupGuideSelection) + const listPath = getWorkbenchWebhookTriggersAbsPath(workbenchId) + const returnPath = routeState?.returnPath ?? listPath + + const { + data: workbenchData, + loading: workbenchLoading, + error: workbenchError, + } = useWorkbenchQuery({ + variables: { id: workbenchId }, + skip: !workbenchId, + }) + const workbench = workbenchData?.workbench + + useSetBreadcrumbs( + useMemo( + () => [ + ...getWorkbenchBreadcrumbs(workbench), + { + label: 'webhook trigger', + url: getWorkbenchWebhookTriggersAbsPath(workbenchId), + }, + { label: 'create webhook' }, + ], + [workbench, workbenchId] + ) + ) + + const error = workbenchError + + const onUnmount = useEffectEvent(() => { + if (isOpen) closeSetupGuidePanel() + }) + useEffect(() => () => onUnmount(), []) + + useEffect(() => { + if (!isOpen) return + if (!setupGuideMarkdownPath) { + closeSetupGuidePanel() + return + } + + openSetupGuidePanel({ + documentationUrl: setupGuideDocumentationUrl, + markdownPath: setupGuideMarkdownPath, + }) + }, [ + isOpen, + setupGuideMarkdownPath, + setupGuideDocumentationUrl, + openSetupGuidePanel, + closeSetupGuidePanel, + ]) + + if (error) return + + return ( + + + + + + + {!workbenchData && workbenchLoading ? ( + + ) : ( + + + navigate(returnPath, { + state: { draftState: routeState?.draftState }, + }) + } + returnPathIsList={returnPath === listPath} + onCreated={(selectedWebhookKey) => { + navigate( + buildReturnPath({ + returnPath, + selectedWebhook: selectedWebhookKey, + }), + { + state: { draftState: routeState?.draftState }, + } + ) + }} + /> + + )} + + {!isOpen && !!setupGuideMarkdownPath && ( +
    + +
    + )} +
    +
    +
    +
    + ) +} + +function CreateWebhookForm({ + onGuideSelectionChange, + onReturn, + returnPathIsList, + onCreated, +}: { + onGuideSelectionChange: (selection: SetupGuideSelection) => void + onReturn: () => void + returnPathIsList?: boolean + onCreated: (selectedWebhookKey: string) => void +}) { + const { popToast } = useSimpleToast() + const [formState, setFormState] = useState( + getInitialCreateWebhookFormState + ) + + const [upsertObservabilityWebhook, upsertObservabilityWebhookState] = + useUpsertObservabilityWebhookMutation() + const [createIssueWebhook, createIssueWebhookState] = + useCreateIssueWebhookMutation() + + const isSaving = + upsertObservabilityWebhookState.loading || createIssueWebhookState.loading + + const error = + upsertObservabilityWebhookState.error ?? createIssueWebhookState.error + + const canCreateObservabilityWebhook = + !!formState.observabilityType && + !!formState.observabilityName.trim() && + !!formState.observabilitySecret.trim() + + const canCreateIssueWebhook = + !!formState.issueProvider && + !!formState.issueName.trim() && + !!formState.issueUrl.trim() && + !!formState.issueSecret.trim() + + const canCreateWebhook = + formState.webhookType === 'observability' + ? canCreateObservabilityWebhook + : canCreateIssueWebhook + + useEffect(() => { + onGuideSelectionChange({ + webhookType: formState.webhookType, + observabilityType: formState.observabilityType, + issueProvider: formState.issueProvider, + }) + }, [ + formState.webhookType, + formState.observabilityType, + formState.issueProvider, + onGuideSelectionChange, + ]) + + const handleCreateNewWebhook = async () => { + if (!canCreateWebhook) return + + if (formState.webhookType === 'observability') { + const response = await upsertObservabilityWebhook({ + variables: { + attributes: { + type: formState.observabilityType!, + name: formState.observabilityName.trim(), + secret: formState.observabilitySecret.trim(), + }, + }, + refetchQueries: ['WorkbenchWebhooks', 'WorkbenchTriggersSummary'], + awaitRefetchQueries: true, + }) + + const createdWebhook = response.data?.upsertObservabilityWebhook + if (!createdWebhook) return + + onCreated(`obs:${createdWebhook.id}`) + popToast({ + name: createdWebhook.name, + action: 'created', + color: 'icon-success', + }) + + return + } + + const response = await createIssueWebhook({ + variables: { + attributes: { + provider: formState.issueProvider!, + name: formState.issueName.trim(), + url: formState.issueUrl.trim(), + secret: formState.issueSecret.trim(), + }, + }, + refetchQueries: ['WorkbenchWebhooks', 'WorkbenchTriggersSummary'], + awaitRefetchQueries: true, + }) + + const createdWebhook = response.data?.createIssueWebhook + if (!createdWebhook) return + + onCreated(`issue:${createdWebhook.id}`) + popToast({ + name: createdWebhook.name, + action: 'created', + color: 'icon-success', + }) + } + + return ( + + {error && } + + + + {formState.webhookType === 'observability' ? ( + <> + + + + + + setFormState((prev) => ({ + ...prev, + observabilityName: e.target.value, + })) + } + /> + + + + setFormState((prev) => ({ + ...prev, + observabilitySecret: e.target.value, + })) + } + /> + + + ) : ( + <> + + + + + + setFormState((prev) => ({ + ...prev, + issueName: e.target.value, + })) + } + /> + + + + setFormState((prev) => ({ + ...prev, + issueUrl: e.target.value, + })) + } + /> + + + + setFormState((prev) => ({ + ...prev, + issueSecret: e.target.value, + })) + } + /> + + + )} + + + + + + ) +} diff --git a/assets/src/components/workbenches/workbench/webhooks/WebhookSetupGuidePanel.tsx b/assets/src/components/workbenches/workbench/webhooks/WebhookSetupGuidePanel.tsx new file mode 100644 index 0000000000..53bc8e84ee --- /dev/null +++ b/assets/src/components/workbenches/workbench/webhooks/WebhookSetupGuidePanel.tsx @@ -0,0 +1,201 @@ +import { + ArrowTopRightIcon, + Button, + CloseIcon, + Flex, + IconFrame, + Markdown, + SidePanelOpenIcon, +} from '@pluralsh/design-system' +import { + SIDE_PANEL_HEADER_HEIGHT, + SidePanelContent, +} from 'components/ai/chatbot/SidePanelShared' +import { + SidePanel, + useTopLevelSidePanel, +} from 'components/layout/TopLevelSidePanel' +import { + ReactNode, + createContext, + use, + useEffect, + useMemo, + useState, +} from 'react' +import styled from 'styled-components' + +const DEFAULT_TITLE = 'Setup guide' +const SIDE_PANEL_TYPE: SidePanel = 'webhook-setup-guide' + +type SetupGuidePanelData = { + documentationUrl?: string + markdownPath: string +} + +type SetupGuidePanelContextT = { + isOpen: boolean + documentationUrl?: string + markdownPath: Nullable + openSetupGuidePanel: (panel: SetupGuidePanelData) => void + closeSetupGuidePanel: () => void +} + +const WebhookSetupGuidePanelContext = createContext({ + isOpen: false, + documentationUrl: undefined, + markdownPath: null, + openSetupGuidePanel: () => + console.error( + 'openSetupGuidePanel must be used within a SetupGuidePanelProvider' + ), + closeSetupGuidePanel: () => + console.error('setOpen must be used within a SetupGuidePanelProvider'), +}) + +export function WebhookSetupGuidePanelProvider({ + children, +}: { + children: ReactNode +}) { + const { sidePanel, setSidePanel } = useTopLevelSidePanel() + const [documentationUrl, setDocumentationUrl] = useState() + const [markdownPath, setMarkdownPath] = useState>(null) + + const isOpen = sidePanel === SIDE_PANEL_TYPE + + const ctx = useMemo( + () => ({ + isOpen, + documentationUrl, + markdownPath, + openSetupGuidePanel: (panel: SetupGuidePanelData) => { + setDocumentationUrl(panel.documentationUrl) + setMarkdownPath(panel.markdownPath) + setSidePanel(SIDE_PANEL_TYPE) + }, + closeSetupGuidePanel: () => setSidePanel(null), + }), + [isOpen, documentationUrl, markdownPath, setSidePanel] + ) + + return ( + + {children} + + ) +} + +export function useWebhookSetupGuidePanel() { + return use(WebhookSetupGuidePanelContext) +} + +export function WebhookSetupGuidePanelContent() { + const { isOpen, documentationUrl, markdownPath, closeSetupGuidePanel } = + useWebhookSetupGuidePanel() + const [markdownText, setMarkdownText] = useState('') + const [isMarkdownLoading, setIsMarkdownLoading] = useState(false) + const [markdownError, setMarkdownError] = useState>(null) + + useEffect(() => { + if (!isOpen || !markdownPath) { + setMarkdownText('') + setMarkdownError(null) + setIsMarkdownLoading(false) + return + } + + const controller = new AbortController() + setIsMarkdownLoading(true) + setMarkdownError(null) + + fetch(markdownPath, { signal: controller.signal }) + .then((response) => { + if (!response.ok) { + throw new Error( + `Failed to load setup guide markdown (${response.status})` + ) + } + + return response.text() + }) + .then((text) => setMarkdownText(text)) + .catch((error: { name?: string }) => { + if (error?.name === 'AbortError') return + + setMarkdownError('Failed to load setup guide content.') + }) + .finally(() => setIsMarkdownLoading(false)) + + return () => controller.abort() + }, [isOpen, markdownPath]) + + return ( + + + + + {documentationUrl && ( + + )} + } + onClick={closeSetupGuidePanel} + tooltip="Close panel" + /> + + + + {isMarkdownLoading ? ( + 'Loading setup guide...' + ) : markdownError ? ( + markdownError + ) : ( + + )} + + + ) +} + +const PanelHeaderSC = styled.div(({ theme }) => ({ + ...theme.partials.text.overline, + color: theme.colors['text-xlight'], + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + minHeight: SIDE_PANEL_HEADER_HEIGHT, + padding: `${theme.spacing.small}px ${theme.spacing.medium}px`, + borderBottom: theme.borders.default, + flexShrink: 0, +})) diff --git a/assets/src/components/workbenches/workbench/triggers/WorkbenchWebhookDeleteModal.tsx b/assets/src/components/workbenches/workbench/webhooks/WebhookTriggerDeleteModal.tsx similarity index 87% rename from assets/src/components/workbenches/workbench/triggers/WorkbenchWebhookDeleteModal.tsx rename to assets/src/components/workbenches/workbench/webhooks/WebhookTriggerDeleteModal.tsx index c71024933f..365a5fa97c 100644 --- a/assets/src/components/workbenches/workbench/triggers/WorkbenchWebhookDeleteModal.tsx +++ b/assets/src/components/workbenches/workbench/webhooks/WebhookTriggerDeleteModal.tsx @@ -3,11 +3,10 @@ import { useDeleteWorkbenchWebhookMutation, WorkbenchWebhookFragment, } from 'generated/graphql' -import { WEBHOOK_TRIGGER_REFETCH_QUERIES } from './WorkbenchTriggers' import { useSimpleToast } from 'components/utils/SimpleToastContext' import { StrongSC } from 'components/utils/typography/Text' -export function WorkbenchWebhookDeleteModal({ +export function WebhookTriggerDeleteModal({ open, webhook, onClose, @@ -27,7 +26,7 @@ export function WorkbenchWebhookDeleteModal({ }) onClose() }, - refetchQueries: WEBHOOK_TRIGGER_REFETCH_QUERIES, + refetchQueries: ['WorkbenchWebhooks', 'WorkbenchTriggersSummary'], awaitRefetchQueries: true, }) diff --git a/assets/src/components/workbenches/workbench/webhooks/WebhookTriggerForm.tsx b/assets/src/components/workbenches/workbench/webhooks/WebhookTriggerForm.tsx new file mode 100644 index 0000000000..57755cdde6 --- /dev/null +++ b/assets/src/components/workbenches/workbench/webhooks/WebhookTriggerForm.tsx @@ -0,0 +1,607 @@ +import { + Button, + Checkbox, + Chip, + EmptyState, + Flex, + FormField, + Input2, + ListBoxFooter, + ListBoxItem, + PlusIcon, + ReturnIcon, + Select, + Tab, + TabList, + TicketIcon, + VisualInspectionIcon, + useSetBreadcrumbs, +} from '@pluralsh/design-system' +import { GqlError } from 'components/utils/Alert' +import { RectangleSkeleton } from 'components/utils/SkeletonLoaders' +import { useSimpleToast } from 'components/utils/SimpleToastContext' +import { StackedText } from 'components/utils/table/StackedText' +import { + WorkbenchWebhookFragment, + useCreateWorkbenchWebhookMutation, + useGetWorkbenchWebhookMutation, + useIssueWebhooksQuery, + useObservabilityWebhooksQuery, + useUpdateWorkbenchWebhookMutation, + useWorkbenchQuery, +} from 'generated/graphql' +import { isEmpty, isEqual } from 'lodash' +import { InlineA } from 'components/utils/typography/Text' +import { Key, useEffect, useMemo, useRef, useState } from 'react' +import { + useLocation, + useNavigate, + useParams, + useSearchParams, +} from 'react-router-dom' +import { useTheme } from 'styled-components' +import { mapExistingNodes } from 'utils/graphql' +import { + getWorkbenchWebhookTriggerCreateWebhookAbsPath, + getWorkbenchWebhookTriggersAbsPath, + WORKBENCH_PARAM_ID, + WORKBENCHES_WEBHOOK_SELECTED_QUERY_PARAM, + WORKBENCHES_WEBHOOK_PARAM_ID, +} from 'routes/workbenchesRoutesConsts' +import { getWorkbenchBreadcrumbs } from '../Workbench' +import { + FormCardSC, + StickyActionsFooterSC, +} from '../create-edit/WorkbenchCreateOrEdit' +import { + getIssueWebhookProviderIcon, + getObservabilityWebhookTypeIcon, +} from './utils' + +type MatchType = 'regex' | 'substring' + +// Prefixed key used in the Select: 'obs:{id}' for observability webhooks, 'issue:{id}' for issue webhooks +export type WebhookTriggerFormState = { + name: string + selectedWebhookKey: string + matchType: MatchType + regex: string + substring: string + caseInsensitive: boolean +} + +function parseWebhookKey(key: string): { + webhookId?: string + issueWebhookId?: string +} { + if (key.startsWith('obs:')) return { webhookId: key.slice(4) } + if (key.startsWith('issue:')) return { issueWebhookId: key.slice(6) } + return {} +} + +type RouteState = { + draftState?: WebhookTriggerFormState +} + +export function WebhookTriggerForm({ mode }: { mode: 'create' | 'edit' }) { + const theme = useTheme() + const location = useLocation() + const navigate = useNavigate() + const [searchParams] = useSearchParams() + const workbenchId = useParams()[WORKBENCH_PARAM_ID] ?? '' + const webhookId = useParams()[WORKBENCHES_WEBHOOK_PARAM_ID] + const routeState = location.state as Nullable + const webhookKeyParam = + searchParams.get(WORKBENCHES_WEBHOOK_SELECTED_QUERY_PARAM) ?? undefined + + const { + data: workbenchData, + loading: workbenchLoading, + error: workbenchError, + } = useWorkbenchQuery({ + variables: { id: workbenchId }, + skip: !workbenchId, + }) + const workbench = workbenchData?.workbench + + const [fetchWorkbenchWebhook, fetchWorkbenchWebhookState] = + useGetWorkbenchWebhookMutation() + + useEffect(() => { + if (mode === 'edit' && !!webhookId) + fetchWorkbenchWebhook({ variables: { id: webhookId } }) + }, [fetchWorkbenchWebhook, mode, webhookId]) + + const webhook = fetchWorkbenchWebhookState.data?.getWorkbenchWebhook + const editing = !!webhook + + // Source form state is based on the location state (if coming back from create webhook page) or the fetched webhook (if editing), + // with the selected webhook key from the query param taking precedence if present. + const sourceFormState = useMemo( + () => ({ + ...(routeState?.draftState ?? getInitialFormState(webhook)), + ...(webhookKeyParam ? { selectedWebhookKey: webhookKeyParam } : {}), + }), + [routeState?.draftState, webhookKeyParam, webhook] + ) + + // Local form draft state that can be modified as the user interacts with the form. + const [formDraft, setFormDraft] = + useState>(null) + + const formState = formDraft ?? sourceFormState + + // Updater that works with both the form draft (when it exists) and the source form state, + // allowing us to keep the draft in sync with changes to the source. + // This ensures that if the user goes to create a new webhook, selects it, and comes back, the form will update to + // reflect the newly selected webhook while still preserving any unsaved changes they made to other fields in the form. + const setFormState = ( + updater: (prev: WebhookTriggerFormState) => WebhookTriggerFormState + ) => { + setFormDraft((prevDraft) => updater(prevDraft ?? sourceFormState)) + } + + const { popToast } = useSimpleToast() + + // TODO: Add pagination for webhook queries. + const { + data: obsData, + loading: obsLoading, + error: obsError, + } = useObservabilityWebhooksQuery({ + variables: { first: 100 }, + fetchPolicy: 'cache-and-network', + }) + + const { + data: issueData, + loading: issueLoading, + error: issueWebhooksError, + } = useIssueWebhooksQuery({ + variables: { first: 100 }, + fetchPolicy: 'cache-and-network', + }) + + const observabilityWebhooks = useMemo( + () => mapExistingNodes(obsData?.observabilityWebhooks), + [obsData] + ) + const issueWebhooks = useMemo( + () => mapExistingNodes(issueData?.issueWebhooks), + [issueData] + ) + const selectedWebhookIcon = useMemo(() => { + const selectedKey = formState.selectedWebhookKey + if (!selectedKey) return undefined + + if (selectedKey.startsWith('obs:')) { + const selectedWebhook = observabilityWebhooks.find( + (wh) => `obs:${wh.id}` === selectedKey + ) + + return selectedWebhook + ? getObservabilityWebhookTypeIcon(selectedWebhook.type) + : undefined + } + + if (selectedKey.startsWith('issue:')) { + const selectedWebhook = issueWebhooks.find( + (wh) => `issue:${wh.id}` === selectedKey + ) + + return selectedWebhook + ? getIssueWebhookProviderIcon(selectedWebhook.provider) + : undefined + } + + return undefined + }, [formState.selectedWebhookKey, observabilityWebhooks, issueWebhooks]) + + const webhooksLoading = obsLoading || issueLoading + const tabStateRef = useRef(undefined) + + const label = formState.name.trim() + const selectedWebhookKeyValue = formState.selectedWebhookKey + const regex = formState.regex.trim() + const substring = formState.substring.trim() + const activeMatchValue = formState.matchType === 'regex' ? regex : substring + + const attributes = { + name: label, + ...parseWebhookKey(selectedWebhookKeyValue), + matches: activeMatchValue + ? formState.matchType === 'regex' + ? { regex: activeMatchValue } + : { + substring: activeMatchValue, + caseInsensitive: formState.caseInsensitive, + } + : undefined, + } + + const canSave = + !!label && + !!selectedWebhookKeyValue && + !!activeMatchValue && + !isEqual(attributes, getAttributesFromState(getInitialFormState(webhook))) + + const handleCompleted = () => { + popToast({ + name: label, + action: editing ? 'updated' : 'created', + color: 'icon-success', + }) + + navigate(getWorkbenchWebhookTriggersAbsPath(workbenchId)) + } + + const [createWorkbenchWebhook, createState] = + useCreateWorkbenchWebhookMutation({ + variables: { workbenchId, attributes }, + onCompleted: handleCompleted, + refetchQueries: ['WorkbenchWebhooks', 'WorkbenchTriggersSummary'], + awaitRefetchQueries: true, + }) + const [updateWorkbenchWebhook, updateState] = + useUpdateWorkbenchWebhookMutation({ + variables: { id: webhook?.id ?? '', attributes }, + onCompleted: handleCompleted, + refetchQueries: ['WorkbenchWebhooks', 'WorkbenchTriggersSummary'], + awaitRefetchQueries: true, + }) + + const isSaving = createState.loading || updateState.loading + const formError = + obsError ?? issueWebhooksError ?? createState.error ?? updateState.error + + const handleSave = () => { + if (!canSave) return + if (editing && webhook) updateWorkbenchWebhook() + else createWorkbenchWebhook() + } + + useSetBreadcrumbs( + useMemo( + () => [ + ...getWorkbenchBreadcrumbs(workbench), + { + label: 'webhook trigger', + url: getWorkbenchWebhookTriggersAbsPath(workbenchId), + }, + { label: mode === 'create' ? 'create' : 'edit' }, + ], + [mode, workbench, workbenchId] + ) + ) + + if (workbenchError) return + + if (fetchWorkbenchWebhookState.error) + return + + if (mode === 'edit' && !fetchWorkbenchWebhookState.loading && !webhook) + return ( + + + + ) + + const isLoading = + (!workbenchData && workbenchLoading) || + (mode === 'edit' && fetchWorkbenchWebhookState.loading) + + return ( + + + + {isLoading ? ( + + ) : ( + + + {formError && } + + + setFormState((prev) => ({ ...prev, name: e.target.value })) + } + placeholder="Webhook label" + /> + + + +
    + Select webhook* +
    + { + e.preventDefault() + navigate( + getWorkbenchWebhookTriggerCreateWebhookAbsPath( + workbenchId + ), + { + state: { + returnPath: `${location.pathname}${location.search}`, + draftState: formState, + }, + } + ) + }} + css={{ display: 'flex', alignItems: 'center', gap: '4px' }} + > + Create new webhook + +
    + + + +
    + + setFormState((prev) => ({ + ...prev, + matchType: String(key) as MatchType, + })), + }} + > + + Substring + + + REGEX + + + {formState.matchType === 'regex' ? ( + + + setFormState((prev) => ({ + ...prev, + regex: e.target.value, + })) + } + placeholder="REGEX" + /> + + ) : ( + <> + + + + setFormState((prev) => ({ + ...prev, + substring: e.target.value, + })) + } + placeholder="Substring" + /> + + + + setFormState((prev) => ({ + ...prev, + caseInsensitive: e.target.checked, + })) + } + > + Case insensitive match + + + )} + + + + +
    +
    + )} +
    +
    + ) +} + +function getInitialFormState( + webhook?: Nullable +): WebhookTriggerFormState { + const matchType: MatchType = webhook?.matches?.regex ? 'regex' : 'substring' + + let selectedWebhookKey = '' + if (webhook?.webhook?.id) selectedWebhookKey = `obs:${webhook.webhook.id}` + else if (webhook?.issueWebhook?.id) + selectedWebhookKey = `issue:${webhook.issueWebhook.id}` + + return { + name: webhook?.name ?? '', + selectedWebhookKey, + matchType, + regex: webhook?.matches?.regex ?? '', + substring: webhook?.matches?.substring ?? '', + caseInsensitive: webhook?.matches?.caseInsensitive ?? false, + } +} + +function getAttributesFromState(formState: WebhookTriggerFormState) { + const name = formState.name.trim() + const regex = formState.regex.trim() + const substring = formState.substring.trim() + const activeMatchValue = formState.matchType === 'regex' ? regex : substring + + return { + name, + ...parseWebhookKey(formState.selectedWebhookKey), + matches: activeMatchValue + ? formState.matchType === 'regex' + ? { regex: activeMatchValue } + : { + substring: activeMatchValue, + caseInsensitive: formState.caseInsensitive, + } + : undefined, + } +} diff --git a/assets/src/components/workbenches/workbench/webhooks/WebhookTriggers.tsx b/assets/src/components/workbenches/workbench/webhooks/WebhookTriggers.tsx new file mode 100644 index 0000000000..6129c0fd18 --- /dev/null +++ b/assets/src/components/workbenches/workbench/webhooks/WebhookTriggers.tsx @@ -0,0 +1,309 @@ +import { + Button, + Card, + EmptyState, + Flex, + IconFrame, + PencilIcon, + Table, + TrashCanIcon, + useSetBreadcrumbs, +} from '@pluralsh/design-system' +import { createColumnHelper } from '@tanstack/react-table' +import { GqlError } from 'components/utils/Alert' +import { StretchedFlex } from 'components/utils/StretchedFlex' +import { StackedText } from 'components/utils/table/StackedText' +import { useFetchPaginatedData } from 'components/utils/table/useFetchPaginatedData' +import { Body2P } from 'components/utils/typography/Text' +import { + useWorkbenchQuery, + useWorkbenchWebhooksQuery, + WorkbenchWebhookFragment, +} from 'generated/graphql' +import { useMemo, useState } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { + getWorkbenchWebhookTriggerCreateWebhookAbsPath, + getWorkbenchWebhookTriggerCreateAbsPath, + getWorkbenchWebhookTriggerEditAbsPath, + getWorkbenchWebhookTriggersAbsPath, + WORKBENCH_PARAM_ID, +} from 'routes/workbenchesRoutesConsts' +import { useTheme } from 'styled-components' +import { mapExistingNodes } from 'utils/graphql' +import { getWorkbenchBreadcrumbs } from '../Workbench' +import { WebhookTriggerDeleteModal } from './WebhookTriggerDeleteModal' +import isEmpty from 'lodash/isEmpty' +import { webhookTypeIcon, webhookTypeLabel, webhookURL } from './utils' + +export function WebhookTriggers() { + const navigate = useNavigate() + const theme = useTheme() + const workbenchId = useParams()[WORKBENCH_PARAM_ID] ?? '' + const [deletingWebhook, setDeletingWebhook] = + useState>(null) + + const { + data: workbenchData, + loading: workbenchLoading, + error: workbenchError, + } = useWorkbenchQuery({ + variables: { id: workbenchId }, + skip: !workbenchId, + }) + + const workbench = workbenchData?.workbench + + const { data, loading, error, pageInfo, fetchNextPage, setVirtualSlice } = + useFetchPaginatedData( + { + queryHook: useWorkbenchWebhooksQuery, + keyPath: ['workbench', 'webhooks'], + }, + { id: workbenchId } + ) + + const webhooks = useMemo( + () => mapExistingNodes(data?.workbench?.webhooks), + [data] + ) + + useSetBreadcrumbs( + useMemo( + () => [ + ...getWorkbenchBreadcrumbs(workbench), + { label: 'webhook trigger' }, + ], + [workbench] + ) + ) + + const columns = useMemo( + () => + getColumns({ + onEdit: (webhook) => + navigate( + getWorkbenchWebhookTriggerEditAbsPath({ + workbenchId, + webhookId: webhook.id, + }) + ), + onDelete: (webhook) => setDeletingWebhook(webhook), + }), + [navigate, workbenchId] + ) + + if (workbenchError) return + + if (error) return + + return ( + + + + {!!data && isEmpty(webhooks) ? ( + + + + + + + + + ) : ( + + + + Add webhooks to trigger this workbench. + + + + + + +
    + + )} + + setDeletingWebhook(null)} + /> + + ) +} + +const columnHelper = createColumnHelper() +function getColumns({ + onEdit, + onDelete, +}: { + onEdit: (webhook: WorkbenchWebhookFragment) => void + onDelete: (webhook: WorkbenchWebhookFragment) => void +}) { + return [ + columnHelper.accessor((webhook) => webhook, { + id: 'icons', + meta: { gridTemplate: '46px' }, + cell: ({ getValue }) => { + const webhook = getValue() + + return ( + + ) + }, + }), + columnHelper.accessor((webhook) => webhook, { + id: 'details', + meta: { truncate: true, gridTemplate: 'minmax(0, 1fr)' }, + cell: ({ getValue }) => { + const webhook = getValue() + + return ( + + ) + }, + }), + columnHelper.display({ + id: 'actions', + meta: { gridTemplate: '100px' }, + cell: ({ row }) => ( + + } + onClick={() => onEdit(row.original)} + /> + } + onClick={() => onDelete(row.original)} + /> + + ), + }), + ] +} diff --git a/assets/src/components/workbenches/workbench/webhooks/utils.tsx b/assets/src/components/workbenches/workbench/webhooks/utils.tsx new file mode 100644 index 0000000000..125979e237 --- /dev/null +++ b/assets/src/components/workbenches/workbench/webhooks/utils.tsx @@ -0,0 +1,70 @@ +import { + DatadogLogoIcon, + GitHubLogoIcon, + GitLabLogoIcon, + GrafanaLogoIcon, + NewrelicLogoIcon, + PagerdutyLogoIcon, + SentryLogoIcon, + TicketIcon, + VisualInspectionIcon, + WebhooksIcon, +} from '@pluralsh/design-system' + +import { + IssueWebhookProvider, + ObservabilityWebhookType, + WorkbenchWebhookFragment, +} from 'generated/graphql' + +export function webhookURL(webhook: WorkbenchWebhookFragment) { + if (webhook.issueWebhook) return webhook.issueWebhook.url + + if (webhook.webhook) return webhook.webhook.url + + return undefined +} + +export function webhookTypeIcon(webhook: WorkbenchWebhookFragment) { + if (webhook.issueWebhook) return + + return +} + +export function webhookTypeLabel(webhook: WorkbenchWebhookFragment) { + if (webhook.issueWebhook) return 'Ticketing' + + return 'Observability' +} + +export function getObservabilityWebhookTypeIcon(type: Nullable) { + switch (type) { + case ObservabilityWebhookType.Grafana: + return + case ObservabilityWebhookType.Datadog: + return + case ObservabilityWebhookType.Newrelic: + return + case ObservabilityWebhookType.Pagerduty: + return + case ObservabilityWebhookType.Sentry: + return + case ObservabilityWebhookType.Plural: + default: + return + } +} + +export function getIssueWebhookProviderIcon(provider: Nullable) { + switch (provider) { + case IssueWebhookProvider.Github: + return + case IssueWebhookProvider.Gitlab: + return + case IssueWebhookProvider.Jira: + case IssueWebhookProvider.Linear: + case IssueWebhookProvider.Asana: + default: + return + } +} diff --git a/assets/src/generated/graphql.ts b/assets/src/generated/graphql.ts index adb05d9e7e..3110470202 100644 --- a/assets/src/generated/graphql.ts +++ b/assets/src/generated/graphql.ts @@ -947,6 +947,8 @@ export type Alert = { insight?: Maybe; /** Detailed message or summary supplied by the provider */ message?: Maybe; + /** Raw webhook payload received for this alert */ + payload?: Maybe; /** The project this alert was associated with */ project?: Maybe; /** The human‑authored resolution for this alert, if one exists */ @@ -5086,6 +5088,8 @@ export type Issue = { /** the unique identifier of the issue */ id: Scalars['ID']['output']; insertedAt?: Maybe; + /** raw webhook payload received for this issue */ + payload?: Maybe; /** the provider (e.g., Linear, GitHub) that originated this issue */ provider: IssueWebhookProvider; /** the current status of the issue (e.g., open, in progress, completed, cancelled) */ @@ -8538,6 +8542,8 @@ export type RootMutationType = { cancelAgentRun?: Maybe; /** Cancels a chat message, if the user has access to the thread, by just deleting the chat record */ cancelChat?: Maybe; + /** Cancels a workbench job. Allowed for the job owner or users with write access to the workbench. */ + cancelWorkbenchJob?: Maybe; /** saves a set of messages and generates a new one transactionally */ chat?: Maybe; /** Wipes your current chat history blank */ @@ -8612,6 +8618,10 @@ export type RootMutationType = { /** Creates a new workbench job. Requires read access to the workbench. */ createWorkbenchJob?: Maybe; createWorkbenchMessage?: Maybe; + /** Creates a saved prompt for a workbench. Requires read access to the workbench. */ + createWorkbenchPrompt?: Maybe; + /** Creates a saved skill for a workbench. Requires write access to the workbench. */ + createWorkbenchSkill?: Maybe; createWorkbenchTool?: Maybe; createWorkbenchWebhook?: Maybe; deleteAccessToken?: Maybe; @@ -8675,6 +8685,10 @@ export type RootMutationType = { deleteVirtualCluster?: Maybe; deleteWorkbench?: Maybe; deleteWorkbenchCron?: Maybe; + /** Deletes a saved workbench prompt. Requires read access to the workbench. */ + deleteWorkbenchPrompt?: Maybe; + /** Deletes a saved workbench skill. Requires write access to the workbench. */ + deleteWorkbenchSkill?: Maybe; deleteWorkbenchTool?: Maybe; deleteWorkbenchWebhook?: Maybe; delinkBackups?: Maybe; @@ -8689,6 +8703,8 @@ export type RootMutationType = { fixResearchDiagram?: Maybe; /** forces a pipeline gate to be in open state */ forceGate?: Maybe; + /** Fetches a workbench webhook by id. Requires read access to the workbench. */ + getWorkbenchWebhook?: Maybe; /** Chat mutation that can also execute MCP servers in line with the overall completion */ hybridChat?: Maybe>>; impersonateServiceAccount?: Maybe; @@ -8797,6 +8813,12 @@ export type RootMutationType = { updateUser?: Maybe; updateWorkbench?: Maybe; updateWorkbenchCron?: Maybe; + /** Updates only the topology field on the job's result. Requires read access to the job's workbench; only the job owner may update. */ + updateWorkbenchJob?: Maybe; + /** Updates a saved workbench prompt. Requires read access to the workbench. */ + updateWorkbenchPrompt?: Maybe; + /** Updates a saved workbench skill. Requires write access to the workbench. */ + updateWorkbenchSkill?: Maybe; updateWorkbenchTool?: Maybe; updateWorkbenchWebhook?: Maybe; upsertAgentRuntime?: Maybe; @@ -8821,6 +8843,8 @@ export type RootMutationType = { upsertUser?: Maybe; upsertVirtualCluster?: Maybe; upsertVulnerabilities?: Maybe; + /** Fetches a workbench cron by id. Requires read access to the workbench. */ + workbenchCron?: Maybe; }; @@ -8884,6 +8908,11 @@ export type RootMutationTypeCancelChatArgs = { }; +export type RootMutationTypeCancelWorkbenchJobArgs = { + jobId: Scalars['ID']['input']; +}; + + export type RootMutationTypeChatArgs = { messages?: InputMaybe>>; threadId?: InputMaybe; @@ -9226,6 +9255,18 @@ export type RootMutationTypeCreateWorkbenchMessageArgs = { }; +export type RootMutationTypeCreateWorkbenchPromptArgs = { + attributes: WorkbenchPromptAttributes; + workbenchId: Scalars['ID']['input']; +}; + + +export type RootMutationTypeCreateWorkbenchSkillArgs = { + attributes: WorkbenchSkillAttributes; + workbenchId: Scalars['ID']['input']; +}; + + export type RootMutationTypeCreateWorkbenchToolArgs = { attributes?: InputMaybe; }; @@ -9534,6 +9575,16 @@ export type RootMutationTypeDeleteWorkbenchCronArgs = { }; +export type RootMutationTypeDeleteWorkbenchPromptArgs = { + id: Scalars['ID']['input']; +}; + + +export type RootMutationTypeDeleteWorkbenchSkillArgs = { + id: Scalars['ID']['input']; +}; + + export type RootMutationTypeDeleteWorkbenchToolArgs = { id: Scalars['ID']['input']; }; @@ -9576,6 +9627,11 @@ export type RootMutationTypeForceGateArgs = { }; +export type RootMutationTypeGetWorkbenchWebhookArgs = { + id: Scalars['ID']['input']; +}; + + export type RootMutationTypeHybridChatArgs = { messages?: InputMaybe>>; threadId?: InputMaybe; @@ -10079,6 +10135,24 @@ export type RootMutationTypeUpdateWorkbenchCronArgs = { }; +export type RootMutationTypeUpdateWorkbenchJobArgs = { + attributes: WorkbenchJobUpdateAttributes; + jobId: Scalars['ID']['input']; +}; + + +export type RootMutationTypeUpdateWorkbenchPromptArgs = { + attributes: WorkbenchPromptAttributes; + id: Scalars['ID']['input']; +}; + + +export type RootMutationTypeUpdateWorkbenchSkillArgs = { + attributes: WorkbenchSkillAttributes; + id: Scalars['ID']['input']; +}; + + export type RootMutationTypeUpdateWorkbenchToolArgs = { attributes?: InputMaybe; id: Scalars['ID']['input']; @@ -10197,6 +10271,11 @@ export type RootMutationTypeUpsertVulnerabilitiesArgs = { vulnerabilities?: InputMaybe>>; }; + +export type RootMutationTypeWorkbenchCronArgs = { + id: Scalars['ID']['input']; +}; + export type RootQueryType = { __typename?: 'RootQueryType'; accessToken?: Maybe; @@ -11848,6 +11927,7 @@ export type RootSubscriptionType = { workbenchJobActivityDelta?: Maybe; workbenchJobDelta?: Maybe; workbenchJobProgress?: Maybe; + workbenchTextStream?: Maybe; }; @@ -11898,6 +11978,11 @@ export type RootSubscriptionTypeWorkbenchJobProgressArgs = { jobId: Scalars['ID']['input']; }; + +export type RootSubscriptionTypeWorkbenchTextStreamArgs = { + jobId: Scalars['ID']['input']; +}; + export type RouterFilterAttributes = { /** whether to enable delivery for events associated with this cluster */ clusterId?: InputMaybe; @@ -14603,6 +14688,7 @@ export type Workbench = { name: Scalars['String']['output']; /** the project of this workbench */ project?: Maybe; + prompts?: Maybe; /** read policy for this service */ readBindings?: Maybe>>; /** the git repository for this workbench */ @@ -14616,6 +14702,7 @@ export type Workbench = { tools?: Maybe>>; updatedAt?: Maybe; webhooks?: Maybe; + workbenchSkills?: Maybe; /** write policy of this service */ writeBindings?: Maybe>>; }; @@ -14645,6 +14732,14 @@ export type WorkbenchIssuesArgs = { }; +export type WorkbenchPromptsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + + export type WorkbenchRunsArgs = { after?: InputMaybe; before?: InputMaybe; @@ -14660,6 +14755,14 @@ export type WorkbenchWebhooksArgs = { last?: InputMaybe; }; + +export type WorkbenchWorkbenchSkillsArgs = { + after?: InputMaybe; + before?: InputMaybe; + first?: InputMaybe; + last?: InputMaybe; +}; + export type WorkbenchAttributes = { /** the agent runtime for this workbench */ agentRuntimeId?: InputMaybe; @@ -14830,6 +14933,8 @@ export type WorkbenchJobActivity = { __typename?: 'WorkbenchJobActivity'; /** the agent run that executed this activity */ agentRun?: Maybe; + /** all agent runs associated with this activity (sideloadable) */ + agentRuns?: Maybe>>; /** the id of the activity */ id: Scalars['String']['output']; insertedAt?: Maybe; @@ -14912,6 +15017,7 @@ export enum WorkbenchJobActivityStatus { export enum WorkbenchJobActivityType { Coding = 'CODING', + Conclusion = 'CONCLUSION', Infrastructure = 'INFRASTRUCTURE', Integration = 'INTEGRATION', Memo = 'MEMO', @@ -15022,6 +15128,11 @@ export type WorkbenchJobThoughtAttributes = { metrics?: Maybe>>; }; +export type WorkbenchJobUpdateAttributes = { + /** the result for this job */ + result?: InputMaybe; +}; + export type WorkbenchMessageAttributes = { /** the prompt for the message */ prompt: Scalars['String']['input']; @@ -15042,6 +15153,77 @@ export type WorkbenchObservabilityAttributes = { metrics?: InputMaybe; }; +export type WorkbenchPrompt = { + __typename?: 'WorkbenchPrompt'; + /** the id of the saved prompt */ + id: Scalars['String']['output']; + insertedAt?: Maybe; + /** the saved prompt text */ + prompt?: Maybe; + updatedAt?: Maybe; + /** the workbench this prompt belongs to */ + workbench?: Maybe; +}; + +export type WorkbenchPromptAttributes = { + /** the saved prompt text */ + prompt: Scalars['String']['input']; +}; + +export type WorkbenchPromptConnection = { + __typename?: 'WorkbenchPromptConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type WorkbenchPromptEdge = { + __typename?: 'WorkbenchPromptEdge'; + cursor?: Maybe; + node?: Maybe; +}; + +export type WorkbenchResultAttributes = { + /** mermaid diagram text for the job result topology (only field clients may set via this mutation) */ + topology: Scalars['String']['input']; +}; + +export type WorkbenchSkill = { + __typename?: 'WorkbenchSkill'; + /** the saved skill contents */ + contents?: Maybe; + /** the saved skill description */ + description?: Maybe; + /** the id of the saved skill */ + id: Scalars['String']['output']; + insertedAt?: Maybe; + /** the saved skill name */ + name?: Maybe; + updatedAt?: Maybe; + /** the workbench this skill belongs to */ + workbench?: Maybe; +}; + +export type WorkbenchSkillAttributes = { + /** the saved skill contents */ + contents: Scalars['String']['input']; + /** the saved skill description */ + description?: InputMaybe; + /** the saved skill name */ + name: Scalars['String']['input']; +}; + +export type WorkbenchSkillConnection = { + __typename?: 'WorkbenchSkillConnection'; + edges?: Maybe>>; + pageInfo: PageInfo; +}; + +export type WorkbenchSkillEdge = { + __typename?: 'WorkbenchSkillEdge'; + cursor?: Maybe; + node?: Maybe; +}; + export type WorkbenchSkills = { __typename?: 'WorkbenchSkills'; /** files to include */ @@ -15057,6 +15239,12 @@ export type WorkbenchSkillsAttributes = { ref?: InputMaybe; }; +export type WorkbenchTextStream = { + __typename?: 'WorkbenchTextStream'; + activityId?: Maybe; + text?: Maybe; +}; + export type WorkbenchTool = { __typename?: 'WorkbenchTool'; /** categories for the tool */ @@ -15456,9 +15644,9 @@ export type YamlOverlayAttributes = { yaml: Scalars['String']['input']; }; -export type AgentRunTinyFragment = { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null }; +export type AgentRunTinyFragment = { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null }; -export type AgentRunFragment = { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, messages?: Array<{ __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null> | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null }; +export type AgentRunFragment = { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, messages?: Array<{ __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null> | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null }; export type AgentRuntimeFragment = { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType, aiProxy?: boolean | null, default?: boolean | null, cluster?: { __typename?: 'Cluster', self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null }; @@ -15478,9 +15666,9 @@ export type AgentMessageCostFragment = { __typename?: 'AgentMessageCost', total: export type AgentMessageDeltaFragment = { __typename?: 'AgentMessageDelta', delta?: Delta | null, payload?: { __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null }; -export type ClusterUpgradeStepFragment = { __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; +export type ClusterUpgradeStepFragment = { __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; -export type ClusterUpgradeFragment = { __typename?: 'ClusterUpgrade', id: string, status: ClusterUpgradeStatus, version?: string | null, steps?: Array<{ __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, runtime?: { __typename?: 'AgentRuntime', type: AgentRuntimeType, name: string } | null }; +export type ClusterUpgradeFragment = { __typename?: 'ClusterUpgrade', id: string, status: ClusterUpgradeStatus, version?: string | null, steps?: Array<{ __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, runtime?: { __typename?: 'AgentRuntime', type: AgentRuntimeType, name: string } | null }; export type AgentRunsQueryVariables = Exact<{ after?: InputMaybe; @@ -15489,21 +15677,21 @@ export type AgentRunsQueryVariables = Exact<{ }>; -export type AgentRunsQuery = { __typename?: 'RootQueryType', agentRuns?: { __typename?: 'AgentRunConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'AgentRunEdge', node?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null } | null }; +export type AgentRunsQuery = { __typename?: 'RootQueryType', agentRuns?: { __typename?: 'AgentRunConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'AgentRunEdge', node?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null } | null }; export type AgentRunTinyQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type AgentRunTinyQuery = { __typename?: 'RootQueryType', agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; +export type AgentRunTinyQuery = { __typename?: 'RootQueryType', agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; export type AgentRunQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type AgentRunQuery = { __typename?: 'RootQueryType', agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, messages?: Array<{ __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null> | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; +export type AgentRunQuery = { __typename?: 'RootQueryType', agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, messages?: Array<{ __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null> | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; export type AgentRunPodQueryVariables = Exact<{ id: Scalars['ID']['input']; @@ -15558,14 +15746,14 @@ export type CreateAgentRunMutationVariables = Exact<{ }>; -export type CreateAgentRunMutation = { __typename?: 'RootMutationType', createAgentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, messages?: Array<{ __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null> | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; +export type CreateAgentRunMutation = { __typename?: 'RootMutationType', createAgentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, messages?: Array<{ __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null> | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; export type CancelAgentRunMutationVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type CancelAgentRunMutation = { __typename?: 'RootMutationType', cancelAgentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; +export type CancelAgentRunMutation = { __typename?: 'RootMutationType', cancelAgentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; export type UpsertAgentRuntimeMutationVariables = Exact<{ attributes: AgentRuntimeAttributes; @@ -15580,7 +15768,7 @@ export type ShareAgentRunMutationVariables = Exact<{ }>; -export type ShareAgentRunMutation = { __typename?: 'RootMutationType', shareAgentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; +export type ShareAgentRunMutation = { __typename?: 'RootMutationType', shareAgentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; export type CreateClusterUpgradeMutationVariables = Exact<{ id: Scalars['ID']['input']; @@ -15588,7 +15776,7 @@ export type CreateClusterUpgradeMutationVariables = Exact<{ }>; -export type CreateClusterUpgradeMutation = { __typename?: 'RootMutationType', createClusterUpgrade?: { __typename?: 'ClusterUpgrade', id: string, status: ClusterUpgradeStatus, version?: string | null, steps?: Array<{ __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, runtime?: { __typename?: 'AgentRuntime', type: AgentRuntimeType, name: string } | null } | null }; +export type CreateClusterUpgradeMutation = { __typename?: 'RootMutationType', createClusterUpgrade?: { __typename?: 'ClusterUpgrade', id: string, status: ClusterUpgradeStatus, version?: string | null, steps?: Array<{ __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, runtime?: { __typename?: 'AgentRuntime', type: AgentRuntimeType, name: string } | null } | null }; export type AgentRunChatSubscriptionVariables = Exact<{ runId: Scalars['ID']['input']; @@ -15602,15 +15790,15 @@ export type AgentRunDeltaSubscriptionVariables = Exact<{ }>; -export type AgentRunDeltaSubscription = { __typename?: 'RootSubscriptionType', agentRunDelta?: { __typename?: 'AgentRunDelta', delta?: Delta | null, payload?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null } | null } | null }; +export type AgentRunDeltaSubscription = { __typename?: 'RootSubscriptionType', agentRunDelta?: { __typename?: 'AgentRunDelta', delta?: Delta | null, payload?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null } | null } | null }; -export type ChatFragment = { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null }; +export type ChatFragment = { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null }; export type ChatThreadTinyFragment = { __typename?: 'ChatThread', id: string, default: boolean, summary: string, insertedAt?: string | null, updatedAt?: string | null, lastMessageAt?: string | null, settings?: { __typename?: 'ChatThreadSettings', memory?: boolean | null } | null, insight?: { __typename?: 'AiInsight', id: string, summary?: string | null, freshness?: InsightFreshness | null, insertedAt?: string | null, updatedAt?: string | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, flow?: { __typename?: 'Flow', id: string, name: string, icon?: string | null } | null, session?: { __typename?: 'AgentSession', id: string, type?: AgentSessionType | null, done?: boolean | null, planConfirmed?: boolean | null, thread?: { __typename?: 'ChatThread', id: string, summary: string, insertedAt?: string | null, lastMessageAt?: string | null } | null, connection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, cluster?: { __typename?: 'Cluster', id: string } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string } | null, pullRequest?: { __typename?: 'PullRequest', id: string, url: string } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string } | null } | null } | null, research?: { __typename?: 'InfraResearch', id: string, prompt?: string | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null }; export type ChatThreadDetailsFragment = { __typename?: 'ChatThread', id: string, default: boolean, summary: string, insertedAt?: string | null, updatedAt?: string | null, lastMessageAt?: string | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, tools?: Array<{ __typename?: 'McpServerTool', tool?: { __typename?: 'McpTool', name: string, description?: string | null, inputSchema?: Record | null } | null, server?: { __typename?: 'McpServer', id: string, name: string, url: string, confirm?: boolean | null, readBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, authentication?: { __typename?: 'McpServerAuthentication', plural?: boolean | null, headers?: Array<{ __typename?: 'McpServerHeader', name: string, value: string } | null> | null } | null } | null } | null> | null, settings?: { __typename?: 'ChatThreadSettings', memory?: boolean | null } | null, flow?: { __typename?: 'Flow', id: string, name: string, icon?: string | null } | null, session?: { __typename?: 'AgentSession', id: string, type?: AgentSessionType | null, done?: boolean | null, planConfirmed?: boolean | null, thread?: { __typename?: 'ChatThread', id: string, summary: string, insertedAt?: string | null, lastMessageAt?: string | null } | null, connection?: { __typename?: 'CloudConnection', id: string, name: string, provider: Provider } | null, cluster?: { __typename?: 'Cluster', id: string } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string } | null, pullRequest?: { __typename?: 'PullRequest', id: string, url: string } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string } | null } | null } | null, research?: { __typename?: 'InfraResearch', id: string, prompt?: string | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null }; -export type ChatThreadMessagesFragment = { __typename?: 'ChatThread', id: string, chats?: { __typename?: 'ChatConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ChatEdge', node?: { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null } | null> | null } | null }; +export type ChatThreadMessagesFragment = { __typename?: 'ChatThread', id: string, chats?: { __typename?: 'ChatConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ChatEdge', node?: { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null } | null> | null } | null }; export type ChatThreadSettingsFragment = { __typename?: 'ChatThreadSettings', memory?: boolean | null }; @@ -15659,7 +15847,7 @@ export type ChatThreadMessagesQueryVariables = Exact<{ }>; -export type ChatThreadMessagesQuery = { __typename?: 'RootQueryType', chatThread?: { __typename?: 'ChatThread', id: string, chats?: { __typename?: 'ChatConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ChatEdge', node?: { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null } | null> | null } | null } | null }; +export type ChatThreadMessagesQuery = { __typename?: 'RootQueryType', chatThread?: { __typename?: 'ChatThread', id: string, chats?: { __typename?: 'ChatConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ChatEdge', node?: { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null } | null> | null } | null } | null }; export type AgentSessionsQueryVariables = Exact<{ first?: InputMaybe; @@ -15688,28 +15876,28 @@ export type HybridChatMutationVariables = Exact<{ }>; -export type HybridChatMutation = { __typename?: 'RootMutationType', hybridChat?: Array<{ __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null> | null }; +export type HybridChatMutation = { __typename?: 'RootMutationType', hybridChat?: Array<{ __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null> | null }; export type ConfirmChatMutationVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type ConfirmChatMutation = { __typename?: 'RootMutationType', confirmChat?: { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null }; +export type ConfirmChatMutation = { __typename?: 'RootMutationType', confirmChat?: { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null }; export type ConfirmChatPlanMutationVariables = Exact<{ threadId: Scalars['ID']['input']; }>; -export type ConfirmChatPlanMutation = { __typename?: 'RootMutationType', confirmPlan?: Array<{ __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null> | null }; +export type ConfirmChatPlanMutation = { __typename?: 'RootMutationType', confirmPlan?: Array<{ __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null> | null }; export type DeleteChatMutationVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type DeleteChatMutation = { __typename?: 'RootMutationType', deleteChat?: { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null }; +export type DeleteChatMutation = { __typename?: 'RootMutationType', deleteChat?: { __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null }; export type CreateChatThreadMutationVariables = Exact<{ attributes: ChatThreadAttributes; @@ -15748,7 +15936,7 @@ export type AddChatContextMutationVariables = Exact<{ }>; -export type AddChatContextMutation = { __typename?: 'RootMutationType', addChatContext?: Array<{ __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null> | null }; +export type AddChatContextMutation = { __typename?: 'RootMutationType', addChatContext?: Array<{ __typename?: 'Chat', id: string, content?: string | null, role: AiRole, seq: number, type: ChatType, confirm?: boolean | null, confirmedAt?: string | null, insertedAt?: string | null, updatedAt?: string | null, attributes?: { __typename?: 'ChatTypeAttributes', file?: { __typename?: 'ChatFile', name?: string | null } | null, tool?: { __typename?: 'ChatTool', name?: string | null, arguments?: Record | null } | null, prCall?: { __typename?: 'PrCallAttributes', context?: Record | null, branch?: string | null } | null } | null, pullRequest?: { __typename?: 'PullRequest', labels?: Array | null, patch?: string | null, id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, prAutomation?: { __typename?: 'PrAutomation', id: string, name: string, icon?: string | null, darkIcon?: string | null, documentation?: string | null, addon?: string | null, identifier?: string | null, role?: PrRole | null, cluster?: { __typename?: 'Cluster', protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string } | null, connection?: { __typename?: 'ScmConnection', id: string, name: string, insertedAt?: string | null, updatedAt?: string | null, type: ScmType, username?: string | null, baseUrl?: string | null, apiUrl?: string | null, azure?: { __typename?: 'AzureDevopsConfiguration', username: string, organization: string, project: string } | null } | null, createBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, writeBindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: Array<{ __typename?: 'PrConfiguration', values?: Array | null, default?: string | null, documentation?: string | null, displayName?: string | null, longform?: string | null, name: string, optional?: boolean | null, placeholder?: string | null, type: ConfigurationType, page?: number | null, condition?: { __typename?: 'PrConfigurationCondition', field: string, operation: Operation, value?: string | null } | null } | null> | null, confirmation?: { __typename?: 'PrConfirmation', text?: string | null, checklist?: Array<{ __typename?: 'PrChecklist', label: string } | null> | null } | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null, server?: { __typename?: 'McpServer', id: string, name: string } | null } | null> | null }; export type CreateAgentSessionMutationVariables = Exact<{ attributes: AgentSessionAttributes; @@ -17684,7 +17872,7 @@ export type CloudAddonUpgradeFragment = { __typename?: 'CloudAddonUpgrade', addo export type ClusterWithUpgradeFragment = { __typename?: 'Cluster', id: string, name: string, currentVersion?: string | null, version?: string | null, deprecatedCustomResources?: Array<{ __typename?: 'DeprecatedCustomResource', name?: string | null, group: string, kind: string, namespace?: string | null, version: string, nextVersion: string } | null> | null, upgradePlanSummary?: { __typename?: 'UpgradePlanSummary', blockingAddons?: Array<{ __typename?: 'RuntimeAddonUpgrade', callout?: string | null, addon?: { __typename?: 'RuntimeAddon', name: string, icon?: string | null } | null, current?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null } | null, fix?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null, summary?: { __typename?: 'AddonVersionSummary', breakingChanges?: Array | null, chartUpdates?: Array | null, features?: Array | null, helmChanges?: string | null } | null } | null } | null> | null, blockingCloudAddons?: Array<{ __typename?: 'CloudAddonUpgrade', addon?: { __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null, current?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null, fix?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null } | null> | null } | null, runtimeServices?: Array<{ __typename?: 'RuntimeService', id: string, name: string, version: string, addon?: { __typename?: 'RuntimeAddon', icon?: string | null, versions?: Array<{ __typename?: 'AddonVersion', version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null> | null } | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null, helm?: { __typename?: 'HelmSpec', version?: string | null } | null } | null, addonVersion?: { __typename?: 'AddonVersion', blocking?: boolean | null, version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null } | null> | null, apiDeprecations?: Array<{ __typename?: 'ApiDeprecation', availableIn?: string | null, blocking?: boolean | null, deprecatedIn?: string | null, removedIn?: string | null, replacement?: string | null, component?: { __typename?: 'ServiceComponent', group?: string | null, version?: string | null, kind: string, name: string, namespace?: string | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null } | null } | null } | null> | null, upgradeInsights?: Array<{ __typename?: 'UpgradeInsight', id: string, name: string, description?: string | null, refreshedAt?: string | null, transitionedAt?: string | null, version?: string | null, status?: UpgradeInsightStatus | null, details?: Array<{ __typename?: 'UpgradeInsightDetail', id: string, removedIn?: string | null, replacedIn?: string | null, replacement?: string | null, status?: UpgradeInsightStatus | null, used?: string | null, clientInfo?: Array<{ __typename?: 'InsightClientInfo', userAgent?: string | null, count?: string | null, lastRequestAt?: string | null } | null> | null } | null> | null } | null> | null, cloudAddons?: Array<{ __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null> | null }; -export type ClusterOverviewDetailsFragment = { __typename?: 'Cluster', currentVersion?: string | null, id: string, self?: boolean | null, healthy?: boolean | null, healthScore?: number | null, protect?: boolean | null, name: string, handle?: string | null, distro?: ClusterDistro | null, cpuTotal?: number | null, memoryTotal?: number | null, cpuUtil?: number | null, nodeCount?: number | null, namespaceCount?: number | null, availabilityZones?: Array | null, podCount?: number | null, memoryUtil?: number | null, installed?: boolean | null, pingedAt?: string | null, deletedAt?: string | null, version?: string | null, kubeletVersion?: string | null, virtual?: boolean | null, disableAi?: boolean | null, currentUpgrade?: { __typename?: 'ClusterUpgrade', id: string, status: ClusterUpgradeStatus, version?: string | null, steps?: Array<{ __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, runtime?: { __typename?: 'AgentRuntime', type: AgentRuntimeType, name: string } | null } | null, nodeStatistics?: Array<{ __typename?: 'NodeStatistic', id: string, name: string, pendingPods?: number | null, health?: NodeStatisticHealth | null, insertedAt?: string | null, updatedAt?: string | null, cluster?: { __typename?: 'Cluster', id: string } | null } | null> | null, provider?: { __typename?: 'ClusterProvider', id: string, cloud: string, name: string, namespace: string, supportedVersions?: Array | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, repository?: { __typename?: 'GitRepository', url: string } | null } | null, tags?: Array<{ __typename?: 'Tag', name: string, value: string } | null> | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null, kubeletSkew?: boolean | null } | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, extendedSupport?: { __typename?: 'ExtendedSupportInfo', extended?: boolean | null, extendedFrom?: string | null } | null, deprecatedCustomResources?: Array<{ __typename?: 'DeprecatedCustomResource', name?: string | null, group: string, kind: string, namespace?: string | null, version: string, nextVersion: string } | null> | null, upgradePlanSummary?: { __typename?: 'UpgradePlanSummary', blockingAddons?: Array<{ __typename?: 'RuntimeAddonUpgrade', callout?: string | null, addon?: { __typename?: 'RuntimeAddon', name: string, icon?: string | null } | null, current?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null } | null, fix?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null, summary?: { __typename?: 'AddonVersionSummary', breakingChanges?: Array | null, chartUpdates?: Array | null, features?: Array | null, helmChanges?: string | null } | null } | null } | null> | null, blockingCloudAddons?: Array<{ __typename?: 'CloudAddonUpgrade', addon?: { __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null, current?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null, fix?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null } | null> | null } | null, insightComponents?: Array<{ __typename?: 'ClusterInsightComponent', id: string, kind: string, name: string, namespace?: string | null, group?: string | null, version: string, priority?: InsightComponentPriority | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null } | null> | null, runtimeServices?: Array<{ __typename?: 'RuntimeService', id: string, name: string, version: string, addon?: { __typename?: 'RuntimeAddon', icon?: string | null, versions?: Array<{ __typename?: 'AddonVersion', version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null> | null } | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null, helm?: { __typename?: 'HelmSpec', version?: string | null } | null } | null, addonVersion?: { __typename?: 'AddonVersion', blocking?: boolean | null, version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null } | null> | null, apiDeprecations?: Array<{ __typename?: 'ApiDeprecation', availableIn?: string | null, blocking?: boolean | null, deprecatedIn?: string | null, removedIn?: string | null, replacement?: string | null, component?: { __typename?: 'ServiceComponent', group?: string | null, version?: string | null, kind: string, name: string, namespace?: string | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null } | null } | null } | null> | null, upgradeInsights?: Array<{ __typename?: 'UpgradeInsight', id: string, name: string, description?: string | null, refreshedAt?: string | null, transitionedAt?: string | null, version?: string | null, status?: UpgradeInsightStatus | null, details?: Array<{ __typename?: 'UpgradeInsightDetail', id: string, removedIn?: string | null, replacedIn?: string | null, replacement?: string | null, status?: UpgradeInsightStatus | null, used?: string | null, clientInfo?: Array<{ __typename?: 'InsightClientInfo', userAgent?: string | null, count?: string | null, lastRequestAt?: string | null } | null> | null } | null> | null } | null> | null, cloudAddons?: Array<{ __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null> | null }; +export type ClusterOverviewDetailsFragment = { __typename?: 'Cluster', currentVersion?: string | null, id: string, self?: boolean | null, healthy?: boolean | null, healthScore?: number | null, protect?: boolean | null, name: string, handle?: string | null, distro?: ClusterDistro | null, cpuTotal?: number | null, memoryTotal?: number | null, cpuUtil?: number | null, nodeCount?: number | null, namespaceCount?: number | null, availabilityZones?: Array | null, podCount?: number | null, memoryUtil?: number | null, installed?: boolean | null, pingedAt?: string | null, deletedAt?: string | null, version?: string | null, kubeletVersion?: string | null, virtual?: boolean | null, disableAi?: boolean | null, currentUpgrade?: { __typename?: 'ClusterUpgrade', id: string, status: ClusterUpgradeStatus, version?: string | null, steps?: Array<{ __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, runtime?: { __typename?: 'AgentRuntime', type: AgentRuntimeType, name: string } | null } | null, nodeStatistics?: Array<{ __typename?: 'NodeStatistic', id: string, name: string, pendingPods?: number | null, health?: NodeStatisticHealth | null, insertedAt?: string | null, updatedAt?: string | null, cluster?: { __typename?: 'Cluster', id: string } | null } | null> | null, provider?: { __typename?: 'ClusterProvider', id: string, cloud: string, name: string, namespace: string, supportedVersions?: Array | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, repository?: { __typename?: 'GitRepository', url: string } | null } | null, tags?: Array<{ __typename?: 'Tag', name: string, value: string } | null> | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null, kubeletSkew?: boolean | null } | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, extendedSupport?: { __typename?: 'ExtendedSupportInfo', extended?: boolean | null, extendedFrom?: string | null } | null, deprecatedCustomResources?: Array<{ __typename?: 'DeprecatedCustomResource', name?: string | null, group: string, kind: string, namespace?: string | null, version: string, nextVersion: string } | null> | null, upgradePlanSummary?: { __typename?: 'UpgradePlanSummary', blockingAddons?: Array<{ __typename?: 'RuntimeAddonUpgrade', callout?: string | null, addon?: { __typename?: 'RuntimeAddon', name: string, icon?: string | null } | null, current?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null } | null, fix?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null, summary?: { __typename?: 'AddonVersionSummary', breakingChanges?: Array | null, chartUpdates?: Array | null, features?: Array | null, helmChanges?: string | null } | null } | null } | null> | null, blockingCloudAddons?: Array<{ __typename?: 'CloudAddonUpgrade', addon?: { __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null, current?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null, fix?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null } | null> | null } | null, insightComponents?: Array<{ __typename?: 'ClusterInsightComponent', id: string, kind: string, name: string, namespace?: string | null, group?: string | null, version: string, priority?: InsightComponentPriority | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null } | null> | null, runtimeServices?: Array<{ __typename?: 'RuntimeService', id: string, name: string, version: string, addon?: { __typename?: 'RuntimeAddon', icon?: string | null, versions?: Array<{ __typename?: 'AddonVersion', version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null> | null } | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null, helm?: { __typename?: 'HelmSpec', version?: string | null } | null } | null, addonVersion?: { __typename?: 'AddonVersion', blocking?: boolean | null, version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null } | null> | null, apiDeprecations?: Array<{ __typename?: 'ApiDeprecation', availableIn?: string | null, blocking?: boolean | null, deprecatedIn?: string | null, removedIn?: string | null, replacement?: string | null, component?: { __typename?: 'ServiceComponent', group?: string | null, version?: string | null, kind: string, name: string, namespace?: string | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null } | null } | null } | null> | null, upgradeInsights?: Array<{ __typename?: 'UpgradeInsight', id: string, name: string, description?: string | null, refreshedAt?: string | null, transitionedAt?: string | null, version?: string | null, status?: UpgradeInsightStatus | null, details?: Array<{ __typename?: 'UpgradeInsightDetail', id: string, removedIn?: string | null, replacedIn?: string | null, replacement?: string | null, status?: UpgradeInsightStatus | null, used?: string | null, clientInfo?: Array<{ __typename?: 'InsightClientInfo', userAgent?: string | null, count?: string | null, lastRequestAt?: string | null } | null> | null } | null> | null } | null> | null, cloudAddons?: Array<{ __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null> | null }; export type ClusterHealthScoreFragment = { __typename?: 'Cluster', id: string, name: string, healthScore?: number | null }; @@ -17698,7 +17886,7 @@ export type ClusterOverviewDetailsQueryVariables = Exact<{ }>; -export type ClusterOverviewDetailsQuery = { __typename?: 'RootQueryType', cluster?: { __typename?: 'Cluster', currentVersion?: string | null, id: string, self?: boolean | null, healthy?: boolean | null, healthScore?: number | null, protect?: boolean | null, name: string, handle?: string | null, distro?: ClusterDistro | null, cpuTotal?: number | null, memoryTotal?: number | null, cpuUtil?: number | null, nodeCount?: number | null, namespaceCount?: number | null, availabilityZones?: Array | null, podCount?: number | null, memoryUtil?: number | null, installed?: boolean | null, pingedAt?: string | null, deletedAt?: string | null, version?: string | null, kubeletVersion?: string | null, virtual?: boolean | null, disableAi?: boolean | null, currentUpgrade?: { __typename?: 'ClusterUpgrade', id: string, status: ClusterUpgradeStatus, version?: string | null, steps?: Array<{ __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, runtime?: { __typename?: 'AgentRuntime', type: AgentRuntimeType, name: string } | null } | null, nodeStatistics?: Array<{ __typename?: 'NodeStatistic', id: string, name: string, pendingPods?: number | null, health?: NodeStatisticHealth | null, insertedAt?: string | null, updatedAt?: string | null, cluster?: { __typename?: 'Cluster', id: string } | null } | null> | null, provider?: { __typename?: 'ClusterProvider', id: string, cloud: string, name: string, namespace: string, supportedVersions?: Array | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, repository?: { __typename?: 'GitRepository', url: string } | null } | null, tags?: Array<{ __typename?: 'Tag', name: string, value: string } | null> | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null, kubeletSkew?: boolean | null } | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, extendedSupport?: { __typename?: 'ExtendedSupportInfo', extended?: boolean | null, extendedFrom?: string | null } | null, deprecatedCustomResources?: Array<{ __typename?: 'DeprecatedCustomResource', name?: string | null, group: string, kind: string, namespace?: string | null, version: string, nextVersion: string } | null> | null, upgradePlanSummary?: { __typename?: 'UpgradePlanSummary', blockingAddons?: Array<{ __typename?: 'RuntimeAddonUpgrade', callout?: string | null, addon?: { __typename?: 'RuntimeAddon', name: string, icon?: string | null } | null, current?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null } | null, fix?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null, summary?: { __typename?: 'AddonVersionSummary', breakingChanges?: Array | null, chartUpdates?: Array | null, features?: Array | null, helmChanges?: string | null } | null } | null } | null> | null, blockingCloudAddons?: Array<{ __typename?: 'CloudAddonUpgrade', addon?: { __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null, current?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null, fix?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null } | null> | null } | null, insightComponents?: Array<{ __typename?: 'ClusterInsightComponent', id: string, kind: string, name: string, namespace?: string | null, group?: string | null, version: string, priority?: InsightComponentPriority | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null } | null> | null, runtimeServices?: Array<{ __typename?: 'RuntimeService', id: string, name: string, version: string, addon?: { __typename?: 'RuntimeAddon', icon?: string | null, versions?: Array<{ __typename?: 'AddonVersion', version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null> | null } | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null, helm?: { __typename?: 'HelmSpec', version?: string | null } | null } | null, addonVersion?: { __typename?: 'AddonVersion', blocking?: boolean | null, version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null } | null> | null, apiDeprecations?: Array<{ __typename?: 'ApiDeprecation', availableIn?: string | null, blocking?: boolean | null, deprecatedIn?: string | null, removedIn?: string | null, replacement?: string | null, component?: { __typename?: 'ServiceComponent', group?: string | null, version?: string | null, kind: string, name: string, namespace?: string | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null } | null } | null } | null> | null, upgradeInsights?: Array<{ __typename?: 'UpgradeInsight', id: string, name: string, description?: string | null, refreshedAt?: string | null, transitionedAt?: string | null, version?: string | null, status?: UpgradeInsightStatus | null, details?: Array<{ __typename?: 'UpgradeInsightDetail', id: string, removedIn?: string | null, replacedIn?: string | null, replacement?: string | null, status?: UpgradeInsightStatus | null, used?: string | null, clientInfo?: Array<{ __typename?: 'InsightClientInfo', userAgent?: string | null, count?: string | null, lastRequestAt?: string | null } | null> | null } | null> | null } | null> | null, cloudAddons?: Array<{ __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null> | null } | null, kubernetesChangelog?: { __typename?: 'KubernetesChangelog', version?: string | null, apiUpdates?: Array | null, bugFixes?: Array | null, breakingChanges?: Array | null, deprecations?: Array | null, features?: Array | null, majorChanges?: Array | null, removals?: Array | null } | null }; +export type ClusterOverviewDetailsQuery = { __typename?: 'RootQueryType', cluster?: { __typename?: 'Cluster', currentVersion?: string | null, id: string, self?: boolean | null, healthy?: boolean | null, healthScore?: number | null, protect?: boolean | null, name: string, handle?: string | null, distro?: ClusterDistro | null, cpuTotal?: number | null, memoryTotal?: number | null, cpuUtil?: number | null, nodeCount?: number | null, namespaceCount?: number | null, availabilityZones?: Array | null, podCount?: number | null, memoryUtil?: number | null, installed?: boolean | null, pingedAt?: string | null, deletedAt?: string | null, version?: string | null, kubeletVersion?: string | null, virtual?: boolean | null, disableAi?: boolean | null, currentUpgrade?: { __typename?: 'ClusterUpgrade', id: string, status: ClusterUpgradeStatus, version?: string | null, steps?: Array<{ __typename?: 'ClusterUpgradeStep', id: string, name: string, prompt: string, status: ClusterUpgradeStatus, type: ClusterUpgradeStepType, error?: string | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null, runtime?: { __typename?: 'AgentRuntime', type: AgentRuntimeType, name: string } | null } | null, nodeStatistics?: Array<{ __typename?: 'NodeStatistic', id: string, name: string, pendingPods?: number | null, health?: NodeStatisticHealth | null, insertedAt?: string | null, updatedAt?: string | null, cluster?: { __typename?: 'Cluster', id: string } | null } | null> | null, provider?: { __typename?: 'ClusterProvider', id: string, cloud: string, name: string, namespace: string, supportedVersions?: Array | null } | null, service?: { __typename?: 'ServiceDeployment', id: string, repository?: { __typename?: 'GitRepository', url: string } | null } | null, tags?: Array<{ __typename?: 'Tag', name: string, value: string } | null> | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null, kubeletSkew?: boolean | null } | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, extendedSupport?: { __typename?: 'ExtendedSupportInfo', extended?: boolean | null, extendedFrom?: string | null } | null, deprecatedCustomResources?: Array<{ __typename?: 'DeprecatedCustomResource', name?: string | null, group: string, kind: string, namespace?: string | null, version: string, nextVersion: string } | null> | null, upgradePlanSummary?: { __typename?: 'UpgradePlanSummary', blockingAddons?: Array<{ __typename?: 'RuntimeAddonUpgrade', callout?: string | null, addon?: { __typename?: 'RuntimeAddon', name: string, icon?: string | null } | null, current?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null } | null, fix?: { __typename?: 'AddonVersion', version?: string | null, chartVersion?: string | null, images?: Array | null, releaseUrl?: string | null, summary?: { __typename?: 'AddonVersionSummary', breakingChanges?: Array | null, chartUpdates?: Array | null, features?: Array | null, helmChanges?: string | null } | null } | null } | null> | null, blockingCloudAddons?: Array<{ __typename?: 'CloudAddonUpgrade', addon?: { __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null, current?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null, fix?: { __typename?: 'CloudAddonVersionInformation', version?: string | null } | null } | null> | null } | null, insightComponents?: Array<{ __typename?: 'ClusterInsightComponent', id: string, kind: string, name: string, namespace?: string | null, group?: string | null, version: string, priority?: InsightComponentPriority | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null } | null> | null, runtimeServices?: Array<{ __typename?: 'RuntimeService', id: string, name: string, version: string, addon?: { __typename?: 'RuntimeAddon', icon?: string | null, versions?: Array<{ __typename?: 'AddonVersion', version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null> | null } | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null, helm?: { __typename?: 'HelmSpec', version?: string | null } | null } | null, addonVersion?: { __typename?: 'AddonVersion', blocking?: boolean | null, version?: string | null, kube?: Array | null, chartVersion?: string | null, incompatibilities?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null, requirements?: Array<{ __typename?: 'VersionReference', version: string, name: string } | null> | null } | null } | null> | null, apiDeprecations?: Array<{ __typename?: 'ApiDeprecation', availableIn?: string | null, blocking?: boolean | null, deprecatedIn?: string | null, removedIn?: string | null, replacement?: string | null, component?: { __typename?: 'ServiceComponent', group?: string | null, version?: string | null, kind: string, name: string, namespace?: string | null, service?: { __typename?: 'ServiceDeployment', git?: { __typename?: 'GitRef', ref: string, folder: string } | null, repository?: { __typename?: 'GitRepository', httpsPath?: string | null, urlFormat?: string | null } | null } | null } | null } | null> | null, upgradeInsights?: Array<{ __typename?: 'UpgradeInsight', id: string, name: string, description?: string | null, refreshedAt?: string | null, transitionedAt?: string | null, version?: string | null, status?: UpgradeInsightStatus | null, details?: Array<{ __typename?: 'UpgradeInsightDetail', id: string, removedIn?: string | null, replacedIn?: string | null, replacement?: string | null, status?: UpgradeInsightStatus | null, used?: string | null, clientInfo?: Array<{ __typename?: 'InsightClientInfo', userAgent?: string | null, count?: string | null, lastRequestAt?: string | null } | null> | null } | null> | null } | null> | null, cloudAddons?: Array<{ __typename?: 'CloudAddon', id: string, insertedAt?: string | null, updatedAt?: string | null, name: string, distro: ClusterDistro, version: string, info?: { __typename?: 'CloudAddonInformation', name?: string | null, publisher?: string | null, versions?: Array<{ __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null> | null } | null, versionInfo?: { __typename?: 'CloudAddonVersionInformation', version?: string | null, compatibilities?: Array | null, blocking?: boolean | null } | null } | null> | null } | null, kubernetesChangelog?: { __typename?: 'KubernetesChangelog', version?: string | null, apiUpdates?: Array | null, bugFixes?: Array | null, breakingChanges?: Array | null, deprecations?: Array | null, features?: Array | null, majorChanges?: Array | null, removals?: Array | null } | null }; export type UpgradeStatisticsQueryVariables = Exact<{ projectId?: InputMaybe; @@ -18975,6 +19163,8 @@ export type WorkbenchToolTinyFragment = { __typename?: 'WorkbenchTool', id: stri export type WorkbenchToolFragment = { __typename?: 'WorkbenchTool', id: string, name: string, tool: WorkbenchToolType, categories?: Array | null, configuration?: { __typename?: 'WorkbenchToolConfiguration', http?: { __typename?: 'WorkbenchToolHttpConfiguration', url?: string | null, method?: string | null, body?: string | null, inputSchema?: Record | null, headers?: Array<{ __typename?: 'WorkbenchToolHttpHeader', name?: string | null, value?: string | null } | null> | null } | null, datadog?: { __typename?: 'WorkbenchToolDatadogConnection', site?: string | null } | null, elastic?: { __typename?: 'WorkbenchToolElasticConnection', index: string, url: string, username: string } | null, loki?: { __typename?: 'WorkbenchToolLokiConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, prometheus?: { __typename?: 'WorkbenchToolPrometheusConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, tempo?: { __typename?: 'WorkbenchToolTempoConnection', url?: string | null, username?: string | null, tenantId?: string | null } | null, atlassian?: { __typename?: 'WorkbenchToolAtlassianConnection', email?: string | null, url: string } | null, linear?: { __typename?: 'WorkbenchToolLinearConnection', url: string } | null, splunk?: { __typename?: 'WorkbenchToolSplunkConnection', url?: string | null, username?: string | null } | null } | null }; +export type WorkbenchJobThoughtFragment = { __typename?: 'WorkbenchJobThought', id: string, content?: string | null, toolName?: string | null, toolArgs?: Record | null }; + export type WorkbenchJobResultTodoFragment = { __typename?: 'WorkbenchJobResultTodo', name?: string | null, description?: string | null, done?: boolean | null }; export type WorkbenchJobActivityMetricFragment = { __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null }; @@ -18985,7 +19175,7 @@ export type WorkbenchJobResultFragment = { __typename?: 'WorkbenchJobResult', id export type WorkbenchJobTinyFragment = { __typename?: 'WorkbenchJob', id: string, prompt?: string | null, status: WorkbenchJobStatus, workbench?: { __typename?: 'Workbench', id: string } | null }; -export type WorkbenchJobActivityFragment = { __typename?: 'WorkbenchJobActivity', id: string, type?: WorkbenchJobActivityType | null, status: WorkbenchJobActivityStatus, prompt?: string | null, insertedAt?: string | null, result?: { __typename?: 'WorkbenchJobActivityResult', output?: string | null, jobUpdate?: { __typename?: 'WorkbenchJobActivityJobUpdate', diff?: string | null, workingTheory?: string | null, conclusion?: string | null } | null, logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null, thoughts?: Array<{ __typename?: 'WorkbenchJobThought', id: string, content?: string | null, toolName?: string | null, toolArgs?: Record | null, insertedAt?: string | null, attributes?: { __typename?: 'WorkbenchJobThoughtAttributes', logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null> | null, agentRun?: { __typename?: 'AgentRun', id: string, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null } | null }; +export type WorkbenchJobActivityFragment = { __typename?: 'WorkbenchJobActivity', id: string, type?: WorkbenchJobActivityType | null, status: WorkbenchJobActivityStatus, prompt?: string | null, insertedAt?: string | null, thoughts?: Array<{ __typename?: 'WorkbenchJobThought', id: string, content?: string | null, toolName?: string | null, toolArgs?: Record | null, insertedAt?: string | null, attributes?: { __typename?: 'WorkbenchJobThoughtAttributes', logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null> | null, result?: { __typename?: 'WorkbenchJobActivityResult', output?: string | null, error?: string | null, jobUpdate?: { __typename?: 'WorkbenchJobActivityJobUpdate', diff?: string | null, workingTheory?: string | null, conclusion?: string | null } | null, logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, messages?: Array<{ __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null> | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null }; export type WorkbenchJobProgressFragment = { __typename?: 'WorkbenchJobProgress', activityId: string, tool?: string | null, text?: string | null, arguments?: Record | null }; @@ -18995,7 +19185,7 @@ export type WorkbenchWebhookFragment = { __typename?: 'WorkbenchWebhook', id: st export type WorkbenchIssueFragment = { __typename?: 'Issue', id: string, title: string, externalId: string, provider: IssueWebhookProvider, status: IssueStatus, url: string, insertedAt?: string | null, updatedAt?: string | null, workbench?: { __typename?: 'Workbench', id: string, runs?: { __typename?: 'WorkbenchJobConnection', edges?: Array<{ __typename?: 'WorkbenchJobEdge', node?: { __typename?: 'WorkbenchJob', id: string } | null } | null> | null } | null } | null }; -export type WorkbenchJobFragment = { __typename?: 'WorkbenchJob', insertedAt?: string | null, error?: string | null, id: string, prompt?: string | null, status: WorkbenchJobStatus, workbench?: { __typename?: 'Workbench', id: string, name: string } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null, type: ObservabilityWebhookType, severity: AlertSeverity, state: AlertState, fingerprint?: string | null, url?: string | null, annotations?: Record | null, updatedAt?: string | null, tags?: Array<{ __typename?: 'Tag', id: string, name: string, value: string } | null> | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, resolution?: { __typename?: 'AlertResolution', resolution: string } | null } | null, issue?: { __typename?: 'Issue', id: string, title: string, externalId: string, insertedAt?: string | null, status: IssueStatus, url: string, provider: IssueWebhookProvider } | null, result?: { __typename?: 'WorkbenchJobResult', id: string, workingTheory?: string | null, conclusion?: string | null, todos?: Array<{ __typename?: 'WorkbenchJobResultTodo', name?: string | null, description?: string | null, done?: boolean | null } | null> | null, metadata?: { __typename?: 'WorkbenchJobResultMetadata', metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null }; +export type WorkbenchJobFragment = { __typename?: 'WorkbenchJob', insertedAt?: string | null, error?: string | null, id: string, prompt?: string | null, status: WorkbenchJobStatus, workbench?: { __typename?: 'Workbench', id: string, name: string } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null, type: ObservabilityWebhookType, severity: AlertSeverity, state: AlertState, fingerprint?: string | null, url?: string | null, annotations?: Record | null, updatedAt?: string | null, tags?: Array<{ __typename?: 'Tag', id: string, name: string, value: string } | null> | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, resolution?: { __typename?: 'AlertResolution', resolution: string } | null } | null, issue?: { __typename?: 'Issue', id: string, title: string, externalId: string, insertedAt?: string | null, status: IssueStatus, url: string, provider: IssueWebhookProvider } | null, result?: { __typename?: 'WorkbenchJobResult', id: string, workingTheory?: string | null, conclusion?: string | null, todos?: Array<{ __typename?: 'WorkbenchJobResultTodo', name?: string | null, description?: string | null, done?: boolean | null } | null> | null, metadata?: { __typename?: 'WorkbenchJobResultMetadata', metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null }; export type WorkbenchesQueryVariables = Exact<{ first?: InputMaybe; @@ -19048,6 +19238,13 @@ export type WorkbenchesIssuesQueryVariables = Exact<{ export type WorkbenchesIssuesQuery = { __typename?: 'RootQueryType', workbenchIssues?: { __typename?: 'IssueConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'IssueEdge', node?: { __typename?: 'Issue', id: string, title: string, externalId: string, provider: IssueWebhookProvider, status: IssueStatus, url: string, insertedAt?: string | null, updatedAt?: string | null, workbench?: { __typename?: 'Workbench', id: string, runs?: { __typename?: 'WorkbenchJobConnection', edges?: Array<{ __typename?: 'WorkbenchJobEdge', node?: { __typename?: 'WorkbenchJob', id: string } | null } | null> | null } | null } | null } | null } | null> | null } | null }; +export type GetWorkbenchCronMutationVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type GetWorkbenchCronMutation = { __typename?: 'RootMutationType', workbenchCron?: { __typename?: 'WorkbenchCron', id: string, crontab?: string | null, prompt?: string | null, lastRunAt?: string | null, nextRunAt?: string | null, insertedAt?: string | null, updatedAt?: string | null } | null }; + export type WorkbenchCronsQueryVariables = Exact<{ id: Scalars['ID']['input']; first?: InputMaybe; @@ -19066,40 +19263,55 @@ export type WorkbenchWebhooksQueryVariables = Exact<{ export type WorkbenchWebhooksQuery = { __typename?: 'RootQueryType', workbench?: { __typename?: 'Workbench', id: string, webhooks?: { __typename?: 'WorkbenchWebhookConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, insertedAt?: string | null, updatedAt?: string | null, matches?: { __typename?: 'WorkbenchWebhookMatches', regex?: string | null, substring?: string | null, caseInsensitive?: boolean | null } | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, name: string, type: ObservabilityWebhookType, url: string } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, name: string, url: string } | null } | null } | null> | null } | null } | null }; +export type GetWorkbenchWebhookMutationVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type GetWorkbenchWebhookMutation = { __typename?: 'RootMutationType', getWorkbenchWebhook?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, insertedAt?: string | null, updatedAt?: string | null, matches?: { __typename?: 'WorkbenchWebhookMatches', regex?: string | null, substring?: string | null, caseInsensitive?: boolean | null } | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, name: string, type: ObservabilityWebhookType, url: string } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, name: string, url: string } | null } | null }; + +export type IssueWebhooksQueryVariables = Exact<{ + first?: InputMaybe; + after?: InputMaybe; +}>; + + +export type IssueWebhooksQuery = { __typename?: 'RootQueryType', issueWebhooks?: { __typename?: 'IssueWebhookConnection', edges?: Array<{ __typename?: 'IssueWebhookEdge', node?: { __typename?: 'IssueWebhook', id: string, name: string, provider: IssueWebhookProvider, url: string } | null } | null> | null, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null } } | null }; + export type WorkbenchTriggersSummaryQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type WorkbenchTriggersSummaryQuery = { __typename?: 'RootQueryType', workbench?: { __typename?: 'Workbench', id: string, name: string, description?: string | null, crons?: { __typename?: 'WorkbenchCronConnection', edges?: Array<{ __typename?: 'WorkbenchCronEdge', node?: { __typename?: 'WorkbenchCron', id: string } | null } | null> | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string } | null } | null> | null } | null } | null }; +export type WorkbenchTriggersSummaryQuery = { __typename?: 'RootQueryType', workbench?: { __typename?: 'Workbench', id: string, name: string, description?: string | null, crons?: { __typename?: 'WorkbenchCronConnection', edges?: Array<{ __typename?: 'WorkbenchCronEdge', node?: { __typename?: 'WorkbenchCron', id: string, crontab?: string | null, nextRunAt?: string | null } | null } | null> | null } | null, webhooks?: { __typename?: 'WorkbenchWebhookConnection', edges?: Array<{ __typename?: 'WorkbenchWebhookEdge', node?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null, webhook?: { __typename?: 'ObservabilityWebhook', id: string, name: string, type: ObservabilityWebhookType } | null, issueWebhook?: { __typename?: 'IssueWebhook', id: string, name: string, provider: IssueWebhookProvider } | null } | null } | null> | null } | null } | null }; export type WorkbenchJobQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type WorkbenchJobQuery = { __typename?: 'RootQueryType', workbenchJob?: { __typename?: 'WorkbenchJob', insertedAt?: string | null, error?: string | null, id: string, prompt?: string | null, status: WorkbenchJobStatus, workbench?: { __typename?: 'Workbench', id: string, name: string } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null, type: ObservabilityWebhookType, severity: AlertSeverity, state: AlertState, fingerprint?: string | null, url?: string | null, annotations?: Record | null, updatedAt?: string | null, tags?: Array<{ __typename?: 'Tag', id: string, name: string, value: string } | null> | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, resolution?: { __typename?: 'AlertResolution', resolution: string } | null } | null, issue?: { __typename?: 'Issue', id: string, title: string, externalId: string, insertedAt?: string | null, status: IssueStatus, url: string, provider: IssueWebhookProvider } | null, result?: { __typename?: 'WorkbenchJobResult', id: string, workingTheory?: string | null, conclusion?: string | null, todos?: Array<{ __typename?: 'WorkbenchJobResultTodo', name?: string | null, description?: string | null, done?: boolean | null } | null> | null, metadata?: { __typename?: 'WorkbenchJobResultMetadata', metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null } | null }; +export type WorkbenchJobQuery = { __typename?: 'RootQueryType', workbenchJob?: { __typename?: 'WorkbenchJob', insertedAt?: string | null, error?: string | null, id: string, prompt?: string | null, status: WorkbenchJobStatus, workbench?: { __typename?: 'Workbench', id: string, name: string } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null, type: ObservabilityWebhookType, severity: AlertSeverity, state: AlertState, fingerprint?: string | null, url?: string | null, annotations?: Record | null, updatedAt?: string | null, tags?: Array<{ __typename?: 'Tag', id: string, name: string, value: string } | null> | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, resolution?: { __typename?: 'AlertResolution', resolution: string } | null } | null, issue?: { __typename?: 'Issue', id: string, title: string, externalId: string, insertedAt?: string | null, status: IssueStatus, url: string, provider: IssueWebhookProvider } | null, result?: { __typename?: 'WorkbenchJobResult', id: string, workingTheory?: string | null, conclusion?: string | null, todos?: Array<{ __typename?: 'WorkbenchJobResultTodo', name?: string | null, description?: string | null, done?: boolean | null } | null> | null, metadata?: { __typename?: 'WorkbenchJobResultMetadata', metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null } | null }; export type WorkbenchJobActivitiesQueryVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type WorkbenchJobActivitiesQuery = { __typename?: 'RootQueryType', workbenchJob?: { __typename?: 'WorkbenchJob', id: string, prompt?: string | null, activities?: { __typename?: 'WorkbenchJobActivityConnection', edges?: Array<{ __typename?: 'WorkbenchJobActivityEdge', node?: { __typename?: 'WorkbenchJobActivity', id: string, type?: WorkbenchJobActivityType | null, status: WorkbenchJobActivityStatus, prompt?: string | null, insertedAt?: string | null, result?: { __typename?: 'WorkbenchJobActivityResult', output?: string | null, jobUpdate?: { __typename?: 'WorkbenchJobActivityJobUpdate', diff?: string | null, workingTheory?: string | null, conclusion?: string | null } | null, logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null, thoughts?: Array<{ __typename?: 'WorkbenchJobThought', id: string, content?: string | null, toolName?: string | null, toolArgs?: Record | null, insertedAt?: string | null, attributes?: { __typename?: 'WorkbenchJobThoughtAttributes', logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null> | null, agentRun?: { __typename?: 'AgentRun', id: string, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null } | null } | null } | null> | null } | null } | null }; +export type WorkbenchJobActivitiesQuery = { __typename?: 'RootQueryType', workbenchJob?: { __typename?: 'WorkbenchJob', id: string, prompt?: string | null, activities?: { __typename?: 'WorkbenchJobActivityConnection', edges?: Array<{ __typename?: 'WorkbenchJobActivityEdge', node?: { __typename?: 'WorkbenchJobActivity', id: string, type?: WorkbenchJobActivityType | null, status: WorkbenchJobActivityStatus, prompt?: string | null, insertedAt?: string | null, thoughts?: Array<{ __typename?: 'WorkbenchJobThought', id: string, content?: string | null, toolName?: string | null, toolArgs?: Record | null, insertedAt?: string | null, attributes?: { __typename?: 'WorkbenchJobThoughtAttributes', logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null> | null, result?: { __typename?: 'WorkbenchJobActivityResult', output?: string | null, error?: string | null, jobUpdate?: { __typename?: 'WorkbenchJobActivityJobUpdate', diff?: string | null, workingTheory?: string | null, conclusion?: string | null } | null, logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, messages?: Array<{ __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null> | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null } | null> | null } | null } | null }; export type WorkbenchJobDeltaSubscriptionVariables = Exact<{ id: Scalars['ID']['input']; }>; -export type WorkbenchJobDeltaSubscription = { __typename?: 'RootSubscriptionType', workbenchJobDelta?: { __typename?: 'WorkbenchJobDelta', delta?: Delta | null, payload?: { __typename?: 'WorkbenchJob', insertedAt?: string | null, error?: string | null, id: string, prompt?: string | null, status: WorkbenchJobStatus, workbench?: { __typename?: 'Workbench', id: string, name: string } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null, type: ObservabilityWebhookType, severity: AlertSeverity, state: AlertState, fingerprint?: string | null, url?: string | null, annotations?: Record | null, updatedAt?: string | null, tags?: Array<{ __typename?: 'Tag', id: string, name: string, value: string } | null> | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, resolution?: { __typename?: 'AlertResolution', resolution: string } | null } | null, issue?: { __typename?: 'Issue', id: string, title: string, externalId: string, insertedAt?: string | null, status: IssueStatus, url: string, provider: IssueWebhookProvider } | null, result?: { __typename?: 'WorkbenchJobResult', id: string, workingTheory?: string | null, conclusion?: string | null, todos?: Array<{ __typename?: 'WorkbenchJobResultTodo', name?: string | null, description?: string | null, done?: boolean | null } | null> | null, metadata?: { __typename?: 'WorkbenchJobResultMetadata', metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null } | null } | null }; +export type WorkbenchJobDeltaSubscription = { __typename?: 'RootSubscriptionType', workbenchJobDelta?: { __typename?: 'WorkbenchJobDelta', delta?: Delta | null, payload?: { __typename?: 'WorkbenchJob', insertedAt?: string | null, error?: string | null, id: string, prompt?: string | null, status: WorkbenchJobStatus, workbench?: { __typename?: 'Workbench', id: string, name: string } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null, type: ObservabilityWebhookType, severity: AlertSeverity, state: AlertState, fingerprint?: string | null, url?: string | null, annotations?: Record | null, updatedAt?: string | null, tags?: Array<{ __typename?: 'Tag', id: string, name: string, value: string } | null> | null, insight?: { __typename?: 'AiInsight', id: string, text?: string | null, summary?: string | null, sha?: string | null, freshness?: InsightFreshness | null, updatedAt?: string | null, insertedAt?: string | null, error?: Array<{ __typename?: 'ServiceError', message: string, source: string } | null> | null, evidence?: Array<{ __typename?: 'AiInsightEvidence', id: string, type: EvidenceType, insertedAt?: string | null, updatedAt?: string | null, logs?: { __typename?: 'LogsEvidence', clusterId?: string | null, serviceId?: string | null, line?: string | null, lines?: Array<{ __typename?: 'LogLine', log?: string | null, timestamp?: string | null, facets?: Array<{ __typename?: 'LogFacet', key: string, value?: string | null } | null> | null } | null> | null } | null, pullRequest?: { __typename?: 'PullRequestEvidence', contents?: string | null, filename?: string | null, patch?: string | null, repo?: string | null, sha?: string | null, title?: string | null, url?: string | null } | null, alert?: { __typename?: 'AlertEvidence', alertId?: string | null, title?: string | null, resolution?: string | null } | null, knowledge?: { __typename?: 'KnowledgeEvidence', name?: string | null, observations?: Array | null, type?: string | null } | null } | null> | null, cluster?: { __typename?: 'Cluster', id: string, name: string, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null, clusterInsightComponent?: { __typename?: 'ClusterInsightComponent', id: string, name: string } | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null, serviceComponent?: { __typename?: 'ServiceComponent', id: string, name: string, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null, distro?: ClusterDistro | null, provider?: { __typename?: 'ClusterProvider', name: string, cloud: string } | null } | null } | null } | null, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string, type: StackType } | null, stackRun?: { __typename?: 'StackRun', id: string, message?: string | null, type: StackType, stack?: { __typename?: 'InfrastructureStack', id?: string | null, name: string } | null } | null, alert?: { __typename?: 'Alert', id: string, title?: string | null, message?: string | null } | null } | null, resolution?: { __typename?: 'AlertResolution', resolution: string } | null } | null, issue?: { __typename?: 'Issue', id: string, title: string, externalId: string, insertedAt?: string | null, status: IssueStatus, url: string, provider: IssueWebhookProvider } | null, result?: { __typename?: 'WorkbenchJobResult', id: string, workingTheory?: string | null, conclusion?: string | null, todos?: Array<{ __typename?: 'WorkbenchJobResultTodo', name?: string | null, description?: string | null, done?: boolean | null } | null> | null, metadata?: { __typename?: 'WorkbenchJobResultMetadata', metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null } | null } | null }; export type WorkbenchJobActivityDeltaSubscriptionVariables = Exact<{ jobId: Scalars['ID']['input']; }>; -export type WorkbenchJobActivityDeltaSubscription = { __typename?: 'RootSubscriptionType', workbenchJobActivityDelta?: { __typename?: 'WorkbenchJobActivityDelta', delta?: Delta | null, payload?: { __typename?: 'WorkbenchJobActivity', id: string, type?: WorkbenchJobActivityType | null, status: WorkbenchJobActivityStatus, prompt?: string | null, insertedAt?: string | null, result?: { __typename?: 'WorkbenchJobActivityResult', output?: string | null, jobUpdate?: { __typename?: 'WorkbenchJobActivityJobUpdate', diff?: string | null, workingTheory?: string | null, conclusion?: string | null } | null, logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null, thoughts?: Array<{ __typename?: 'WorkbenchJobThought', id: string, content?: string | null, toolName?: string | null, toolArgs?: Record | null, insertedAt?: string | null, attributes?: { __typename?: 'WorkbenchJobThoughtAttributes', logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null> | null, agentRun?: { __typename?: 'AgentRun', id: string, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null } | null } | null } | null }; +export type WorkbenchJobActivityDeltaSubscription = { __typename?: 'RootSubscriptionType', workbenchJobActivityDelta?: { __typename?: 'WorkbenchJobActivityDelta', delta?: Delta | null, payload?: { __typename?: 'WorkbenchJobActivity', id: string, type?: WorkbenchJobActivityType | null, status: WorkbenchJobActivityStatus, prompt?: string | null, insertedAt?: string | null, thoughts?: Array<{ __typename?: 'WorkbenchJobThought', id: string, content?: string | null, toolName?: string | null, toolArgs?: Record | null, insertedAt?: string | null, attributes?: { __typename?: 'WorkbenchJobThoughtAttributes', logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null } | null> | null, result?: { __typename?: 'WorkbenchJobActivityResult', output?: string | null, error?: string | null, jobUpdate?: { __typename?: 'WorkbenchJobActivityJobUpdate', diff?: string | null, workingTheory?: string | null, conclusion?: string | null } | null, logs?: Array<{ __typename?: 'WorkbenchJobActivityLog', timestamp?: string | null, message?: string | null, labels?: Record | null } | null> | null, metrics?: Array<{ __typename?: 'WorkbenchJobActivityMetric', timestamp?: string | null, name?: string | null, value?: number | null, labels?: Record | null } | null> | null } | null, agentRun?: { __typename?: 'AgentRun', id: string, status: AgentRunStatus, mode: AgentRunMode, prompt: string, shared?: boolean | null, error?: string | null, repository: string, branch?: string | null, insertedAt?: string | null, updatedAt?: string | null, messages?: Array<{ __typename?: 'AgentMessage', id: string, seq: number, role: AiRole, message: string, cost?: { __typename?: 'AgentMessageCost', total: number, tokens?: { __typename?: 'AgentMessageTokens', input?: number | null, output?: number | null, reasoning?: number | null } | null } | null, metadata?: { __typename?: 'AgentMessageMetadata', reasoning?: { __typename?: 'AgentMessageReasoning', text?: string | null, start?: number | null, end?: number | null } | null, file?: { __typename?: 'AgentMessageFile', name?: string | null, text?: string | null, start?: number | null, end?: number | null } | null, tool?: { __typename?: 'AgentMessageTool', name?: string | null, state?: AgentMessageToolState | null, input?: string | null, output?: string | null } | null } | null } | null> | null, todos?: Array<{ __typename?: 'AgentTodo', title: string, description: string, done?: boolean | null } | null> | null, analysis?: { __typename?: 'AgentAnalysis', summary: string, analysis: string, bullets?: Array | null } | null, runtime?: { __typename?: 'AgentRuntime', id: string, name: string, type: AgentRuntimeType } | null, pullRequests?: Array<{ __typename?: 'PullRequest', id: string, url: string, title?: string | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null } | null> | null, podReference?: { __typename?: 'AgentPodReference', name: string, namespace: string } | null } | null } | null } | null }; export type WorkbenchJobProgressSubscriptionVariables = Exact<{ jobId: Scalars['ID']['input']; @@ -19223,118 +19435,13 @@ export type DeleteWorkbenchWebhookMutationVariables = Exact<{ export type DeleteWorkbenchWebhookMutation = { __typename?: 'RootMutationType', deleteWorkbenchWebhook?: { __typename?: 'WorkbenchWebhook', id: string, name?: string | null } | null }; -export const PullRequestBasicFragmentDoc = gql` - fragment PullRequestBasic on PullRequest { - id - url - title - creator - status - insertedAt - updatedAt -} - `; -export const AgentRunTinyFragmentDoc = gql` - fragment AgentRunTiny on AgentRun { - id - status - mode - prompt - shared - error - runtime { - id - name - type - } - repository - branch - pullRequests { - ...PullRequestBasic - } - podReference { - name - namespace - } -} - ${PullRequestBasicFragmentDoc}`; -export const AgentMessageCostFragmentDoc = gql` - fragment AgentMessageCost on AgentMessageCost { - total - tokens { - input - output - reasoning - } -} - `; -export const AgentMessageMetadataFragmentDoc = gql` - fragment AgentMessageMetadata on AgentMessageMetadata { - reasoning { - text - start - end - } - file { - name - text - start - end - } - tool { - name - state - input - output - } -} - `; -export const AgentMessageFragmentDoc = gql` - fragment AgentMessage on AgentMessage { - id - seq - role - message - cost { - ...AgentMessageCost - } - metadata { - ...AgentMessageMetadata - } -} - ${AgentMessageCostFragmentDoc} -${AgentMessageMetadataFragmentDoc}`; -export const AgentTodoFragmentDoc = gql` - fragment AgentTodo on AgentTodo { - title - description - done -} - `; -export const AgentAnalysisFragmentDoc = gql` - fragment AgentAnalysis on AgentAnalysis { - summary - analysis - bullets -} - `; -export const AgentRunFragmentDoc = gql` - fragment AgentRun on AgentRun { - ...AgentRunTiny - messages { - ...AgentMessage - } - todos { - ...AgentTodo - } - analysis { - ...AgentAnalysis - } -} - ${AgentRunTinyFragmentDoc} -${AgentMessageFragmentDoc} -${AgentTodoFragmentDoc} -${AgentAnalysisFragmentDoc}`; +export type CreateIssueWebhookMutationVariables = Exact<{ + attributes: IssueWebhookAttributes; +}>; + + +export type CreateIssueWebhookMutation = { __typename?: 'RootMutationType', createIssueWebhook?: { __typename?: 'IssueWebhook', id: string, name: string, provider: IssueWebhookProvider, url: string } | null }; + export const ClusterMinimalFragmentDoc = gql` fragment ClusterMinimal on Cluster { id @@ -19402,6 +19509,52 @@ export const AgentRunRepositoryFragmentDoc = gql` lastUsedAt } `; +export const AgentMessageCostFragmentDoc = gql` + fragment AgentMessageCost on AgentMessageCost { + total + tokens { + input + output + reasoning + } +} + `; +export const AgentMessageMetadataFragmentDoc = gql` + fragment AgentMessageMetadata on AgentMessageMetadata { + reasoning { + text + start + end + } + file { + name + text + start + end + } + tool { + name + state + input + output + } +} + `; +export const AgentMessageFragmentDoc = gql` + fragment AgentMessage on AgentMessage { + id + seq + role + message + cost { + ...AgentMessageCost + } + metadata { + ...AgentMessageMetadata + } +} + ${AgentMessageCostFragmentDoc} +${AgentMessageMetadataFragmentDoc}`; export const AgentMessageDeltaFragmentDoc = gql` fragment AgentMessageDelta on AgentMessageDelta { delta @@ -19706,6 +19859,17 @@ export const PageInfoFragmentDoc = gql` startCursor } `; +export const PullRequestBasicFragmentDoc = gql` + fragment PullRequestBasic on PullRequest { + id + url + title + creator + status + insertedAt + updatedAt +} + `; export const ClusterBasicFragmentDoc = gql` fragment ClusterBasic on Cluster { ...ClusterTiny @@ -19815,6 +19979,32 @@ ${ScmConnectionFragmentDoc} ${PolicyBindingFragmentDoc} ${PrConfigurationFragmentDoc} ${PrConfirmationFragmentDoc}`; +export const AgentRunTinyFragmentDoc = gql` + fragment AgentRunTiny on AgentRun { + id + status + mode + prompt + shared + error + runtime { + id + name + type + } + repository + branch + pullRequests { + ...PullRequestBasic + } + podReference { + name + namespace + } + insertedAt + updatedAt +} + ${PullRequestBasicFragmentDoc}`; export const ChatFragmentDoc = gql` fragment Chat on Chat { id @@ -24021,6 +24211,14 @@ export const WorkbenchFragmentDoc = gql` ${WorkbenchTinyFragmentDoc} ${WorkbenchToolFragmentDoc} ${PolicyBindingFragmentDoc}`; +export const WorkbenchJobThoughtFragmentDoc = gql` + fragment WorkbenchJobThought on WorkbenchJobThought { + id + content + toolName + toolArgs +} + `; export const WorkbenchJobActivityLogFragmentDoc = gql` fragment WorkbenchJobActivityLog on WorkbenchJobActivityLog { timestamp @@ -24036,6 +24234,37 @@ export const WorkbenchJobActivityMetricFragmentDoc = gql` labels } `; +export const AgentTodoFragmentDoc = gql` + fragment AgentTodo on AgentTodo { + title + description + done +} + `; +export const AgentAnalysisFragmentDoc = gql` + fragment AgentAnalysis on AgentAnalysis { + summary + analysis + bullets +} + `; +export const AgentRunFragmentDoc = gql` + fragment AgentRun on AgentRun { + ...AgentRunTiny + messages { + ...AgentMessage + } + todos { + ...AgentTodo + } + analysis { + ...AgentAnalysis + } +} + ${AgentRunTinyFragmentDoc} +${AgentMessageFragmentDoc} +${AgentTodoFragmentDoc} +${AgentAnalysisFragmentDoc}`; export const WorkbenchJobActivityFragmentDoc = gql` fragment WorkbenchJobActivity on WorkbenchJobActivity { id @@ -24043,8 +24272,12 @@ export const WorkbenchJobActivityFragmentDoc = gql` status prompt insertedAt + thoughts { + ...WorkbenchJobThought + } result { output + error jobUpdate { diff workingTheory @@ -24073,15 +24306,13 @@ export const WorkbenchJobActivityFragmentDoc = gql` } } agentRun { - id - pullRequests { - ...PullRequestBasic - } + ...AgentRun } } - ${WorkbenchJobActivityLogFragmentDoc} + ${WorkbenchJobThoughtFragmentDoc} +${WorkbenchJobActivityLogFragmentDoc} ${WorkbenchJobActivityMetricFragmentDoc} -${PullRequestBasicFragmentDoc}`; +${AgentRunFragmentDoc}`; export const WorkbenchJobProgressFragmentDoc = gql` fragment WorkbenchJobProgress on WorkbenchJobProgress { activityId @@ -24204,10 +24435,14 @@ export const WorkbenchJobFragmentDoc = gql` result { ...WorkbenchJobResult } + pullRequests { + ...PullRequestBasic + } } ${WorkbenchJobTinyFragmentDoc} ${AlertFragmentDoc} -${WorkbenchJobResultFragmentDoc}`; +${WorkbenchJobResultFragmentDoc} +${PullRequestBasicFragmentDoc}`; export const AgentRunsDocument = gql` query AgentRuns($after: String, $first: Int = 100, $runtimeId: ID) { agentRuns(after: $after, first: $first, runtimeId: $runtimeId) { @@ -40035,6 +40270,39 @@ export type WorkbenchesIssuesQueryHookResult = ReturnType; export type WorkbenchesIssuesSuspenseQueryHookResult = ReturnType; export type WorkbenchesIssuesQueryResult = Apollo.QueryResult; +export const GetWorkbenchCronDocument = gql` + mutation GetWorkbenchCron($id: ID!) { + workbenchCron(id: $id) { + ...WorkbenchCron + } +} + ${WorkbenchCronFragmentDoc}`; +export type GetWorkbenchCronMutationFn = Apollo.MutationFunction; + +/** + * __useGetWorkbenchCronMutation__ + * + * To run a mutation, you first call `useGetWorkbenchCronMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGetWorkbenchCronMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [getWorkbenchCronMutation, { data, loading, error }] = useGetWorkbenchCronMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetWorkbenchCronMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GetWorkbenchCronDocument, options); + } +export type GetWorkbenchCronMutationHookResult = ReturnType; +export type GetWorkbenchCronMutationResult = Apollo.MutationResult; +export type GetWorkbenchCronMutationOptions = Apollo.BaseMutationOptions; export const WorkbenchCronsDocument = gql` query WorkbenchCrons($id: ID!, $first: Int = 100, $after: String) { workbench(id: $id) { @@ -40147,23 +40415,123 @@ export type WorkbenchWebhooksQueryHookResult = ReturnType; export type WorkbenchWebhooksSuspenseQueryHookResult = ReturnType; export type WorkbenchWebhooksQueryResult = Apollo.QueryResult; +export const GetWorkbenchWebhookDocument = gql` + mutation GetWorkbenchWebhook($id: ID!) { + getWorkbenchWebhook(id: $id) { + ...WorkbenchWebhook + } +} + ${WorkbenchWebhookFragmentDoc}`; +export type GetWorkbenchWebhookMutationFn = Apollo.MutationFunction; + +/** + * __useGetWorkbenchWebhookMutation__ + * + * To run a mutation, you first call `useGetWorkbenchWebhookMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGetWorkbenchWebhookMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [getWorkbenchWebhookMutation, { data, loading, error }] = useGetWorkbenchWebhookMutation({ + * variables: { + * id: // value for 'id' + * }, + * }); + */ +export function useGetWorkbenchWebhookMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GetWorkbenchWebhookDocument, options); + } +export type GetWorkbenchWebhookMutationHookResult = ReturnType; +export type GetWorkbenchWebhookMutationResult = Apollo.MutationResult; +export type GetWorkbenchWebhookMutationOptions = Apollo.BaseMutationOptions; +export const IssueWebhooksDocument = gql` + query IssueWebhooks($first: Int, $after: String) { + issueWebhooks(first: $first, after: $after) { + edges { + node { + id + name + provider + url + } + } + pageInfo { + ...PageInfo + } + } +} + ${PageInfoFragmentDoc}`; + +/** + * __useIssueWebhooksQuery__ + * + * To run a query within a React component, call `useIssueWebhooksQuery` and pass it any options that fit your needs. + * When your component renders, `useIssueWebhooksQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useIssueWebhooksQuery({ + * variables: { + * first: // value for 'first' + * after: // value for 'after' + * }, + * }); + */ +export function useIssueWebhooksQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(IssueWebhooksDocument, options); + } +export function useIssueWebhooksLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(IssueWebhooksDocument, options); + } +// @ts-ignore +export function useIssueWebhooksSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions): Apollo.UseSuspenseQueryResult; +export function useIssueWebhooksSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions): Apollo.UseSuspenseQueryResult; +export function useIssueWebhooksSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions) { + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(IssueWebhooksDocument, options); + } +export type IssueWebhooksQueryHookResult = ReturnType; +export type IssueWebhooksLazyQueryHookResult = ReturnType; +export type IssueWebhooksSuspenseQueryHookResult = ReturnType; +export type IssueWebhooksQueryResult = Apollo.QueryResult; export const WorkbenchTriggersSummaryDocument = gql` query WorkbenchTriggersSummary($id: ID!) { workbench(id: $id) { id name description - crons(first: 1) { + crons(first: 30) { edges { node { id + crontab + nextRunAt } } } - webhooks(first: 1) { + webhooks(first: 30) { edges { node { id + name + webhook { + id + name + type + } + issueWebhook { + id + name + provider + } } } } @@ -40933,6 +41301,42 @@ export function useDeleteWorkbenchWebhookMutation(baseOptions?: Apollo.MutationH export type DeleteWorkbenchWebhookMutationHookResult = ReturnType; export type DeleteWorkbenchWebhookMutationResult = Apollo.MutationResult; export type DeleteWorkbenchWebhookMutationOptions = Apollo.BaseMutationOptions; +export const CreateIssueWebhookDocument = gql` + mutation CreateIssueWebhook($attributes: IssueWebhookAttributes!) { + createIssueWebhook(attributes: $attributes) { + id + name + provider + url + } +} + `; +export type CreateIssueWebhookMutationFn = Apollo.MutationFunction; + +/** + * __useCreateIssueWebhookMutation__ + * + * To run a mutation, you first call `useCreateIssueWebhookMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateIssueWebhookMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createIssueWebhookMutation, { data, loading, error }] = useCreateIssueWebhookMutation({ + * variables: { + * attributes: // value for 'attributes' + * }, + * }); + */ +export function useCreateIssueWebhookMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateIssueWebhookDocument, options); + } +export type CreateIssueWebhookMutationHookResult = ReturnType; +export type CreateIssueWebhookMutationResult = Apollo.MutationResult; +export type CreateIssueWebhookMutationOptions = Apollo.BaseMutationOptions; export const namedOperations = { Query: { AgentRuns: 'AgentRuns', @@ -41150,6 +41554,7 @@ export const namedOperations = { WorkbenchesIssues: 'WorkbenchesIssues', WorkbenchCrons: 'WorkbenchCrons', WorkbenchWebhooks: 'WorkbenchWebhooks', + IssueWebhooks: 'IssueWebhooks', WorkbenchTriggersSummary: 'WorkbenchTriggersSummary', WorkbenchJob: 'WorkbenchJob', WorkbenchJobActivities: 'WorkbenchJobActivities', @@ -41305,6 +41710,8 @@ export const namedOperations = { UpdateUser: 'UpdateUser', DeleteUser: 'DeleteUser', CreateInvite: 'CreateInvite', + GetWorkbenchCron: 'GetWorkbenchCron', + GetWorkbenchWebhook: 'GetWorkbenchWebhook', CreateWorkbench: 'CreateWorkbench', UpdateWorkbench: 'UpdateWorkbench', DeleteWorkbench: 'DeleteWorkbench', @@ -41317,7 +41724,8 @@ export const namedOperations = { DeleteWorkbenchCron: 'DeleteWorkbenchCron', CreateWorkbenchWebhook: 'CreateWorkbenchWebhook', UpdateWorkbenchWebhook: 'UpdateWorkbenchWebhook', - DeleteWorkbenchWebhook: 'DeleteWorkbenchWebhook' + DeleteWorkbenchWebhook: 'DeleteWorkbenchWebhook', + CreateIssueWebhook: 'CreateIssueWebhook' }, Subscription: { AgentRunChat: 'AgentRunChat', @@ -41612,6 +42020,7 @@ export const namedOperations = { Workbench: 'Workbench', WorkbenchToolTiny: 'WorkbenchToolTiny', WorkbenchTool: 'WorkbenchTool', + WorkbenchJobThought: 'WorkbenchJobThought', WorkbenchJobResultTodo: 'WorkbenchJobResultTodo', WorkbenchJobActivityMetric: 'WorkbenchJobActivityMetric', WorkbenchJobActivityLog: 'WorkbenchJobActivityLog', diff --git a/assets/src/generated/persisted-queries/client.json b/assets/src/generated/persisted-queries/client.json index c1504e135a..0e57073eb5 100644 --- a/assets/src/generated/persisted-queries/client.json +++ b/assets/src/generated/persisted-queries/client.json @@ -2,20 +2,20 @@ "format": "apollo-persisted-query-manifest", "version": 1, "operations": { - "sha256:96fae161bd121ed6b16c4bad6704a1fa0f0db8a7b3e583d29aa9e106b45ecf4e": { + "sha256:314126ec69fd33d4e03a90c331475840290586fe9362aae24024fa0fa4fe898d": { "type": "query", "name": "AgentRuns", - "body": "query AgentRuns($after: String, $first: Int = 100, $runtimeId: ID) {\n agentRuns(after: $after, first: $first, runtimeId: $runtimeId) {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...AgentRunTiny\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" + "body": "query AgentRuns($after: String, $first: Int = 100, $runtimeId: ID) {\n agentRuns(after: $after, first: $first, runtimeId: $runtimeId) {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...AgentRunTiny\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:e045b877104e284c5b92f3490e5cc09936a842d8a79b49c6b3018f6aafb7786f": { + "sha256:3ff50b6db3009363adfd750c295fc513e911c27c9f3e07a72a65ea57843c2faf": { "type": "query", "name": "AgentRunTiny", - "body": "query AgentRunTiny($id: ID!) {\n agentRun(id: $id) {\n ...AgentRunTiny\n __typename\n }\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" + "body": "query AgentRunTiny($id: ID!) {\n agentRun(id: $id) {\n ...AgentRunTiny\n __typename\n }\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:37270c2baf533759e4bcda27b6f2d4f3b5ea1885a80f636dd8d98e9577e40e44": { + "sha256:77b625f68d5fa5790ec2a918cbea39acaea305e85c274b0cdb8a669e0e22c0df": { "type": "query", "name": "AgentRun", - "body": "query AgentRun($id: ID!) {\n agentRun(id: $id) {\n ...AgentRun\n __typename\n }\n}\n\nfragment AgentRun on AgentRun {\n ...AgentRunTiny\n messages {\n ...AgentMessage\n __typename\n }\n todos {\n ...AgentTodo\n __typename\n }\n analysis {\n ...AgentAnalysis\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment AgentMessage on AgentMessage {\n id\n seq\n role\n message\n cost {\n ...AgentMessageCost\n __typename\n }\n metadata {\n ...AgentMessageMetadata\n __typename\n }\n __typename\n}\n\nfragment AgentMessageCost on AgentMessageCost {\n total\n tokens {\n input\n output\n reasoning\n __typename\n }\n __typename\n}\n\nfragment AgentMessageMetadata on AgentMessageMetadata {\n reasoning {\n text\n start\n end\n __typename\n }\n file {\n name\n text\n start\n end\n __typename\n }\n tool {\n name\n state\n input\n output\n __typename\n }\n __typename\n}\n\nfragment AgentTodo on AgentTodo {\n title\n description\n done\n __typename\n}\n\nfragment AgentAnalysis on AgentAnalysis {\n summary\n analysis\n bullets\n __typename\n}" + "body": "query AgentRun($id: ID!) {\n agentRun(id: $id) {\n ...AgentRun\n __typename\n }\n}\n\nfragment AgentRun on AgentRun {\n ...AgentRunTiny\n messages {\n ...AgentMessage\n __typename\n }\n todos {\n ...AgentTodo\n __typename\n }\n analysis {\n ...AgentAnalysis\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment AgentMessage on AgentMessage {\n id\n seq\n role\n message\n cost {\n ...AgentMessageCost\n __typename\n }\n metadata {\n ...AgentMessageMetadata\n __typename\n }\n __typename\n}\n\nfragment AgentMessageCost on AgentMessageCost {\n total\n tokens {\n input\n output\n reasoning\n __typename\n }\n __typename\n}\n\nfragment AgentMessageMetadata on AgentMessageMetadata {\n reasoning {\n text\n start\n end\n __typename\n }\n file {\n name\n text\n start\n end\n __typename\n }\n tool {\n name\n state\n input\n output\n __typename\n }\n __typename\n}\n\nfragment AgentTodo on AgentTodo {\n title\n description\n done\n __typename\n}\n\nfragment AgentAnalysis on AgentAnalysis {\n summary\n analysis\n bullets\n __typename\n}" }, "sha256:615a16ac4294a59b5d08253b48f11d5180fdb561d49a48be35e80603a51612cf": { "type": "query", @@ -47,40 +47,40 @@ "name": "AgentRunRepositories", "body": "query AgentRunRepositories($after: String, $first: Int = 100, $q: String) {\n agentRunRepositories(after: $after, first: $first, q: $q) {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...AgentRunRepository\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment AgentRunRepository on AgentRunRepository {\n id\n url\n lastUsedAt\n __typename\n}" }, - "sha256:b43aa6b707bcbec13ca2107334a51e0f8f7697bd89b716d19eee1f5a2bb16128": { + "sha256:d7a773022a93fba52f72e8a64012c8b16498b110b5ebd14537a27455c63e13d4": { "type": "mutation", "name": "CreateAgentRun", - "body": "mutation CreateAgentRun($runtimeId: ID!, $attributes: AgentRunAttributes!) {\n createAgentRun(runtimeId: $runtimeId, attributes: $attributes) {\n ...AgentRun\n __typename\n }\n}\n\nfragment AgentRun on AgentRun {\n ...AgentRunTiny\n messages {\n ...AgentMessage\n __typename\n }\n todos {\n ...AgentTodo\n __typename\n }\n analysis {\n ...AgentAnalysis\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment AgentMessage on AgentMessage {\n id\n seq\n role\n message\n cost {\n ...AgentMessageCost\n __typename\n }\n metadata {\n ...AgentMessageMetadata\n __typename\n }\n __typename\n}\n\nfragment AgentMessageCost on AgentMessageCost {\n total\n tokens {\n input\n output\n reasoning\n __typename\n }\n __typename\n}\n\nfragment AgentMessageMetadata on AgentMessageMetadata {\n reasoning {\n text\n start\n end\n __typename\n }\n file {\n name\n text\n start\n end\n __typename\n }\n tool {\n name\n state\n input\n output\n __typename\n }\n __typename\n}\n\nfragment AgentTodo on AgentTodo {\n title\n description\n done\n __typename\n}\n\nfragment AgentAnalysis on AgentAnalysis {\n summary\n analysis\n bullets\n __typename\n}" + "body": "mutation CreateAgentRun($runtimeId: ID!, $attributes: AgentRunAttributes!) {\n createAgentRun(runtimeId: $runtimeId, attributes: $attributes) {\n ...AgentRun\n __typename\n }\n}\n\nfragment AgentRun on AgentRun {\n ...AgentRunTiny\n messages {\n ...AgentMessage\n __typename\n }\n todos {\n ...AgentTodo\n __typename\n }\n analysis {\n ...AgentAnalysis\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment AgentMessage on AgentMessage {\n id\n seq\n role\n message\n cost {\n ...AgentMessageCost\n __typename\n }\n metadata {\n ...AgentMessageMetadata\n __typename\n }\n __typename\n}\n\nfragment AgentMessageCost on AgentMessageCost {\n total\n tokens {\n input\n output\n reasoning\n __typename\n }\n __typename\n}\n\nfragment AgentMessageMetadata on AgentMessageMetadata {\n reasoning {\n text\n start\n end\n __typename\n }\n file {\n name\n text\n start\n end\n __typename\n }\n tool {\n name\n state\n input\n output\n __typename\n }\n __typename\n}\n\nfragment AgentTodo on AgentTodo {\n title\n description\n done\n __typename\n}\n\nfragment AgentAnalysis on AgentAnalysis {\n summary\n analysis\n bullets\n __typename\n}" }, - "sha256:13efdb52fe4a6d23e4a1d9588cb7877122c07c16b94a114e0cd215e1dfe01d3f": { + "sha256:28de3bf55cc1d1e5e3bc96641633383f55a40879926422be3ef9ebbb2f485c94": { "type": "mutation", "name": "CancelAgentRun", - "body": "mutation CancelAgentRun($id: ID!) {\n cancelAgentRun(id: $id) {\n ...AgentRunTiny\n __typename\n }\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" + "body": "mutation CancelAgentRun($id: ID!) {\n cancelAgentRun(id: $id) {\n ...AgentRunTiny\n __typename\n }\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" }, "sha256:b411f7b4699e0fe182eb77f9cb87870dd7933e0aa5f20643ed35edd123d01586": { "type": "mutation", "name": "UpsertAgentRuntime", "body": "mutation UpsertAgentRuntime($attributes: AgentRuntimeAttributes!) {\n upsertAgentRuntime(attributes: $attributes) {\n ...AgentRuntime\n __typename\n }\n}\n\nfragment AgentRuntime on AgentRuntime {\n id\n name\n type\n aiProxy\n cluster {\n ...ClusterTiny\n __typename\n }\n default\n createBindings {\n ...PolicyBinding\n __typename\n }\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" }, - "sha256:29ffa6dc9f4f3784a20333dd645002e4ec7816e1b63124e66fe5ed27bb676f34": { + "sha256:fd6b3d5a2abe3ac236c0fb9ca52b513d08a76795b5b6b61053259242857b6b35": { "type": "mutation", "name": "ShareAgentRun", - "body": "mutation ShareAgentRun($id: ID!, $shared: Boolean!) {\n shareAgentRun(id: $id, shared: $shared) {\n ...AgentRunTiny\n __typename\n }\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" + "body": "mutation ShareAgentRun($id: ID!, $shared: Boolean!) {\n shareAgentRun(id: $id, shared: $shared) {\n ...AgentRunTiny\n __typename\n }\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:dd0fae5ec10e2b85301ab997b437900c5d9c9f2c26ac3d506e32d1805fa34b19": { + "sha256:a81c3023a6c8a52f47b25bd60513b66e613aac5ea991f2247b86f5ba9aad8a34": { "type": "mutation", "name": "CreateClusterUpgrade", - "body": "mutation CreateClusterUpgrade($id: ID!, $attributes: ClusterUpgradeAttributes!) {\n createClusterUpgrade(id: $id, attributes: $attributes) {\n ...ClusterUpgrade\n __typename\n }\n}\n\nfragment ClusterUpgrade on ClusterUpgrade {\n id\n status\n steps {\n ...ClusterUpgradeStep\n __typename\n }\n cluster {\n ...ClusterMinimal\n __typename\n }\n runtime {\n type\n name\n __typename\n }\n version\n __typename\n}\n\nfragment ClusterUpgradeStep on ClusterUpgradeStep {\n id\n name\n prompt\n status\n type\n error\n agentRun {\n ...AgentRunTiny\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}" + "body": "mutation CreateClusterUpgrade($id: ID!, $attributes: ClusterUpgradeAttributes!) {\n createClusterUpgrade(id: $id, attributes: $attributes) {\n ...ClusterUpgrade\n __typename\n }\n}\n\nfragment ClusterUpgrade on ClusterUpgrade {\n id\n status\n steps {\n ...ClusterUpgradeStep\n __typename\n }\n cluster {\n ...ClusterMinimal\n __typename\n }\n runtime {\n type\n name\n __typename\n }\n version\n __typename\n}\n\nfragment ClusterUpgradeStep on ClusterUpgradeStep {\n id\n name\n prompt\n status\n type\n error\n agentRun {\n ...AgentRunTiny\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}" }, "sha256:3fecbf9e0460f6b9499c965ff0bb50cf4ca982397b715f62d18958d96fc77cc0": { "type": "subscription", "name": "AgentRunChat", "body": "subscription AgentRunChat($runId: ID!) {\n agentMessageDelta(runId: $runId) {\n delta\n payload {\n ...AgentMessage\n __typename\n }\n __typename\n }\n}\n\nfragment AgentMessage on AgentMessage {\n id\n seq\n role\n message\n cost {\n ...AgentMessageCost\n __typename\n }\n metadata {\n ...AgentMessageMetadata\n __typename\n }\n __typename\n}\n\nfragment AgentMessageCost on AgentMessageCost {\n total\n tokens {\n input\n output\n reasoning\n __typename\n }\n __typename\n}\n\nfragment AgentMessageMetadata on AgentMessageMetadata {\n reasoning {\n text\n start\n end\n __typename\n }\n file {\n name\n text\n start\n end\n __typename\n }\n tool {\n name\n state\n input\n output\n __typename\n }\n __typename\n}" }, - "sha256:feac005906d81da62c5a2f3e9f3ca609288dbd72eab1a5c71d9f9b455098cea7": { + "sha256:114ddf780e1d6a8e891d5c5b339da86ccf9e46add1c0840435cd091e23167b83": { "type": "subscription", "name": "AgentRunDelta", - "body": "subscription AgentRunDelta($runId: ID!) {\n agentRunDelta(id: $runId) {\n delta\n payload {\n ...AgentRunTiny\n todos {\n title\n description\n done\n __typename\n }\n analysis {\n ...AgentAnalysis\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment AgentAnalysis on AgentAnalysis {\n summary\n analysis\n bullets\n __typename\n}" + "body": "subscription AgentRunDelta($runId: ID!) {\n agentRunDelta(id: $runId) {\n delta\n payload {\n ...AgentRunTiny\n todos {\n title\n description\n done\n __typename\n }\n analysis {\n ...AgentAnalysis\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment AgentAnalysis on AgentAnalysis {\n summary\n analysis\n bullets\n __typename\n}" }, "sha256:6f244c3418b7d862932c3fe519f5efcc0d63af02b626aadba8e73c691ce66fb8": { "type": "query", @@ -97,10 +97,10 @@ "name": "ChatThreadDetails", "body": "query ChatThreadDetails($id: ID!) {\n chatThread(id: $id) {\n ...ChatThreadDetails\n __typename\n }\n}\n\nfragment ChatThreadDetails on ChatThread {\n ...ChatThreadTiny\n insight {\n ...AiInsight\n __typename\n }\n tools {\n ...McpServerTool\n __typename\n }\n __typename\n}\n\nfragment ChatThreadTiny on ChatThread {\n id\n default\n summary\n insertedAt\n updatedAt\n lastMessageAt\n settings {\n ...ChatThreadSettings\n __typename\n }\n insight {\n ...AiInsightSummary\n __typename\n }\n flow {\n id\n name\n icon\n __typename\n }\n session {\n ...AgentSession\n __typename\n }\n research {\n id\n prompt\n __typename\n }\n service {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment ChatThreadSettings on ChatThreadSettings {\n memory\n __typename\n}\n\nfragment AiInsightSummary on AiInsight {\n id\n summary\n freshness\n insertedAt\n updatedAt\n ...AiInsightContext\n __typename\n}\n\nfragment AiInsightContext on AiInsight {\n evidence {\n ...AiInsightEvidence\n __typename\n }\n cluster {\n id\n name\n distro\n provider {\n cloud\n __typename\n }\n __typename\n }\n clusterInsightComponent {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n serviceComponent {\n id\n name\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n __typename\n }\n stack {\n id\n name\n type\n __typename\n }\n stackRun {\n id\n message\n type\n stack {\n id\n name\n __typename\n }\n __typename\n }\n alert {\n id\n title\n message\n __typename\n }\n __typename\n}\n\nfragment AiInsightEvidence on AiInsightEvidence {\n id\n type\n logs {\n ...LogsEvidence\n __typename\n }\n pullRequest {\n ...PullRequestEvidence\n __typename\n }\n alert {\n ...AlertEvidence\n __typename\n }\n knowledge {\n ...KnowledgeEvidence\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment LogsEvidence on LogsEvidence {\n clusterId\n serviceId\n line\n lines {\n ...LogLine\n __typename\n }\n __typename\n}\n\nfragment LogLine on LogLine {\n facets {\n ...LogFacet\n __typename\n }\n log\n timestamp\n __typename\n}\n\nfragment LogFacet on LogFacet {\n key\n value\n __typename\n}\n\nfragment PullRequestEvidence on PullRequestEvidence {\n contents\n filename\n patch\n repo\n sha\n title\n url\n __typename\n}\n\nfragment AlertEvidence on AlertEvidence {\n alertId\n title\n resolution\n __typename\n}\n\nfragment KnowledgeEvidence on KnowledgeEvidence {\n name\n observations\n type\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment AgentSession on AgentSession {\n id\n type\n done\n planConfirmed\n thread {\n id\n summary\n insertedAt\n lastMessageAt\n __typename\n }\n connection {\n id\n name\n provider\n __typename\n }\n cluster {\n id\n __typename\n }\n runtime {\n id\n name\n __typename\n }\n pullRequest {\n id\n url\n __typename\n }\n stack {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n id\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment AiInsight on AiInsight {\n id\n text\n summary\n sha\n freshness\n updatedAt\n insertedAt\n error {\n message\n source\n __typename\n }\n ...AiInsightContext\n __typename\n}\n\nfragment McpServerTool on McpServerTool {\n tool {\n ...McpTool\n __typename\n }\n server {\n ...McpServer\n __typename\n }\n __typename\n}\n\nfragment McpTool on McpTool {\n name\n description\n inputSchema\n __typename\n}\n\nfragment McpServer on McpServer {\n id\n name\n url\n confirm\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n authentication {\n headers {\n name\n value\n __typename\n }\n plural\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" }, - "sha256:7fa0b7ffcec6f576c1a3c2178459069f10ec7824de00f84a23684b908eaaec3d": { + "sha256:e3b3f3f44e46bbe012f2a801ca3fe3da4c83a03d55bfaf1ee17794936f47b062": { "type": "query", "name": "ChatThreadMessages", - "body": "query ChatThreadMessages($id: ID!, $first: Int = 25, $last: Int, $after: String, $before: String, $reverse: Boolean = true) {\n chatThread(id: $id) {\n ...ChatThreadMessages\n __typename\n }\n}\n\nfragment ChatThreadMessages on ChatThread {\n id\n chats(\n first: $first\n last: $last\n after: $after\n before: $before\n reverse: $reverse\n ) {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...Chat\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}" + "body": "query ChatThreadMessages($id: ID!, $first: Int = 25, $last: Int, $after: String, $before: String, $reverse: Boolean = true) {\n chatThread(id: $id) {\n ...ChatThreadMessages\n __typename\n }\n}\n\nfragment ChatThreadMessages on ChatThread {\n id\n chats(\n first: $first\n last: $last\n after: $after\n before: $before\n reverse: $reverse\n ) {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...Chat\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}" }, "sha256:f0aecd83a1ad8e917f93acb525a6e31cdf1208ec92f50adaf7446952f67ea6c8": { "type": "query", @@ -112,25 +112,25 @@ "name": "CloudConnections", "body": "query CloudConnections($first: Int = 100, $last: Int, $after: String, $before: String, $q: String) {\n cloudConnections(\n first: $first\n last: $last\n after: $after\n before: $before\n q: $q\n ) {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...CloudConnectionTiny\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment CloudConnectionTiny on CloudConnection {\n id\n name\n provider\n __typename\n}" }, - "sha256:e77ae35c9a5ef54c6833fbd0c837ef9d6a18c36d9900567eb9545fc8b90d3516": { + "sha256:a34432cd10cda8e7e2667fda34ca798984bee775bc0cd89e6a7ecc04f30ba431": { "type": "mutation", "name": "HybridChat", - "body": "mutation HybridChat($messages: [ChatMessage], $threadId: ID) {\n hybridChat(messages: $messages, threadId: $threadId) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}" + "body": "mutation HybridChat($messages: [ChatMessage], $threadId: ID) {\n hybridChat(messages: $messages, threadId: $threadId) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:d8ebea1d353c2a3a4c71aac1910346d868ec5ca6dc947bbf921bbe04c5832fff": { + "sha256:f37ba4d63ddeb76009ee1926f9c7318a2c667edef799f6ee02cd367b418e41ec": { "type": "mutation", "name": "ConfirmChat", - "body": "mutation ConfirmChat($id: ID!) {\n confirmChat(id: $id) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}" + "body": "mutation ConfirmChat($id: ID!) {\n confirmChat(id: $id) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:dd32ab41aedb9bf2bb018b27dd0e26c170361217bf22fe9b0d2217ef5b19eac1": { + "sha256:78bdb0a61416ca9aefab6c1239b537b3d7175fd7292dc51054d370153ba9f76c": { "type": "mutation", "name": "ConfirmChatPlan", - "body": "mutation ConfirmChatPlan($threadId: ID!) {\n confirmPlan(threadId: $threadId) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}" + "body": "mutation ConfirmChatPlan($threadId: ID!) {\n confirmPlan(threadId: $threadId) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:fd88a236edd5830abe705ee92628e207956dfaec5040e1b6ed324a7b8fd29d9f": { + "sha256:68b0069326b65f6a60d660a8d562a4145e1f99151dc7ce540703281d700f650e": { "type": "mutation", "name": "DeleteChat", - "body": "mutation DeleteChat($id: ID!) {\n deleteChat(id: $id) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}" + "body": "mutation DeleteChat($id: ID!) {\n deleteChat(id: $id) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}" }, "sha256:ef43ed382ef243bc5872321aa0145a77b0f96cf179ca45b7c4897122442765dd": { "type": "mutation", @@ -152,10 +152,10 @@ "name": "CloneChatThread", "body": "mutation CloneChatThread($id: ID!, $seq: Int) {\n cloneThread(id: $id, seq: $seq) {\n ...ChatThreadDetails\n __typename\n }\n}\n\nfragment ChatThreadDetails on ChatThread {\n ...ChatThreadTiny\n insight {\n ...AiInsight\n __typename\n }\n tools {\n ...McpServerTool\n __typename\n }\n __typename\n}\n\nfragment ChatThreadTiny on ChatThread {\n id\n default\n summary\n insertedAt\n updatedAt\n lastMessageAt\n settings {\n ...ChatThreadSettings\n __typename\n }\n insight {\n ...AiInsightSummary\n __typename\n }\n flow {\n id\n name\n icon\n __typename\n }\n session {\n ...AgentSession\n __typename\n }\n research {\n id\n prompt\n __typename\n }\n service {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment ChatThreadSettings on ChatThreadSettings {\n memory\n __typename\n}\n\nfragment AiInsightSummary on AiInsight {\n id\n summary\n freshness\n insertedAt\n updatedAt\n ...AiInsightContext\n __typename\n}\n\nfragment AiInsightContext on AiInsight {\n evidence {\n ...AiInsightEvidence\n __typename\n }\n cluster {\n id\n name\n distro\n provider {\n cloud\n __typename\n }\n __typename\n }\n clusterInsightComponent {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n serviceComponent {\n id\n name\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n __typename\n }\n stack {\n id\n name\n type\n __typename\n }\n stackRun {\n id\n message\n type\n stack {\n id\n name\n __typename\n }\n __typename\n }\n alert {\n id\n title\n message\n __typename\n }\n __typename\n}\n\nfragment AiInsightEvidence on AiInsightEvidence {\n id\n type\n logs {\n ...LogsEvidence\n __typename\n }\n pullRequest {\n ...PullRequestEvidence\n __typename\n }\n alert {\n ...AlertEvidence\n __typename\n }\n knowledge {\n ...KnowledgeEvidence\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment LogsEvidence on LogsEvidence {\n clusterId\n serviceId\n line\n lines {\n ...LogLine\n __typename\n }\n __typename\n}\n\nfragment LogLine on LogLine {\n facets {\n ...LogFacet\n __typename\n }\n log\n timestamp\n __typename\n}\n\nfragment LogFacet on LogFacet {\n key\n value\n __typename\n}\n\nfragment PullRequestEvidence on PullRequestEvidence {\n contents\n filename\n patch\n repo\n sha\n title\n url\n __typename\n}\n\nfragment AlertEvidence on AlertEvidence {\n alertId\n title\n resolution\n __typename\n}\n\nfragment KnowledgeEvidence on KnowledgeEvidence {\n name\n observations\n type\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment AgentSession on AgentSession {\n id\n type\n done\n planConfirmed\n thread {\n id\n summary\n insertedAt\n lastMessageAt\n __typename\n }\n connection {\n id\n name\n provider\n __typename\n }\n cluster {\n id\n __typename\n }\n runtime {\n id\n name\n __typename\n }\n pullRequest {\n id\n url\n __typename\n }\n stack {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n id\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment AiInsight on AiInsight {\n id\n text\n summary\n sha\n freshness\n updatedAt\n insertedAt\n error {\n message\n source\n __typename\n }\n ...AiInsightContext\n __typename\n}\n\nfragment McpServerTool on McpServerTool {\n tool {\n ...McpTool\n __typename\n }\n server {\n ...McpServer\n __typename\n }\n __typename\n}\n\nfragment McpTool on McpTool {\n name\n description\n inputSchema\n __typename\n}\n\nfragment McpServer on McpServer {\n id\n name\n url\n confirm\n readBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n authentication {\n headers {\n name\n value\n __typename\n }\n plural\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}" }, - "sha256:24b2f2bb3f51105ae48349079b9f476017a20943f99dcbed7b4d395197e69162": { + "sha256:2505906fc7c3c508e7587d416bbcc054e251cac15c33fc6fca85cbc9c9913b42": { "type": "mutation", "name": "AddChatContext", - "body": "mutation AddChatContext($source: ContextSource!, $sourceId: ID, $threadId: ID!) {\n addChatContext(source: $source, sourceId: $sourceId, threadId: $threadId) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}" + "body": "mutation AddChatContext($source: ContextSource!, $sourceId: ID, $threadId: ID!) {\n addChatContext(source: $source, sourceId: $sourceId, threadId: $threadId) {\n ...Chat\n __typename\n }\n}\n\nfragment Chat on Chat {\n id\n content\n role\n seq\n type\n confirm\n confirmedAt\n attributes {\n file {\n name\n __typename\n }\n tool {\n name\n arguments\n __typename\n }\n prCall {\n context\n branch\n __typename\n }\n __typename\n }\n pullRequest {\n ...PullRequest\n __typename\n }\n prAutomation {\n ...PrAutomation\n __typename\n }\n agentRun {\n ...AgentRunTiny\n __typename\n }\n server {\n id\n name\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequest on PullRequest {\n ...PullRequestBasic\n service {\n id\n name\n protect\n deletedAt\n __typename\n }\n cluster {\n ...ClusterBasic\n __typename\n }\n labels\n patch\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment ClusterBasic on Cluster {\n ...ClusterTiny\n protect\n deletedAt\n version\n currentVersion\n __typename\n}\n\nfragment ClusterTiny on Cluster {\n ...ClusterMinimal\n self\n upgradePlan {\n compatibilities\n deprecations\n incompatibilities\n __typename\n }\n virtual\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment PrAutomation on PrAutomation {\n id\n name\n icon\n darkIcon\n documentation\n addon\n identifier\n cluster {\n ...ClusterBasic\n __typename\n }\n service {\n id\n name\n __typename\n }\n role\n documentation\n connection {\n ...ScmConnection\n __typename\n }\n createBindings {\n ...PolicyBinding\n __typename\n }\n writeBindings {\n ...PolicyBinding\n __typename\n }\n configuration {\n ...PrConfiguration\n __typename\n }\n confirmation {\n ...PrConfirmation\n __typename\n }\n __typename\n}\n\nfragment ScmConnection on ScmConnection {\n id\n name\n insertedAt\n updatedAt\n type\n username\n baseUrl\n apiUrl\n azure {\n username\n organization\n project\n __typename\n }\n __typename\n}\n\nfragment PolicyBinding on PolicyBinding {\n id\n user {\n id\n name\n email\n __typename\n }\n group {\n id\n name\n __typename\n }\n __typename\n}\n\nfragment PrConfiguration on PrConfiguration {\n condition {\n field\n operation\n value\n __typename\n }\n values\n default\n documentation\n displayName\n longform\n name\n optional\n placeholder\n type\n page\n __typename\n}\n\nfragment PrConfirmation on PrConfirmation {\n checklist {\n label\n __typename\n }\n text\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}" }, "sha256:7b11addd9f8f8ab833c073fadf7621e2f4ff1cc1ac4421df0aab14c9369c8d37": { "type": "mutation", @@ -1137,10 +1137,10 @@ "name": "DeleteGroup", "body": "mutation DeleteGroup($id: ID!) {\n deleteGroup(groupId: $id) {\n ...Group\n __typename\n }\n}\n\nfragment Group on Group {\n id\n name\n description\n global\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:0b09a837fa2e5bee3f00117fac78e992bf1377495fd4e31cddc36d91f3cd329d": { + "sha256:f5fd2a8187d78fa87d5b79d3d4486b9f5676210aab356f49ad33cf4fe3e07165": { "type": "query", "name": "ClusterOverviewDetails", - "body": "query ClusterOverviewDetails($id: ID!, $kubeVersion: String!, $nextKubeVersion: String!, $hasKubeVersion: Boolean!) {\n cluster(id: $id) {\n ...ClusterOverviewDetails\n __typename\n }\n kubernetesChangelog(version: $nextKubeVersion) @include(if: $hasKubeVersion) {\n ...KubernetesChangelog\n __typename\n }\n}\n\nfragment ClusterOverviewDetails on Cluster {\n ...ClustersRow\n ...ClusterWithUpgrade\n ...ClusterInsight\n currentUpgrade {\n ...ClusterUpgrade\n __typename\n }\n nodeStatistics {\n ...NodeStatistic\n __typename\n }\n __typename\n}\n\nfragment ClustersRow on Cluster {\n currentVersion\n id\n self\n healthy\n healthScore\n protect\n name\n handle\n distro\n cpuTotal\n memoryTotal\n cpuUtil\n nodeCount\n namespaceCount\n availabilityZones\n podCount\n memoryUtil\n installed\n pingedAt\n deletedAt\n provider {\n id\n cloud\n name\n namespace\n supportedVersions\n __typename\n }\n self\n service {\n id\n repository {\n url\n __typename\n }\n __typename\n }\n version\n kubeletVersion\n tags {\n name\n value\n __typename\n }\n distro\n upgradePlan {\n ...ClusterUpgradePlan\n __typename\n }\n virtual\n insight {\n ...AiInsightSummary\n __typename\n }\n extendedSupport {\n extended\n extendedFrom\n __typename\n }\n disableAi\n __typename\n}\n\nfragment ClusterUpgradePlan on ClusterUpgradePlan {\n compatibilities\n deprecations\n incompatibilities\n kubeletSkew\n __typename\n}\n\nfragment AiInsightSummary on AiInsight {\n id\n summary\n freshness\n insertedAt\n updatedAt\n ...AiInsightContext\n __typename\n}\n\nfragment AiInsightContext on AiInsight {\n evidence {\n ...AiInsightEvidence\n __typename\n }\n cluster {\n id\n name\n distro\n provider {\n cloud\n __typename\n }\n __typename\n }\n clusterInsightComponent {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n serviceComponent {\n id\n name\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n __typename\n }\n stack {\n id\n name\n type\n __typename\n }\n stackRun {\n id\n message\n type\n stack {\n id\n name\n __typename\n }\n __typename\n }\n alert {\n id\n title\n message\n __typename\n }\n __typename\n}\n\nfragment AiInsightEvidence on AiInsightEvidence {\n id\n type\n logs {\n ...LogsEvidence\n __typename\n }\n pullRequest {\n ...PullRequestEvidence\n __typename\n }\n alert {\n ...AlertEvidence\n __typename\n }\n knowledge {\n ...KnowledgeEvidence\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment LogsEvidence on LogsEvidence {\n clusterId\n serviceId\n line\n lines {\n ...LogLine\n __typename\n }\n __typename\n}\n\nfragment LogLine on LogLine {\n facets {\n ...LogFacet\n __typename\n }\n log\n timestamp\n __typename\n}\n\nfragment LogFacet on LogFacet {\n key\n value\n __typename\n}\n\nfragment PullRequestEvidence on PullRequestEvidence {\n contents\n filename\n patch\n repo\n sha\n title\n url\n __typename\n}\n\nfragment AlertEvidence on AlertEvidence {\n alertId\n title\n resolution\n __typename\n}\n\nfragment KnowledgeEvidence on KnowledgeEvidence {\n name\n observations\n type\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment ClusterWithUpgrade on Cluster {\n ...ClusterRuntimeServices\n deprecatedCustomResources {\n ...ClusterUpgradeDeprecatedCustomResource\n __typename\n }\n upgradePlanSummary {\n blockingAddons {\n ...RuntimeAddonUpgrade\n __typename\n }\n blockingCloudAddons {\n ...CloudAddonUpgrade\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment ClusterRuntimeServices on Cluster {\n id\n name\n currentVersion\n version\n runtimeServices {\n ...RuntimeService\n __typename\n }\n apiDeprecations {\n ...ApiDeprecation\n __typename\n }\n upgradeInsights {\n ...UpgradeInsight\n __typename\n }\n cloudAddons {\n ...CloudAddon\n __typename\n }\n __typename\n}\n\nfragment RuntimeService on RuntimeService {\n id\n name\n version\n addon {\n icon\n versions {\n ...AddonVersion\n __typename\n }\n __typename\n }\n service {\n git {\n ref\n folder\n __typename\n }\n repository {\n httpsPath\n urlFormat\n __typename\n }\n helm {\n version\n __typename\n }\n __typename\n }\n addonVersion {\n ...AddonVersionBlocking @include(if: $hasKubeVersion)\n ...AddonVersion\n __typename\n }\n __typename\n}\n\nfragment AddonVersion on AddonVersion {\n version\n kube\n chartVersion\n incompatibilities {\n version\n name\n __typename\n }\n requirements {\n version\n name\n __typename\n }\n __typename\n}\n\nfragment AddonVersionBlocking on AddonVersion {\n blocking(kubeVersion: $kubeVersion)\n __typename\n}\n\nfragment ApiDeprecation on ApiDeprecation {\n availableIn\n blocking\n component {\n group\n version\n kind\n name\n namespace\n service {\n git {\n ref\n folder\n __typename\n }\n repository {\n httpsPath\n urlFormat\n __typename\n }\n __typename\n }\n __typename\n }\n deprecatedIn\n removedIn\n replacement\n __typename\n}\n\nfragment UpgradeInsight on UpgradeInsight {\n id\n name\n description\n details {\n ...UpgradeInsightDetail\n __typename\n }\n refreshedAt\n transitionedAt\n version\n status\n __typename\n}\n\nfragment UpgradeInsightDetail on UpgradeInsightDetail {\n id\n removedIn\n replacedIn\n replacement\n status\n used\n clientInfo {\n ...InsightClientInfo\n __typename\n }\n __typename\n}\n\nfragment InsightClientInfo on InsightClientInfo {\n userAgent\n count\n lastRequestAt\n __typename\n}\n\nfragment CloudAddon on CloudAddon {\n id\n insertedAt\n updatedAt\n name\n distro\n info {\n name\n publisher\n versions {\n version\n compatibilities\n ...CloudAddonVersionInformation @include(if: $hasKubeVersion)\n __typename\n }\n __typename\n }\n version\n versionInfo {\n version\n compatibilities\n ...CloudAddonVersionInformation @include(if: $hasKubeVersion)\n __typename\n }\n __typename\n}\n\nfragment CloudAddonVersionInformation on CloudAddonVersionInformation {\n blocking(kubeVersion: $kubeVersion)\n __typename\n}\n\nfragment ClusterUpgradeDeprecatedCustomResource on DeprecatedCustomResource {\n name\n group\n kind\n namespace\n version\n nextVersion\n __typename\n}\n\nfragment RuntimeAddonUpgrade on RuntimeAddonUpgrade {\n addon {\n name\n icon\n __typename\n }\n callout\n current {\n ...RuntimeAddonVersion\n __typename\n }\n fix {\n ...RuntimeAddonVersionWithSummary\n __typename\n }\n __typename\n}\n\nfragment RuntimeAddonVersion on AddonVersion {\n version\n chartVersion\n images\n releaseUrl\n __typename\n}\n\nfragment RuntimeAddonVersionWithSummary on AddonVersion {\n ...RuntimeAddonVersion\n summary {\n ...AddonVersionSummary\n __typename\n }\n __typename\n}\n\nfragment AddonVersionSummary on AddonVersionSummary {\n breakingChanges\n chartUpdates\n features\n helmChanges\n __typename\n}\n\nfragment CloudAddonUpgrade on CloudAddonUpgrade {\n addon {\n ...CloudAddon\n __typename\n }\n current {\n ...CloudAddonVersion\n __typename\n }\n fix {\n ...CloudAddonVersion\n __typename\n }\n __typename\n}\n\nfragment CloudAddonVersion on CloudAddonVersionInformation {\n version\n __typename\n}\n\nfragment ClusterInsight on Cluster {\n id\n insight {\n ...AiInsight\n __typename\n }\n insightComponents {\n ...ClusterInsightComponent\n __typename\n }\n __typename\n}\n\nfragment AiInsight on AiInsight {\n id\n text\n summary\n sha\n freshness\n updatedAt\n insertedAt\n error {\n message\n source\n __typename\n }\n ...AiInsightContext\n __typename\n}\n\nfragment ClusterInsightComponent on ClusterInsightComponent {\n id\n kind\n name\n namespace\n group\n version\n priority\n insight {\n ...AiInsight\n __typename\n }\n __typename\n}\n\nfragment ClusterUpgrade on ClusterUpgrade {\n id\n status\n steps {\n ...ClusterUpgradeStep\n __typename\n }\n cluster {\n ...ClusterMinimal\n __typename\n }\n runtime {\n type\n name\n __typename\n }\n version\n __typename\n}\n\nfragment ClusterUpgradeStep on ClusterUpgradeStep {\n id\n name\n prompt\n status\n type\n error\n agentRun {\n ...AgentRunTiny\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment NodeStatistic on NodeStatistic {\n id\n name\n pendingPods\n health\n cluster {\n id\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment KubernetesChangelog on KubernetesChangelog {\n version\n apiUpdates\n bugFixes\n breakingChanges\n deprecations\n features\n majorChanges\n removals\n __typename\n}" + "body": "query ClusterOverviewDetails($id: ID!, $kubeVersion: String!, $nextKubeVersion: String!, $hasKubeVersion: Boolean!) {\n cluster(id: $id) {\n ...ClusterOverviewDetails\n __typename\n }\n kubernetesChangelog(version: $nextKubeVersion) @include(if: $hasKubeVersion) {\n ...KubernetesChangelog\n __typename\n }\n}\n\nfragment ClusterOverviewDetails on Cluster {\n ...ClustersRow\n ...ClusterWithUpgrade\n ...ClusterInsight\n currentUpgrade {\n ...ClusterUpgrade\n __typename\n }\n nodeStatistics {\n ...NodeStatistic\n __typename\n }\n __typename\n}\n\nfragment ClustersRow on Cluster {\n currentVersion\n id\n self\n healthy\n healthScore\n protect\n name\n handle\n distro\n cpuTotal\n memoryTotal\n cpuUtil\n nodeCount\n namespaceCount\n availabilityZones\n podCount\n memoryUtil\n installed\n pingedAt\n deletedAt\n provider {\n id\n cloud\n name\n namespace\n supportedVersions\n __typename\n }\n self\n service {\n id\n repository {\n url\n __typename\n }\n __typename\n }\n version\n kubeletVersion\n tags {\n name\n value\n __typename\n }\n distro\n upgradePlan {\n ...ClusterUpgradePlan\n __typename\n }\n virtual\n insight {\n ...AiInsightSummary\n __typename\n }\n extendedSupport {\n extended\n extendedFrom\n __typename\n }\n disableAi\n __typename\n}\n\nfragment ClusterUpgradePlan on ClusterUpgradePlan {\n compatibilities\n deprecations\n incompatibilities\n kubeletSkew\n __typename\n}\n\nfragment AiInsightSummary on AiInsight {\n id\n summary\n freshness\n insertedAt\n updatedAt\n ...AiInsightContext\n __typename\n}\n\nfragment AiInsightContext on AiInsight {\n evidence {\n ...AiInsightEvidence\n __typename\n }\n cluster {\n id\n name\n distro\n provider {\n cloud\n __typename\n }\n __typename\n }\n clusterInsightComponent {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n serviceComponent {\n id\n name\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n __typename\n }\n stack {\n id\n name\n type\n __typename\n }\n stackRun {\n id\n message\n type\n stack {\n id\n name\n __typename\n }\n __typename\n }\n alert {\n id\n title\n message\n __typename\n }\n __typename\n}\n\nfragment AiInsightEvidence on AiInsightEvidence {\n id\n type\n logs {\n ...LogsEvidence\n __typename\n }\n pullRequest {\n ...PullRequestEvidence\n __typename\n }\n alert {\n ...AlertEvidence\n __typename\n }\n knowledge {\n ...KnowledgeEvidence\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment LogsEvidence on LogsEvidence {\n clusterId\n serviceId\n line\n lines {\n ...LogLine\n __typename\n }\n __typename\n}\n\nfragment LogLine on LogLine {\n facets {\n ...LogFacet\n __typename\n }\n log\n timestamp\n __typename\n}\n\nfragment LogFacet on LogFacet {\n key\n value\n __typename\n}\n\nfragment PullRequestEvidence on PullRequestEvidence {\n contents\n filename\n patch\n repo\n sha\n title\n url\n __typename\n}\n\nfragment AlertEvidence on AlertEvidence {\n alertId\n title\n resolution\n __typename\n}\n\nfragment KnowledgeEvidence on KnowledgeEvidence {\n name\n observations\n type\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment ClusterWithUpgrade on Cluster {\n ...ClusterRuntimeServices\n deprecatedCustomResources {\n ...ClusterUpgradeDeprecatedCustomResource\n __typename\n }\n upgradePlanSummary {\n blockingAddons {\n ...RuntimeAddonUpgrade\n __typename\n }\n blockingCloudAddons {\n ...CloudAddonUpgrade\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment ClusterRuntimeServices on Cluster {\n id\n name\n currentVersion\n version\n runtimeServices {\n ...RuntimeService\n __typename\n }\n apiDeprecations {\n ...ApiDeprecation\n __typename\n }\n upgradeInsights {\n ...UpgradeInsight\n __typename\n }\n cloudAddons {\n ...CloudAddon\n __typename\n }\n __typename\n}\n\nfragment RuntimeService on RuntimeService {\n id\n name\n version\n addon {\n icon\n versions {\n ...AddonVersion\n __typename\n }\n __typename\n }\n service {\n git {\n ref\n folder\n __typename\n }\n repository {\n httpsPath\n urlFormat\n __typename\n }\n helm {\n version\n __typename\n }\n __typename\n }\n addonVersion {\n ...AddonVersionBlocking @include(if: $hasKubeVersion)\n ...AddonVersion\n __typename\n }\n __typename\n}\n\nfragment AddonVersion on AddonVersion {\n version\n kube\n chartVersion\n incompatibilities {\n version\n name\n __typename\n }\n requirements {\n version\n name\n __typename\n }\n __typename\n}\n\nfragment AddonVersionBlocking on AddonVersion {\n blocking(kubeVersion: $kubeVersion)\n __typename\n}\n\nfragment ApiDeprecation on ApiDeprecation {\n availableIn\n blocking\n component {\n group\n version\n kind\n name\n namespace\n service {\n git {\n ref\n folder\n __typename\n }\n repository {\n httpsPath\n urlFormat\n __typename\n }\n __typename\n }\n __typename\n }\n deprecatedIn\n removedIn\n replacement\n __typename\n}\n\nfragment UpgradeInsight on UpgradeInsight {\n id\n name\n description\n details {\n ...UpgradeInsightDetail\n __typename\n }\n refreshedAt\n transitionedAt\n version\n status\n __typename\n}\n\nfragment UpgradeInsightDetail on UpgradeInsightDetail {\n id\n removedIn\n replacedIn\n replacement\n status\n used\n clientInfo {\n ...InsightClientInfo\n __typename\n }\n __typename\n}\n\nfragment InsightClientInfo on InsightClientInfo {\n userAgent\n count\n lastRequestAt\n __typename\n}\n\nfragment CloudAddon on CloudAddon {\n id\n insertedAt\n updatedAt\n name\n distro\n info {\n name\n publisher\n versions {\n version\n compatibilities\n ...CloudAddonVersionInformation @include(if: $hasKubeVersion)\n __typename\n }\n __typename\n }\n version\n versionInfo {\n version\n compatibilities\n ...CloudAddonVersionInformation @include(if: $hasKubeVersion)\n __typename\n }\n __typename\n}\n\nfragment CloudAddonVersionInformation on CloudAddonVersionInformation {\n blocking(kubeVersion: $kubeVersion)\n __typename\n}\n\nfragment ClusterUpgradeDeprecatedCustomResource on DeprecatedCustomResource {\n name\n group\n kind\n namespace\n version\n nextVersion\n __typename\n}\n\nfragment RuntimeAddonUpgrade on RuntimeAddonUpgrade {\n addon {\n name\n icon\n __typename\n }\n callout\n current {\n ...RuntimeAddonVersion\n __typename\n }\n fix {\n ...RuntimeAddonVersionWithSummary\n __typename\n }\n __typename\n}\n\nfragment RuntimeAddonVersion on AddonVersion {\n version\n chartVersion\n images\n releaseUrl\n __typename\n}\n\nfragment RuntimeAddonVersionWithSummary on AddonVersion {\n ...RuntimeAddonVersion\n summary {\n ...AddonVersionSummary\n __typename\n }\n __typename\n}\n\nfragment AddonVersionSummary on AddonVersionSummary {\n breakingChanges\n chartUpdates\n features\n helmChanges\n __typename\n}\n\nfragment CloudAddonUpgrade on CloudAddonUpgrade {\n addon {\n ...CloudAddon\n __typename\n }\n current {\n ...CloudAddonVersion\n __typename\n }\n fix {\n ...CloudAddonVersion\n __typename\n }\n __typename\n}\n\nfragment CloudAddonVersion on CloudAddonVersionInformation {\n version\n __typename\n}\n\nfragment ClusterInsight on Cluster {\n id\n insight {\n ...AiInsight\n __typename\n }\n insightComponents {\n ...ClusterInsightComponent\n __typename\n }\n __typename\n}\n\nfragment AiInsight on AiInsight {\n id\n text\n summary\n sha\n freshness\n updatedAt\n insertedAt\n error {\n message\n source\n __typename\n }\n ...AiInsightContext\n __typename\n}\n\nfragment ClusterInsightComponent on ClusterInsightComponent {\n id\n kind\n name\n namespace\n group\n version\n priority\n insight {\n ...AiInsight\n __typename\n }\n __typename\n}\n\nfragment ClusterUpgrade on ClusterUpgrade {\n id\n status\n steps {\n ...ClusterUpgradeStep\n __typename\n }\n cluster {\n ...ClusterMinimal\n __typename\n }\n runtime {\n type\n name\n __typename\n }\n version\n __typename\n}\n\nfragment ClusterUpgradeStep on ClusterUpgradeStep {\n id\n name\n prompt\n status\n type\n error\n agentRun {\n ...AgentRunTiny\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment NodeStatistic on NodeStatistic {\n id\n name\n pendingPods\n health\n cluster {\n id\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment KubernetesChangelog on KubernetesChangelog {\n version\n apiUpdates\n bugFixes\n breakingChanges\n deprecations\n features\n majorChanges\n removals\n __typename\n}" }, "sha256:1225b5ef3c2d595c46bacc27710f9a3231a853da068a706edf1074998e1d52a1": { "type": "query", @@ -1827,6 +1827,11 @@ "name": "WorkbenchesIssues", "body": "query WorkbenchesIssues($first: Int = 100, $after: String) {\n workbenchIssues(first: $first, after: $after) {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...WorkbenchIssue\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment WorkbenchIssue on Issue {\n id\n title\n externalId\n provider\n status\n url\n insertedAt\n updatedAt\n workbench {\n id\n runs(first: 1) {\n edges {\n node {\n id\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n}" }, + "sha256:5563c9fb9619eb108ef47eaa50daa19c038925a4aa2d4611ee311c67a4ef1ab7": { + "type": "mutation", + "name": "GetWorkbenchCron", + "body": "mutation GetWorkbenchCron($id: ID!) {\n workbenchCron(id: $id) {\n ...WorkbenchCron\n __typename\n }\n}\n\nfragment WorkbenchCron on WorkbenchCron {\n id\n crontab\n prompt\n lastRunAt\n nextRunAt\n insertedAt\n updatedAt\n __typename\n}" + }, "sha256:c72fb2a7d5f15fa359f0641fa1a7627842eb6937b73188271c288728d444d6aa": { "type": "query", "name": "WorkbenchCrons", @@ -1837,30 +1842,40 @@ "name": "WorkbenchWebhooks", "body": "query WorkbenchWebhooks($id: ID!, $first: Int = 100, $after: String) {\n workbench(id: $id) {\n id\n webhooks(first: $first, after: $after) {\n pageInfo {\n ...PageInfo\n __typename\n }\n edges {\n node {\n ...WorkbenchWebhook\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}\n\nfragment WorkbenchWebhook on WorkbenchWebhook {\n id\n name\n matches {\n regex\n substring\n caseInsensitive\n __typename\n }\n insertedAt\n updatedAt\n webhook {\n id\n name\n type\n url\n __typename\n }\n issueWebhook {\n id\n name\n url\n __typename\n }\n __typename\n}" }, - "sha256:719a9241ffd9ad9439b41a39d960bd39b3b87d850c7c64af313b1917a6b8a64d": { + "sha256:f3eb55afe04bb2a781f2e0b7837a782aa44919829f4125343f7c9fdc7e93e34e": { + "type": "mutation", + "name": "GetWorkbenchWebhook", + "body": "mutation GetWorkbenchWebhook($id: ID!) {\n getWorkbenchWebhook(id: $id) {\n ...WorkbenchWebhook\n __typename\n }\n}\n\nfragment WorkbenchWebhook on WorkbenchWebhook {\n id\n name\n matches {\n regex\n substring\n caseInsensitive\n __typename\n }\n insertedAt\n updatedAt\n webhook {\n id\n name\n type\n url\n __typename\n }\n issueWebhook {\n id\n name\n url\n __typename\n }\n __typename\n}" + }, + "sha256:b16d57eb5906aa2e1a4aa07916bed26d6460c295a7c011db47273b84070c469e": { + "type": "query", + "name": "IssueWebhooks", + "body": "query IssueWebhooks($first: Int, $after: String) {\n issueWebhooks(first: $first, after: $after) {\n edges {\n node {\n id\n name\n provider\n url\n __typename\n }\n __typename\n }\n pageInfo {\n ...PageInfo\n __typename\n }\n __typename\n }\n}\n\nfragment PageInfo on PageInfo {\n hasNextPage\n endCursor\n hasPreviousPage\n startCursor\n __typename\n}" + }, + "sha256:656d696b60ebe86401f98ea28d3e7c67842d4d1c0da7a1c061f4b8d53d6c61d7": { "type": "query", "name": "WorkbenchTriggersSummary", - "body": "query WorkbenchTriggersSummary($id: ID!) {\n workbench(id: $id) {\n id\n name\n description\n crons(first: 1) {\n edges {\n node {\n id\n __typename\n }\n __typename\n }\n __typename\n }\n webhooks(first: 1) {\n edges {\n node {\n id\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}" + "body": "query WorkbenchTriggersSummary($id: ID!) {\n workbench(id: $id) {\n id\n name\n description\n crons(first: 30) {\n edges {\n node {\n id\n crontab\n nextRunAt\n __typename\n }\n __typename\n }\n __typename\n }\n webhooks(first: 30) {\n edges {\n node {\n id\n name\n webhook {\n id\n name\n type\n __typename\n }\n issueWebhook {\n id\n name\n provider\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}" }, - "sha256:f1ac9fcb8889dba6e1ad1887ee3487cddf4b659ffca316765d475c8277919923": { + "sha256:bd4ed008844344042c23776e7296d6cea9b7b2769a4a23c6ed5fc3350e5a10e2": { "type": "query", "name": "WorkbenchJob", - "body": "query WorkbenchJob($id: ID!) {\n workbenchJob(id: $id) {\n ...WorkbenchJob\n __typename\n }\n}\n\nfragment WorkbenchJob on WorkbenchJob {\n ...WorkbenchJobTiny\n insertedAt\n workbench {\n id\n name\n __typename\n }\n error\n alert {\n ...Alert\n __typename\n }\n issue {\n id\n title\n externalId\n insertedAt\n status\n url\n provider\n __typename\n }\n result {\n ...WorkbenchJobResult\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobTiny on WorkbenchJob {\n id\n prompt\n status\n workbench {\n id\n __typename\n }\n __typename\n}\n\nfragment Alert on Alert {\n id\n title\n message\n type\n severity\n state\n fingerprint\n url\n annotations\n tags {\n id\n name\n value\n __typename\n }\n insight {\n ...AiInsight\n __typename\n }\n resolution {\n ...AlertResolution\n __typename\n }\n updatedAt\n __typename\n}\n\nfragment AiInsight on AiInsight {\n id\n text\n summary\n sha\n freshness\n updatedAt\n insertedAt\n error {\n message\n source\n __typename\n }\n ...AiInsightContext\n __typename\n}\n\nfragment AiInsightContext on AiInsight {\n evidence {\n ...AiInsightEvidence\n __typename\n }\n cluster {\n id\n name\n distro\n provider {\n cloud\n __typename\n }\n __typename\n }\n clusterInsightComponent {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n serviceComponent {\n id\n name\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n __typename\n }\n stack {\n id\n name\n type\n __typename\n }\n stackRun {\n id\n message\n type\n stack {\n id\n name\n __typename\n }\n __typename\n }\n alert {\n id\n title\n message\n __typename\n }\n __typename\n}\n\nfragment AiInsightEvidence on AiInsightEvidence {\n id\n type\n logs {\n ...LogsEvidence\n __typename\n }\n pullRequest {\n ...PullRequestEvidence\n __typename\n }\n alert {\n ...AlertEvidence\n __typename\n }\n knowledge {\n ...KnowledgeEvidence\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment LogsEvidence on LogsEvidence {\n clusterId\n serviceId\n line\n lines {\n ...LogLine\n __typename\n }\n __typename\n}\n\nfragment LogLine on LogLine {\n facets {\n ...LogFacet\n __typename\n }\n log\n timestamp\n __typename\n}\n\nfragment LogFacet on LogFacet {\n key\n value\n __typename\n}\n\nfragment PullRequestEvidence on PullRequestEvidence {\n contents\n filename\n patch\n repo\n sha\n title\n url\n __typename\n}\n\nfragment AlertEvidence on AlertEvidence {\n alertId\n title\n resolution\n __typename\n}\n\nfragment KnowledgeEvidence on KnowledgeEvidence {\n name\n observations\n type\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment AlertResolution on AlertResolution {\n resolution\n __typename\n}\n\nfragment WorkbenchJobResult on WorkbenchJobResult {\n id\n workingTheory\n conclusion\n todos {\n ...WorkbenchJobResultTodo\n __typename\n }\n metadata {\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobResultTodo on WorkbenchJobResultTodo {\n name\n description\n done\n __typename\n}\n\nfragment WorkbenchJobActivityMetric on WorkbenchJobActivityMetric {\n timestamp\n name\n value\n labels\n __typename\n}" + "body": "query WorkbenchJob($id: ID!) {\n workbenchJob(id: $id) {\n ...WorkbenchJob\n __typename\n }\n}\n\nfragment WorkbenchJob on WorkbenchJob {\n ...WorkbenchJobTiny\n insertedAt\n workbench {\n id\n name\n __typename\n }\n error\n alert {\n ...Alert\n __typename\n }\n issue {\n id\n title\n externalId\n insertedAt\n status\n url\n provider\n __typename\n }\n result {\n ...WorkbenchJobResult\n __typename\n }\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobTiny on WorkbenchJob {\n id\n prompt\n status\n workbench {\n id\n __typename\n }\n __typename\n}\n\nfragment Alert on Alert {\n id\n title\n message\n type\n severity\n state\n fingerprint\n url\n annotations\n tags {\n id\n name\n value\n __typename\n }\n insight {\n ...AiInsight\n __typename\n }\n resolution {\n ...AlertResolution\n __typename\n }\n updatedAt\n __typename\n}\n\nfragment AiInsight on AiInsight {\n id\n text\n summary\n sha\n freshness\n updatedAt\n insertedAt\n error {\n message\n source\n __typename\n }\n ...AiInsightContext\n __typename\n}\n\nfragment AiInsightContext on AiInsight {\n evidence {\n ...AiInsightEvidence\n __typename\n }\n cluster {\n id\n name\n distro\n provider {\n cloud\n __typename\n }\n __typename\n }\n clusterInsightComponent {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n serviceComponent {\n id\n name\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n __typename\n }\n stack {\n id\n name\n type\n __typename\n }\n stackRun {\n id\n message\n type\n stack {\n id\n name\n __typename\n }\n __typename\n }\n alert {\n id\n title\n message\n __typename\n }\n __typename\n}\n\nfragment AiInsightEvidence on AiInsightEvidence {\n id\n type\n logs {\n ...LogsEvidence\n __typename\n }\n pullRequest {\n ...PullRequestEvidence\n __typename\n }\n alert {\n ...AlertEvidence\n __typename\n }\n knowledge {\n ...KnowledgeEvidence\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment LogsEvidence on LogsEvidence {\n clusterId\n serviceId\n line\n lines {\n ...LogLine\n __typename\n }\n __typename\n}\n\nfragment LogLine on LogLine {\n facets {\n ...LogFacet\n __typename\n }\n log\n timestamp\n __typename\n}\n\nfragment LogFacet on LogFacet {\n key\n value\n __typename\n}\n\nfragment PullRequestEvidence on PullRequestEvidence {\n contents\n filename\n patch\n repo\n sha\n title\n url\n __typename\n}\n\nfragment AlertEvidence on AlertEvidence {\n alertId\n title\n resolution\n __typename\n}\n\nfragment KnowledgeEvidence on KnowledgeEvidence {\n name\n observations\n type\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment AlertResolution on AlertResolution {\n resolution\n __typename\n}\n\nfragment WorkbenchJobResult on WorkbenchJobResult {\n id\n workingTheory\n conclusion\n todos {\n ...WorkbenchJobResultTodo\n __typename\n }\n metadata {\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobResultTodo on WorkbenchJobResultTodo {\n name\n description\n done\n __typename\n}\n\nfragment WorkbenchJobActivityMetric on WorkbenchJobActivityMetric {\n timestamp\n name\n value\n labels\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:901a573259b3c953669d816d68bd1038b12c40536be1d3a189daa3ad08517bae": { + "sha256:5a890ad801cb3e1a0f631b374bb1e6cc7cc497e68dac2df561de2bc6c8d663d3": { "type": "query", "name": "WorkbenchJobActivities", - "body": "query WorkbenchJobActivities($id: ID!) {\n workbenchJob(id: $id) {\n id\n prompt\n activities(first: 100) {\n edges {\n node {\n ...WorkbenchJobActivity\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment WorkbenchJobActivity on WorkbenchJobActivity {\n id\n type\n status\n prompt\n insertedAt\n result {\n output\n jobUpdate {\n diff\n workingTheory\n conclusion\n __typename\n }\n logs {\n ...WorkbenchJobActivityLog\n __typename\n }\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n thoughts {\n id\n content\n toolName\n toolArgs\n insertedAt\n attributes {\n logs {\n ...WorkbenchJobActivityLog\n __typename\n }\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n __typename\n }\n agentRun {\n id\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobActivityLog on WorkbenchJobActivityLog {\n timestamp\n message\n labels\n __typename\n}\n\nfragment WorkbenchJobActivityMetric on WorkbenchJobActivityMetric {\n timestamp\n name\n value\n labels\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" + "body": "query WorkbenchJobActivities($id: ID!) {\n workbenchJob(id: $id) {\n id\n prompt\n activities(first: 100) {\n edges {\n node {\n ...WorkbenchJobActivity\n __typename\n }\n __typename\n }\n __typename\n }\n __typename\n }\n}\n\nfragment WorkbenchJobActivity on WorkbenchJobActivity {\n id\n type\n status\n prompt\n insertedAt\n thoughts {\n ...WorkbenchJobThought\n __typename\n }\n result {\n output\n error\n jobUpdate {\n diff\n workingTheory\n conclusion\n __typename\n }\n logs {\n ...WorkbenchJobActivityLog\n __typename\n }\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n thoughts {\n id\n content\n toolName\n toolArgs\n insertedAt\n attributes {\n logs {\n ...WorkbenchJobActivityLog\n __typename\n }\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n __typename\n }\n agentRun {\n ...AgentRun\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobThought on WorkbenchJobThought {\n id\n content\n toolName\n toolArgs\n __typename\n}\n\nfragment WorkbenchJobActivityLog on WorkbenchJobActivityLog {\n timestamp\n message\n labels\n __typename\n}\n\nfragment WorkbenchJobActivityMetric on WorkbenchJobActivityMetric {\n timestamp\n name\n value\n labels\n __typename\n}\n\nfragment AgentRun on AgentRun {\n ...AgentRunTiny\n messages {\n ...AgentMessage\n __typename\n }\n todos {\n ...AgentTodo\n __typename\n }\n analysis {\n ...AgentAnalysis\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment AgentMessage on AgentMessage {\n id\n seq\n role\n message\n cost {\n ...AgentMessageCost\n __typename\n }\n metadata {\n ...AgentMessageMetadata\n __typename\n }\n __typename\n}\n\nfragment AgentMessageCost on AgentMessageCost {\n total\n tokens {\n input\n output\n reasoning\n __typename\n }\n __typename\n}\n\nfragment AgentMessageMetadata on AgentMessageMetadata {\n reasoning {\n text\n start\n end\n __typename\n }\n file {\n name\n text\n start\n end\n __typename\n }\n tool {\n name\n state\n input\n output\n __typename\n }\n __typename\n}\n\nfragment AgentTodo on AgentTodo {\n title\n description\n done\n __typename\n}\n\nfragment AgentAnalysis on AgentAnalysis {\n summary\n analysis\n bullets\n __typename\n}" }, - "sha256:732201e9d15a33c093dc4b032879384eac9520973ac2d27bfee0f0ca4255471c": { + "sha256:c266afd413edad8a89a86dd098c89542afa2684ec3ab33316b69d09cfc0e4539": { "type": "subscription", "name": "WorkbenchJobDelta", - "body": "subscription WorkbenchJobDelta($id: ID!) {\n workbenchJobDelta(id: $id) {\n delta\n payload {\n ...WorkbenchJob\n __typename\n }\n __typename\n }\n}\n\nfragment WorkbenchJob on WorkbenchJob {\n ...WorkbenchJobTiny\n insertedAt\n workbench {\n id\n name\n __typename\n }\n error\n alert {\n ...Alert\n __typename\n }\n issue {\n id\n title\n externalId\n insertedAt\n status\n url\n provider\n __typename\n }\n result {\n ...WorkbenchJobResult\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobTiny on WorkbenchJob {\n id\n prompt\n status\n workbench {\n id\n __typename\n }\n __typename\n}\n\nfragment Alert on Alert {\n id\n title\n message\n type\n severity\n state\n fingerprint\n url\n annotations\n tags {\n id\n name\n value\n __typename\n }\n insight {\n ...AiInsight\n __typename\n }\n resolution {\n ...AlertResolution\n __typename\n }\n updatedAt\n __typename\n}\n\nfragment AiInsight on AiInsight {\n id\n text\n summary\n sha\n freshness\n updatedAt\n insertedAt\n error {\n message\n source\n __typename\n }\n ...AiInsightContext\n __typename\n}\n\nfragment AiInsightContext on AiInsight {\n evidence {\n ...AiInsightEvidence\n __typename\n }\n cluster {\n id\n name\n distro\n provider {\n cloud\n __typename\n }\n __typename\n }\n clusterInsightComponent {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n serviceComponent {\n id\n name\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n __typename\n }\n stack {\n id\n name\n type\n __typename\n }\n stackRun {\n id\n message\n type\n stack {\n id\n name\n __typename\n }\n __typename\n }\n alert {\n id\n title\n message\n __typename\n }\n __typename\n}\n\nfragment AiInsightEvidence on AiInsightEvidence {\n id\n type\n logs {\n ...LogsEvidence\n __typename\n }\n pullRequest {\n ...PullRequestEvidence\n __typename\n }\n alert {\n ...AlertEvidence\n __typename\n }\n knowledge {\n ...KnowledgeEvidence\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment LogsEvidence on LogsEvidence {\n clusterId\n serviceId\n line\n lines {\n ...LogLine\n __typename\n }\n __typename\n}\n\nfragment LogLine on LogLine {\n facets {\n ...LogFacet\n __typename\n }\n log\n timestamp\n __typename\n}\n\nfragment LogFacet on LogFacet {\n key\n value\n __typename\n}\n\nfragment PullRequestEvidence on PullRequestEvidence {\n contents\n filename\n patch\n repo\n sha\n title\n url\n __typename\n}\n\nfragment AlertEvidence on AlertEvidence {\n alertId\n title\n resolution\n __typename\n}\n\nfragment KnowledgeEvidence on KnowledgeEvidence {\n name\n observations\n type\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment AlertResolution on AlertResolution {\n resolution\n __typename\n}\n\nfragment WorkbenchJobResult on WorkbenchJobResult {\n id\n workingTheory\n conclusion\n todos {\n ...WorkbenchJobResultTodo\n __typename\n }\n metadata {\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobResultTodo on WorkbenchJobResultTodo {\n name\n description\n done\n __typename\n}\n\nfragment WorkbenchJobActivityMetric on WorkbenchJobActivityMetric {\n timestamp\n name\n value\n labels\n __typename\n}" + "body": "subscription WorkbenchJobDelta($id: ID!) {\n workbenchJobDelta(id: $id) {\n delta\n payload {\n ...WorkbenchJob\n __typename\n }\n __typename\n }\n}\n\nfragment WorkbenchJob on WorkbenchJob {\n ...WorkbenchJobTiny\n insertedAt\n workbench {\n id\n name\n __typename\n }\n error\n alert {\n ...Alert\n __typename\n }\n issue {\n id\n title\n externalId\n insertedAt\n status\n url\n provider\n __typename\n }\n result {\n ...WorkbenchJobResult\n __typename\n }\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobTiny on WorkbenchJob {\n id\n prompt\n status\n workbench {\n id\n __typename\n }\n __typename\n}\n\nfragment Alert on Alert {\n id\n title\n message\n type\n severity\n state\n fingerprint\n url\n annotations\n tags {\n id\n name\n value\n __typename\n }\n insight {\n ...AiInsight\n __typename\n }\n resolution {\n ...AlertResolution\n __typename\n }\n updatedAt\n __typename\n}\n\nfragment AiInsight on AiInsight {\n id\n text\n summary\n sha\n freshness\n updatedAt\n insertedAt\n error {\n message\n source\n __typename\n }\n ...AiInsightContext\n __typename\n}\n\nfragment AiInsightContext on AiInsight {\n evidence {\n ...AiInsightEvidence\n __typename\n }\n cluster {\n id\n name\n distro\n provider {\n cloud\n __typename\n }\n __typename\n }\n clusterInsightComponent {\n id\n name\n __typename\n }\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n serviceComponent {\n id\n name\n service {\n id\n name\n cluster {\n ...ClusterMinimal\n __typename\n }\n __typename\n }\n __typename\n }\n stack {\n id\n name\n type\n __typename\n }\n stackRun {\n id\n message\n type\n stack {\n id\n name\n __typename\n }\n __typename\n }\n alert {\n id\n title\n message\n __typename\n }\n __typename\n}\n\nfragment AiInsightEvidence on AiInsightEvidence {\n id\n type\n logs {\n ...LogsEvidence\n __typename\n }\n pullRequest {\n ...PullRequestEvidence\n __typename\n }\n alert {\n ...AlertEvidence\n __typename\n }\n knowledge {\n ...KnowledgeEvidence\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment LogsEvidence on LogsEvidence {\n clusterId\n serviceId\n line\n lines {\n ...LogLine\n __typename\n }\n __typename\n}\n\nfragment LogLine on LogLine {\n facets {\n ...LogFacet\n __typename\n }\n log\n timestamp\n __typename\n}\n\nfragment LogFacet on LogFacet {\n key\n value\n __typename\n}\n\nfragment PullRequestEvidence on PullRequestEvidence {\n contents\n filename\n patch\n repo\n sha\n title\n url\n __typename\n}\n\nfragment AlertEvidence on AlertEvidence {\n alertId\n title\n resolution\n __typename\n}\n\nfragment KnowledgeEvidence on KnowledgeEvidence {\n name\n observations\n type\n __typename\n}\n\nfragment ClusterMinimal on Cluster {\n id\n name\n handle\n provider {\n name\n cloud\n __typename\n }\n distro\n __typename\n}\n\nfragment AlertResolution on AlertResolution {\n resolution\n __typename\n}\n\nfragment WorkbenchJobResult on WorkbenchJobResult {\n id\n workingTheory\n conclusion\n todos {\n ...WorkbenchJobResultTodo\n __typename\n }\n metadata {\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobResultTodo on WorkbenchJobResultTodo {\n name\n description\n done\n __typename\n}\n\nfragment WorkbenchJobActivityMetric on WorkbenchJobActivityMetric {\n timestamp\n name\n value\n labels\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" }, - "sha256:856fc81607645a859f87c9ef85ed77b87620916c6221e3da58dcf772a6bd9393": { + "sha256:ec43c7ad324dd0d8523159498e209a151965d5bdb7bc297d9c872f7d3b27cd5d": { "type": "subscription", "name": "WorkbenchJobActivityDelta", - "body": "subscription WorkbenchJobActivityDelta($jobId: ID!) {\n workbenchJobActivityDelta(jobId: $jobId) {\n delta\n payload {\n ...WorkbenchJobActivity\n __typename\n }\n __typename\n }\n}\n\nfragment WorkbenchJobActivity on WorkbenchJobActivity {\n id\n type\n status\n prompt\n insertedAt\n result {\n output\n jobUpdate {\n diff\n workingTheory\n conclusion\n __typename\n }\n logs {\n ...WorkbenchJobActivityLog\n __typename\n }\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n thoughts {\n id\n content\n toolName\n toolArgs\n insertedAt\n attributes {\n logs {\n ...WorkbenchJobActivityLog\n __typename\n }\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n __typename\n }\n agentRun {\n id\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobActivityLog on WorkbenchJobActivityLog {\n timestamp\n message\n labels\n __typename\n}\n\nfragment WorkbenchJobActivityMetric on WorkbenchJobActivityMetric {\n timestamp\n name\n value\n labels\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}" + "body": "subscription WorkbenchJobActivityDelta($jobId: ID!) {\n workbenchJobActivityDelta(jobId: $jobId) {\n delta\n payload {\n ...WorkbenchJobActivity\n __typename\n }\n __typename\n }\n}\n\nfragment WorkbenchJobActivity on WorkbenchJobActivity {\n id\n type\n status\n prompt\n insertedAt\n thoughts {\n ...WorkbenchJobThought\n __typename\n }\n result {\n output\n error\n jobUpdate {\n diff\n workingTheory\n conclusion\n __typename\n }\n logs {\n ...WorkbenchJobActivityLog\n __typename\n }\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n thoughts {\n id\n content\n toolName\n toolArgs\n insertedAt\n attributes {\n logs {\n ...WorkbenchJobActivityLog\n __typename\n }\n metrics {\n ...WorkbenchJobActivityMetric\n __typename\n }\n __typename\n }\n __typename\n }\n agentRun {\n ...AgentRun\n __typename\n }\n __typename\n}\n\nfragment WorkbenchJobThought on WorkbenchJobThought {\n id\n content\n toolName\n toolArgs\n __typename\n}\n\nfragment WorkbenchJobActivityLog on WorkbenchJobActivityLog {\n timestamp\n message\n labels\n __typename\n}\n\nfragment WorkbenchJobActivityMetric on WorkbenchJobActivityMetric {\n timestamp\n name\n value\n labels\n __typename\n}\n\nfragment AgentRun on AgentRun {\n ...AgentRunTiny\n messages {\n ...AgentMessage\n __typename\n }\n todos {\n ...AgentTodo\n __typename\n }\n analysis {\n ...AgentAnalysis\n __typename\n }\n __typename\n}\n\nfragment AgentRunTiny on AgentRun {\n id\n status\n mode\n prompt\n shared\n error\n runtime {\n id\n name\n type\n __typename\n }\n repository\n branch\n pullRequests {\n ...PullRequestBasic\n __typename\n }\n podReference {\n name\n namespace\n __typename\n }\n insertedAt\n updatedAt\n __typename\n}\n\nfragment PullRequestBasic on PullRequest {\n id\n url\n title\n creator\n status\n insertedAt\n updatedAt\n __typename\n}\n\nfragment AgentMessage on AgentMessage {\n id\n seq\n role\n message\n cost {\n ...AgentMessageCost\n __typename\n }\n metadata {\n ...AgentMessageMetadata\n __typename\n }\n __typename\n}\n\nfragment AgentMessageCost on AgentMessageCost {\n total\n tokens {\n input\n output\n reasoning\n __typename\n }\n __typename\n}\n\nfragment AgentMessageMetadata on AgentMessageMetadata {\n reasoning {\n text\n start\n end\n __typename\n }\n file {\n name\n text\n start\n end\n __typename\n }\n tool {\n name\n state\n input\n output\n __typename\n }\n __typename\n}\n\nfragment AgentTodo on AgentTodo {\n title\n description\n done\n __typename\n}\n\nfragment AgentAnalysis on AgentAnalysis {\n summary\n analysis\n bullets\n __typename\n}" }, "sha256:4e927bf0d844e0a308bf892000faa31ee627280a733053f758093ee88fccca84": { "type": "subscription", @@ -1941,6 +1956,11 @@ "type": "mutation", "name": "DeleteWorkbenchWebhook", "body": "mutation DeleteWorkbenchWebhook($id: ID!) {\n deleteWorkbenchWebhook(id: $id) {\n id\n name\n __typename\n }\n}" + }, + "sha256:d6e640c4f225eb58605c970fb843fb87041195d4f214f22419fccc474f768c09": { + "type": "mutation", + "name": "CreateIssueWebhook", + "body": "mutation CreateIssueWebhook($attributes: IssueWebhookAttributes!) {\n createIssueWebhook(attributes: $attributes) {\n id\n name\n provider\n url\n __typename\n }\n}" } } } \ No newline at end of file diff --git a/assets/src/graph/ai/agent.graphql b/assets/src/graph/ai/agent.graphql index cad6dfe37b..1656a53fd1 100644 --- a/assets/src/graph/ai/agent.graphql +++ b/assets/src/graph/ai/agent.graphql @@ -19,6 +19,8 @@ fragment AgentRunTiny on AgentRun { name namespace } + insertedAt + updatedAt } fragment AgentRun on AgentRun { diff --git a/assets/src/graph/workbench.graphql b/assets/src/graph/workbench.graphql index 4da9005d23..dc3ca01ea4 100644 --- a/assets/src/graph/workbench.graphql +++ b/assets/src/graph/workbench.graphql @@ -105,6 +105,13 @@ fragment WorkbenchTool on WorkbenchTool { } } +fragment WorkbenchJobThought on WorkbenchJobThought { + id + content + toolName + toolArgs +} + fragment WorkbenchJobResultTodo on WorkbenchJobResultTodo { name description @@ -153,8 +160,12 @@ fragment WorkbenchJobActivity on WorkbenchJobActivity { status prompt insertedAt + thoughts { + ...WorkbenchJobThought + } result { output + error jobUpdate { diff workingTheory @@ -183,10 +194,7 @@ fragment WorkbenchJobActivity on WorkbenchJobActivity { } } agentRun { - id - pullRequests { - ...PullRequestBasic - } + ...AgentRun } } @@ -274,6 +282,9 @@ fragment WorkbenchJob on WorkbenchJob { result { ...WorkbenchJobResult } + pullRequests { + ...PullRequestBasic + } } query Workbenches($first: Int = 100, $after: String, $q: String) { @@ -353,6 +364,12 @@ query WorkbenchesIssues($first: Int = 100, $after: String) { } } +mutation GetWorkbenchCron($id: ID!) { + workbenchCron(id: $id) { + ...WorkbenchCron + } +} + query WorkbenchCrons($id: ID!, $first: Int = 100, $after: String) { workbench(id: $id) { id @@ -385,22 +402,57 @@ query WorkbenchWebhooks($id: ID!, $first: Int = 100, $after: String) { } } +mutation GetWorkbenchWebhook($id: ID!) { + getWorkbenchWebhook(id: $id) { + ...WorkbenchWebhook + } +} + +query IssueWebhooks($first: Int, $after: String) { + issueWebhooks(first: $first, after: $after) { + edges { + node { + id + name + provider + url + } + } + pageInfo { + ...PageInfo + } + } +} + query WorkbenchTriggersSummary($id: ID!) { workbench(id: $id) { id name description - crons(first: 1) { + crons(first: 30) { edges { node { id + crontab + nextRunAt } } } - webhooks(first: 1) { + webhooks(first: 30) { edges { node { id + name + webhook { + id + name + type + } + issueWebhook { + id + name + provider + } } } } @@ -563,3 +615,12 @@ mutation DeleteWorkbenchWebhook($id: ID!) { name } } + +mutation CreateIssueWebhook($attributes: IssueWebhookAttributes!) { + createIssueWebhook(attributes: $attributes) { + id + name + provider + url + } +} diff --git a/assets/src/routes/workbenchesRoutes.tsx b/assets/src/routes/workbenchesRoutes.tsx index 5a77e2a990..1e0cfae62a 100644 --- a/assets/src/routes/workbenchesRoutes.tsx +++ b/assets/src/routes/workbenchesRoutes.tsx @@ -6,9 +6,12 @@ import { WorkbenchToolCreateOrEdit } from 'components/workbenches/tools/Workbenc import { WorkbenchTools } from 'components/workbenches/tools/WorkbenchTools' import { Workbench } from 'components/workbenches/workbench/Workbench' import { WorkbenchCreateOrEdit } from 'components/workbenches/workbench/create-edit/WorkbenchCreateOrEdit' -import { WorkbenchScheduleTrigger } from 'components/workbenches/workbench/triggers/WorkbenchScheduleTrigger' -import { WorkbenchWebhookTrigger } from 'components/workbenches/workbench/triggers/WorkbenchWebhookTrigger' -import { Navigate, Route } from 'react-router-dom' +import { CronSchedules } from 'components/workbenches/workbench/crons/CronSchedules' +import { WebhookTriggers } from 'components/workbenches/workbench/webhooks/WebhookTriggers' +import { WebhookForm } from 'components/workbenches/workbench/webhooks/WebhookForm' +import { CronScheduleForm } from 'components/workbenches/workbench/crons/CronScheduleForm' +import { WebhookTriggerForm } from 'components/workbenches/workbench/webhooks/WebhookTriggerForm' +import { Route } from 'react-router-dom' import { WORKBENCH_JOB_ABS_PATH, WORKBENCH_PARAM_ID, @@ -20,12 +23,13 @@ import { WORKBENCHES_TOOLS_ABS_PATH, WORKBENCHES_TOOLS_PARAM_ID, WORKBENCHES_TOOLS_REL_PATH, - WORKBENCHES_TRIGGERS_REL_PATH, - WORKBENCHES_TRIGGERS_SCHEDULE_REL_PATH, - WORKBENCHES_TRIGGERS_WEBHOOK_REL_PATH, + WORKBENCHES_CRON_PARAM_ID, + WORKBENCHES_CRON_SCHEDULES_REL_PATH, + WORKBENCHES_WEBHOOK_PARAM_ID, + WORKBENCHES_WEBHOOK_TRIGGERS_CREATE_WEBHOOK_REL_PATH, + WORKBENCHES_WEBHOOK_TRIGGERS_REL_PATH, } from './workbenchesRoutesConsts' import { WorkbenchJob } from 'components/workbenches/workbench/job/WorkbenchJob' -import { WorkbenchTriggers } from '../components/workbenches/workbench/triggers/WorkbenchTriggers.tsx' export const workbenchesRoutes = [ } />, } - > - - } - /> - } - /> - } - /> - , + path={`${WORKBENCHES_ABS_PATH}/:${WORKBENCH_PARAM_ID}/${WORKBENCHES_CRON_SCHEDULES_REL_PATH}`} + element={} + />, + } + />, + } + />, + } + />, + } + />, + } + />, + } + />, } diff --git a/assets/src/routes/workbenchesRoutesConsts.tsx b/assets/src/routes/workbenchesRoutesConsts.tsx index 1f8a7ee0e6..220ed9d730 100644 --- a/assets/src/routes/workbenchesRoutesConsts.tsx +++ b/assets/src/routes/workbenchesRoutesConsts.tsx @@ -2,12 +2,15 @@ export const WORKBENCHES_ABS_PATH = '/workbenches' export const WORKBENCHES_TOOLS_PARAM_ID = 'toolId' export const WORKBENCH_PARAM_ID = 'workbenchId' +export const WORKBENCHES_CRON_PARAM_ID = 'cronId' +export const WORKBENCHES_WEBHOOK_PARAM_ID = 'webhookId' export const WORKBENCHES_CREATE_REL_PATH = 'create' export const WORKBENCHES_EDIT_REL_PATH = 'edit' -export const WORKBENCHES_TRIGGERS_REL_PATH = 'triggers' -export const WORKBENCHES_TRIGGERS_SCHEDULE_REL_PATH = 'schedule' -export const WORKBENCHES_TRIGGERS_WEBHOOK_REL_PATH = 'webhook' -export const WORKBENCHES_TRIGGERS_CREATE_QUERY_PARAM = 'create' +export const WORKBENCHES_CRON_SCHEDULES_REL_PATH = 'cron-schedules' +export const WORKBENCHES_WEBHOOK_TRIGGERS_REL_PATH = 'webhook-triggers' +export const WORKBENCHES_WEBHOOK_TRIGGERS_CREATE_WEBHOOK_REL_PATH = + 'create-webhook' +export const WORKBENCHES_WEBHOOK_SELECTED_QUERY_PARAM = 'selectedWebhook' export const WORKBENCHES_TOOLS_REL_PATH = 'tools' export const WORKBENCHES_ALERTS_REL_PATH = 'alerts' @@ -21,6 +24,49 @@ export const WORKBENCHES_CREATE_ABS_PATH = `${WORKBENCHES_ABS_PATH}/${WORKBENCHE export const getWorkbenchAbsPath = (workbenchId: Nullable) => `${WORKBENCHES_ABS_PATH}/${workbenchId ?? ''}` +export const getWorkbenchCronSchedulesAbsPath = ( + workbenchId: Nullable +) => + `${getWorkbenchAbsPath(workbenchId)}/${WORKBENCHES_CRON_SCHEDULES_REL_PATH}` + +export const getWorkbenchCronScheduleCreateAbsPath = ( + workbenchId: Nullable +) => + `${getWorkbenchCronSchedulesAbsPath(workbenchId)}/${WORKBENCHES_CREATE_REL_PATH}` + +export const getWorkbenchCronScheduleEditAbsPath = ({ + workbenchId, + cronId, +}: { + workbenchId: Nullable + cronId: Nullable +}) => + `${getWorkbenchCronSchedulesAbsPath(workbenchId)}/${cronId ?? ''}/${WORKBENCHES_EDIT_REL_PATH}` + +export const getWorkbenchWebhookTriggersAbsPath = ( + workbenchId: Nullable +) => + `${getWorkbenchAbsPath(workbenchId)}/${WORKBENCHES_WEBHOOK_TRIGGERS_REL_PATH}` + +export const getWorkbenchWebhookTriggerCreateAbsPath = ( + workbenchId: Nullable +) => + `${getWorkbenchWebhookTriggersAbsPath(workbenchId)}/${WORKBENCHES_CREATE_REL_PATH}` + +export const getWorkbenchWebhookTriggerCreateWebhookAbsPath = ( + workbenchId: Nullable +) => + `${getWorkbenchWebhookTriggersAbsPath(workbenchId)}/${WORKBENCHES_WEBHOOK_TRIGGERS_CREATE_WEBHOOK_REL_PATH}` + +export const getWorkbenchWebhookTriggerEditAbsPath = ({ + workbenchId, + webhookId, +}: { + workbenchId: Nullable + webhookId: Nullable +}) => + `${getWorkbenchWebhookTriggersAbsPath(workbenchId)}/${webhookId ?? ''}/${WORKBENCHES_EDIT_REL_PATH}` + export const getWorkbenchJobAbsPath = ({ workbenchId, jobId, @@ -35,3 +81,6 @@ export const WORKBENCH_JOB_ABS_PATH = getWorkbenchJobAbsPath({ workbenchId: `:${WORKBENCH_PARAM_ID}`, jobId: `:${WORKBENCH_JOBS_PARAM_JOB}`, }) + +export const WORKBENCH_WEBHOOK_TRIGGERS_PATH_MATCHER_ABS = `${WORKBENCHES_ABS_PATH}/:${WORKBENCH_PARAM_ID}/${WORKBENCHES_WEBHOOK_TRIGGERS_REL_PATH}/*` +export const WORKBENCH_JOBS_PATH_MATCHER_ABS = `${WORKBENCHES_ABS_PATH}/:${WORKBENCH_PARAM_ID}/${WORKBENCH_JOBS_REL_PATH}/*` diff --git a/config/config.exs b/config/config.exs index 724301e08f..f6530be8ea 100644 --- a/config/config.exs +++ b/config/config.exs @@ -189,8 +189,8 @@ config :req_llm, pools: %{ :default => [ protocols: [:http1], - size: 20, - count: 5, + size: 10, + count: 100, pool_max_idle_time: :timer.seconds(30), conn_max_idle_time: :timer.seconds(30) ] diff --git a/go/client/models_gen.go b/go/client/models_gen.go index 5ee3bcdce6..975e90d66c 100644 --- a/go/client/models_gen.go +++ b/go/client/models_gen.go @@ -742,6 +742,8 @@ type Alert struct { Fingerprint *string `json:"fingerprint,omitempty"` // Arbitrary key/value annotations attached to this alert Annotations map[string]any `json:"annotations,omitempty"` + // Raw webhook payload received for this alert + Payload map[string]any `json:"payload,omitempty"` // Link back to the originating dashboard or alert view in the provider URL *string `json:"url,omitempty"` // Key/value tags associated with this alert, often used for filtering clusters @@ -4160,6 +4162,8 @@ type Issue struct { Title string `json:"title"` // the detailed description or body content of the issue Body string `json:"body"` + // raw webhook payload received for this issue + Payload map[string]any `json:"payload,omitempty"` // the flow this issue is associated with Flow *Flow `json:"flow,omitempty"` // the workbench this issue is associated with @@ -9400,14 +9404,16 @@ type Workbench struct { // read policy for this service ReadBindings []*PolicyBinding `json:"readBindings,omitempty"` // write policy of this service - WriteBindings []*PolicyBinding `json:"writeBindings,omitempty"` - Runs *WorkbenchJobConnection `json:"runs,omitempty"` - Crons *WorkbenchCronConnection `json:"crons,omitempty"` - Webhooks *WorkbenchWebhookConnection `json:"webhooks,omitempty"` - Alerts *AlertConnection `json:"alerts,omitempty"` - Issues *IssueConnection `json:"issues,omitempty"` - InsertedAt *string `json:"insertedAt,omitempty"` - UpdatedAt *string `json:"updatedAt,omitempty"` + WriteBindings []*PolicyBinding `json:"writeBindings,omitempty"` + Runs *WorkbenchJobConnection `json:"runs,omitempty"` + Crons *WorkbenchCronConnection `json:"crons,omitempty"` + Prompts *WorkbenchPromptConnection `json:"prompts,omitempty"` + WorkbenchSkills *WorkbenchSkillConnection `json:"workbenchSkills,omitempty"` + Webhooks *WorkbenchWebhookConnection `json:"webhooks,omitempty"` + Alerts *AlertConnection `json:"alerts,omitempty"` + Issues *IssueConnection `json:"issues,omitempty"` + InsertedAt *string `json:"insertedAt,omitempty"` + UpdatedAt *string `json:"updatedAt,omitempty"` } type WorkbenchAttributes struct { @@ -9575,9 +9581,11 @@ type WorkbenchJobActivity struct { // the job this activity belongs to WorkbenchJob *WorkbenchJob `json:"workbenchJob,omitempty"` // the agent run that executed this activity - AgentRun *AgentRun `json:"agentRun,omitempty"` - InsertedAt *string `json:"insertedAt,omitempty"` - UpdatedAt *string `json:"updatedAt,omitempty"` + AgentRun *AgentRun `json:"agentRun,omitempty"` + // all agent runs associated with this activity (sideloadable) + AgentRuns []*AgentRun `json:"agentRuns,omitempty"` + InsertedAt *string `json:"insertedAt,omitempty"` + UpdatedAt *string `json:"updatedAt,omitempty"` } type WorkbenchJobActivityConnection struct { @@ -9710,6 +9718,11 @@ type WorkbenchJobThoughtAttributes struct { Logs []*WorkbenchJobActivityLog `json:"logs,omitempty"` } +type WorkbenchJobUpdateAttributes struct { + // the result for this job + Result *WorkbenchResultAttributes `json:"result,omitempty"` +} + type WorkbenchMessageAttributes struct { // the prompt for the message Prompt string `json:"prompt"` @@ -9729,6 +9742,71 @@ type WorkbenchObservabilityAttributes struct { Metrics *bool `json:"metrics,omitempty"` } +type WorkbenchPrompt struct { + // the id of the saved prompt + ID string `json:"id"` + // the saved prompt text + Prompt *string `json:"prompt,omitempty"` + // the workbench this prompt belongs to + Workbench *Workbench `json:"workbench,omitempty"` + InsertedAt *string `json:"insertedAt,omitempty"` + UpdatedAt *string `json:"updatedAt,omitempty"` +} + +type WorkbenchPromptAttributes struct { + // the saved prompt text + Prompt string `json:"prompt"` +} + +type WorkbenchPromptConnection struct { + PageInfo PageInfo `json:"pageInfo"` + Edges []*WorkbenchPromptEdge `json:"edges,omitempty"` +} + +type WorkbenchPromptEdge struct { + Node *WorkbenchPrompt `json:"node,omitempty"` + Cursor *string `json:"cursor,omitempty"` +} + +type WorkbenchResultAttributes struct { + // mermaid diagram text for the job result topology (only field clients may set via this mutation) + Topology string `json:"topology"` +} + +type WorkbenchSkill struct { + // the id of the saved skill + ID string `json:"id"` + // the saved skill name + Name *string `json:"name,omitempty"` + // the saved skill description + Description *string `json:"description,omitempty"` + // the saved skill contents + Contents *string `json:"contents,omitempty"` + // the workbench this skill belongs to + Workbench *Workbench `json:"workbench,omitempty"` + InsertedAt *string `json:"insertedAt,omitempty"` + UpdatedAt *string `json:"updatedAt,omitempty"` +} + +type WorkbenchSkillAttributes struct { + // the saved skill name + Name string `json:"name"` + // the saved skill description + Description *string `json:"description,omitempty"` + // the saved skill contents + Contents string `json:"contents"` +} + +type WorkbenchSkillConnection struct { + PageInfo PageInfo `json:"pageInfo"` + Edges []*WorkbenchSkillEdge `json:"edges,omitempty"` +} + +type WorkbenchSkillEdge struct { + Node *WorkbenchSkill `json:"node,omitempty"` + Cursor *string `json:"cursor,omitempty"` +} + type WorkbenchSkills struct { // git reference for skills Ref *GitRef `json:"ref,omitempty"` @@ -9743,6 +9821,11 @@ type WorkbenchSkillsAttributes struct { Files []*string `json:"files,omitempty"` } +type WorkbenchTextStream struct { + ActivityID *string `json:"activityId,omitempty"` + Text *string `json:"text,omitempty"` +} + type WorkbenchTool struct { // the id of the tool ID string `json:"id"` @@ -15859,6 +15942,7 @@ const ( WorkbenchJobActivityTypePlan WorkbenchJobActivityType = "PLAN" WorkbenchJobActivityTypeUser WorkbenchJobActivityType = "USER" WorkbenchJobActivityTypeMemory WorkbenchJobActivityType = "MEMORY" + WorkbenchJobActivityTypeConclusion WorkbenchJobActivityType = "CONCLUSION" ) var AllWorkbenchJobActivityType = []WorkbenchJobActivityType{ @@ -15871,11 +15955,12 @@ var AllWorkbenchJobActivityType = []WorkbenchJobActivityType{ WorkbenchJobActivityTypePlan, WorkbenchJobActivityTypeUser, WorkbenchJobActivityTypeMemory, + WorkbenchJobActivityTypeConclusion, } func (e WorkbenchJobActivityType) IsValid() bool { switch e { - case WorkbenchJobActivityTypeCoding, WorkbenchJobActivityTypeObservability, WorkbenchJobActivityTypeIntegration, WorkbenchJobActivityTypeTicketing, WorkbenchJobActivityTypeInfrastructure, WorkbenchJobActivityTypeMemo, WorkbenchJobActivityTypePlan, WorkbenchJobActivityTypeUser, WorkbenchJobActivityTypeMemory: + case WorkbenchJobActivityTypeCoding, WorkbenchJobActivityTypeObservability, WorkbenchJobActivityTypeIntegration, WorkbenchJobActivityTypeTicketing, WorkbenchJobActivityTypeInfrastructure, WorkbenchJobActivityTypeMemo, WorkbenchJobActivityTypePlan, WorkbenchJobActivityTypeUser, WorkbenchJobActivityTypeMemory, WorkbenchJobActivityTypeConclusion: return true } return false diff --git a/lib/console.ex b/lib/console.ex index e1f46e8684..6bcd95d2f1 100644 --- a/lib/console.ex +++ b/lib/console.ex @@ -313,6 +313,14 @@ defmodule Console do |> Base.encode16(case: :lower) end + def safely(fun, fallback) when is_function(fun, 0) and is_function(fallback, 1) do + try do + fun.() + catch + _, err -> fallback.(err) + end + end + def async_retry(fun, tries \\ 0, res \\ :error) def async_retry(_, 3, res), do: res def async_retry(fun, tries, _) do diff --git a/lib/console/ai/chat.ex b/lib/console/ai/chat.ex index a7bcb7bb6f..91ae82f060 100644 --- a/lib/console/ai/chat.ex +++ b/lib/console/ai/chat.ex @@ -412,7 +412,13 @@ defmodule Console.AI.Chat do Finds all MCP tools associated with a chat thread """ @spec find_tools(ChatThread.t) :: {:ok, [McpTool.t]} | Console.error - def find_tools(%ChatThread{flow: %Flow{servers: [_ | _]}} = thread), do: Discovery.tools(thread) + def find_tools(%ChatThread{flow: %Flow{servers: [_ | _]}} = thread) do + case Discovery.tools(thread) do + {:ok, tools} -> {:ok, tools} + {:error, _} = err -> err + err -> {:error, "internal mcp error: #{inspect(err)}"} + end + end def find_tools(_), do: {:ok, []} @doc """ diff --git a/lib/console/ai/chat/memory_engine.ex b/lib/console/ai/chat/memory_engine.ex index bcac80d9cd..bac18d6de3 100644 --- a/lib/console/ai/chat/memory_engine.ex +++ b/lib/console/ai/chat/memory_engine.ex @@ -69,6 +69,7 @@ defmodule Console.AI.Chat.MemoryEngine do |> fun.(acc) |> case do {:halt, res} -> {:ok, res} + {:message, msg} ->loop(%{engine | acc: acc, messages: messages ++ [msg]}, iter + 1) {:cont, acc} -> loop(%{engine | acc: acc, messages: messages ++ msgs}, iter + 1) end end diff --git a/lib/console/ai/fixer/stack.ex b/lib/console/ai/fixer/stack.ex index 89579f0764..a5ee0c1de0 100644 --- a/lib/console/ai/fixer/stack.ex +++ b/lib/console/ai/fixer/stack.ex @@ -81,11 +81,5 @@ defmodule Console.AI.Fixer.Stack do """ end - defp last_run(%Stack{} = stack) do - StackRun.for_stack(stack.id) - |> StackRun.for_status(:failed) - |> StackRun.ordered(desc: :id) - |> StackRun.limit(1) - |> Repo.one() - end + defp last_run(%Stack{} = stack), do: Stacks.last_failed_run(stack.id) end diff --git a/lib/console/ai/mcp/client.ex b/lib/console/ai/mcp/client.ex index c2726d0d67..735d78f6f6 100644 --- a/lib/console/ai/mcp/client.ex +++ b/lib/console/ai/mcp/client.ex @@ -5,3 +5,11 @@ defmodule Console.AI.MCP.Client do protocol_version: "2025-03-26", capabilities: [:roots, :sampling] end + +defmodule Console.AI.MCP.LegacyClient do + use Hermes.Client, + name: "Plural", + version: "1.0.0", + protocol_version: "2024-11-05", + capabilities: [:roots, :sampling] +end diff --git a/lib/console/ai/mcp/client_supervisor.ex b/lib/console/ai/mcp/client_supervisor.ex index a3fa40e712..5793fe9171 100644 --- a/lib/console/ai/mcp/client_supervisor.ex +++ b/lib/console/ai/mcp/client_supervisor.ex @@ -18,14 +18,20 @@ defmodule Console.AI.MCP.ClientSupervisor do end def server_child(%ChatThread{} = t, %McpServer{url: url, protocol: proto} = s) do - Console.AI.MCP.Client.child_spec([ + proto = proto || :sse + mod = client_module(proto) + + mod.child_spec([ client_name: Agent.name(:client, t, s), transport_name: Agent.name(:transport, t, s), - transport: {proto || :sse, [base_url: url, headers: auth_headers(t, s)]} + transport: {proto, [base_url: url, headers: auth_headers(t, s)]} ]) |> Map.put(:restart, :transient) end + def client_module(:sse), do: Console.AI.MCP.LegacyClient + def client_module(_), do: Console.AI.MCP.Client + defp auth_headers(%ChatThread{user: %User{} = user}, %McpServer{authentication: %{plural: true}}) do {:ok, jwt, _} = MCP.mint(user) %{"Authorization" => "Bearer #{jwt}"} diff --git a/lib/console/ai/provider/base.ex b/lib/console/ai/provider/base.ex index d65dce29ac..075ebd09d6 100644 --- a/lib/console/ai/provider/base.ex +++ b/lib/console/ai/provider/base.ex @@ -14,10 +14,10 @@ defmodule Console.AI.Provider.Base do |> reqllm_tools() end - def generate_text(messages, model, %Stream{}, opts) do + def generate_text(messages, model, %Stream{} = s, opts) do with {:ok, model} <- model(model), {:ok, stream} <- stream_retrier(model, messages, opts), - {:ok, result} <- StreamResponse.process_stream(stream, on_result: &Stream.publish/1) do + {:ok, result} <- StreamResponse.process_stream(stream, Stream.stream_options(s)) do Stream.offset(1) {:ok, result} end @@ -33,15 +33,20 @@ defmodule Console.AI.Provider.Base do {:system, content} -> [Context.system(content)] {:user, content} -> [Context.user(content)] {:assistant, content} -> [Context.assistant(content)] - {:tool, content, %{call_id: id, name: name, arguments: args}} when is_binary(id) and is_binary(content) -> + {:tool, content, %{call_id: id, name: name, arguments: args}} when is_binary(content) -> + id = tid(id) [Context.assistant("", tool_calls: [ToolCall.new(id, name, Jason.encode!(args))]), Context.tool_result(id, content)] - {:tool, content, %{call_id: id, name: name, arguments: args}} when is_binary(id) -> + {:tool, content, %{call_id: id, name: name, arguments: args}} -> + id = tid(id) [Context.assistant("", tool_calls: [ToolCall.new(id, name, Jason.encode!(args))]), Context.tool_result(id, Poison.encode!(content))] {:tool, content} -> [Context.tool_result("unknown", content)] end) |> Context.new() end + defp tid(id) when is_binary(id), do: id + defp tid(_id), do: Ecto.UUID.generate() + def reqllm_tools(tools) do Enum.map(tools, fn %Console.AI.MCP.Tool{name: name, description: description, input_schema: schema} -> @@ -85,8 +90,12 @@ defmodule Console.AI.Provider.Base do defp model(%LLMDB.Model{} = model), do: {:ok, model} defp model(model), do: {:error, "invalid model: #{inspect(model)}"} - defp to_tool(%ToolCall{id: id, function: %{name: name, arguments: args}}), - do: %Tool{id: id, name: name, arguments: JSON.decode!(args)} + defp to_tool(%ToolCall{id: id, function: %{name: name, arguments: args}}) do + case JSON.decode(args) do + {:ok, args} -> %Tool{id: id, name: name, arguments: args} + _ -> %Tool{id: id, name: name, arguments: %{}} + end + end defp stream_retrier(model, messages, opts, retries \\ 0) defp stream_retrier(model, messages, opts, retries) when retries < 2 do diff --git a/lib/console/ai/stream.ex b/lib/console/ai/stream.ex index 6d4b185de9..861f84f108 100644 --- a/lib/console/ai/stream.ex +++ b/lib/console/ai/stream.ex @@ -2,7 +2,7 @@ defmodule Console.AI.Stream do alias Console.Schema.User alias ConsoleWeb.AIChannel - defstruct [:topic, role: :assistant, offset: 0, msg: 0, index: 0, subagent: false] + defstruct [:topic, stream_callbacks: [], role: :assistant, offset: 0, msg: 0, index: 0, subagent: false] @stream {__MODULE__, :ai, :stream} @tool {__MODULE__, :ai, :tool} @@ -22,6 +22,10 @@ defmodule Console.AI.Stream do end end + def stream_callbacks(callbacks) do + Process.put(@stream, %__MODULE__{stream_callbacks: callbacks}) + end + def stream(role) do case Process.get(@stream) do %__MODULE__{} = s -> %{s | role: role} @@ -38,6 +42,10 @@ defmodule Console.AI.Stream do end end + def stream_options(%__MODULE__{stream_callbacks: [_ | _] = callbacks}), + do: callbacks + def stream_options(_), do: [on_result: &publish/1] + def publish(c) do case stream() do %__MODULE__{topic: topic, offset: offset, msg: msg, role: role, index: index, subagent: false} = s -> diff --git a/lib/console/ai/tools/workbench/coding_agent.ex b/lib/console/ai/tools/workbench/coding_agent.ex index 12fead86d6..95b689f02f 100644 --- a/lib/console/ai/tools/workbench/coding_agent.ex +++ b/lib/console/ai/tools/workbench/coding_agent.ex @@ -1,10 +1,11 @@ defmodule Console.AI.Tools.Workbench.CodingAgent do use Console.AI.Tools.Workbench.Base alias Console.AI.Tool - alias Console.Schema.{User, AgentRuntime, AgentRun} + alias Console.Schema.{User, AgentRuntime, AgentRun, Workbench} alias Console.Deployments.Agents embedded_schema do + field :workbench, :map, virtual: true field :mode, AgentRun.Mode field :repository, :string field :prompt, :string @@ -12,26 +13,38 @@ defmodule Console.AI.Tools.Workbench.CodingAgent do @valid ~w(mode repository prompt)a - def changeset(model, attrs) do + def changeset(%__MODULE__{workbench: bench} = model, attrs) do model |> cast(attrs, @valid) |> validate_required(@valid) + |> validate_mode(bench) + |> validate_repository(bench) end + defp validate_mode(cs, %Workbench{configuration: %{coding: %{mode: :read}}}) do + case get_change(cs, :mode) do + :write -> + add_error(cs, :mode, "write mode is not allowed for workbenches that specify read-only coding agents") + _ -> cs + end + end + defp validate_mode(cs, _), do: cs + + # defp validate_repository(cs, %Workbench{configuration: %{coding: %{repositories: [_ | _] = repos}}}), + # do: validate_inclusion(cs, :repository, repos) + defp validate_repository(cs, _), do: cs + @json_schema Console.priv_file!("tools/workbench/coding_agent.json") |> Jason.decode!() - def json_schema(), do: @json_schema - def name(), do: "workbench_coding_agent" - def description(), do: "Invokes a coding agent to make a code change with the given prompt and repository. Only use this once you've gathered enough information to craft an effective prompt" + def json_schema(_), do: @json_schema + def name(_), do: "workbench_coding_agent" + def description(_), do: "Invokes a coding agent to make a code change with the given prompt and repository. Only use this once you've gathered enough information to craft an effective prompt to either analyze the code in question or modify it and generate a reviewable PR." - def implement(%__MODULE__{mode: mode, repository: repository, prompt: prompt}) do + def implement(_, %__MODULE__{id: tool, mode: mode, repository: repo, prompt: prompt}) do with {:user, %User{} = user} <- {:user, Tool.actor()}, - {:runtime, %AgentRuntime{} = runtime} <- {:runtime, Tool.agent_runtime()} do - Agents.create_agent_run(%{ - repository: repository, - prompt: prompt, - mode: mode - }, runtime.id, user) + {:runtime, %AgentRuntime{} = runtime} <- {:runtime, Tool.agent_runtime()}, + {:ok, run} <- Agents.create_agent_run(%{repository: repo, prompt: prompt, mode: mode}, runtime.id, user) do + {:ok, %{run | tool: tool}} else {:user, _} -> {:error, "no actor found for this session"} {:runtime, _} -> {:error, "no runtime found, you need to manually specify this in the chat context menu"} diff --git a/lib/console/ai/tools/workbench/fech_notes.ex b/lib/console/ai/tools/workbench/fech_notes.ex index a12d118599..fc1c045277 100644 --- a/lib/console/ai/tools/workbench/fech_notes.ex +++ b/lib/console/ai/tools/workbench/fech_notes.ex @@ -21,4 +21,5 @@ defmodule Console.AI.Tools.Workbench.FetchNotes do Console.mapify(result) |> Jason.encode() end + def implement(_, _), do: {:ok, "no notes set up yet"} end diff --git a/lib/console/ai/tools/workbench/infrastructure/cluster.ex b/lib/console/ai/tools/workbench/infrastructure/cluster.ex new file mode 100644 index 0000000000..736e92790a --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/cluster.ex @@ -0,0 +1,48 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.Cluster do + use Console.AI.Tools.Agent.Base + alias Console.Repo + alias Console.Deployments.{Clusters, Policies} + alias Console.Schema.{User} + + require EEx + + embedded_schema do + field :user, :map, virtual: true + field :handle, :string + end + + @valid ~w(handle)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> validate_required(@valid) + end + + @json_schema Console.priv_file!("tools/workbench/infrastructure/cluster.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_cluster" + def description(_), do: "Get the details of a cluster from the Plural API." + + def implement(_, %__MODULE__{user: %User{} = user, handle: handle}) do + Clusters.get_cluster_by_handle(handle) + |> Repo.preload([:tags, :project]) + |> Policies.allow(user, :read) + |> case do + {:ok, cluster} -> + {:ok, cluster_prompt(cluster: cluster, upgrade_plan: Clusters.upgrade_plan(cluster))} + + nil -> {:error, "could not find cluster with handle #{handle}"} + error -> error + end + end + + EEx.function_from_file( + :defp, + :cluster_prompt, + Path.join(:code.priv_dir(:console), "prompts/workbench/infrastructure/cluster.md.eex"), + [:assigns], + trim: true + ) +end diff --git a/lib/console/ai/tools/workbench/infrastructure/cluster_list.ex b/lib/console/ai/tools/workbench/infrastructure/cluster_list.ex new file mode 100644 index 0000000000..f94f5aa26c --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/cluster_list.ex @@ -0,0 +1,72 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.ClusterList do + use Console.AI.Tools.Agent.Base + alias Console.Deployments.Clusters + alias Console.Repo + alias Console.Schema.{Cluster, User} + + embedded_schema do + field :user, :map, virtual: true + field :q, :string + end + + @valid ~w(q)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + end + + @json_schema Console.priv_file!("tools/workbench/infrastructure/cluster_list.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_clusters" + def description(_), do: "List Kubernetes clusters the current user can read. Returns compact JSON; use plrl_cluster with a handle for full details." + + def implement(_, %__MODULE__{user: %User{} = user, q: q}) do + Cluster.ordered() + |> Cluster.for_user(user) + |> Cluster.preloaded([:tags, :project]) + |> maybe_search(q) + |> Repo.all() + |> Enum.map(&cluster_brief/1) + |> Jason.encode() + end + + defp maybe_search(query, q) when is_binary(q) and q != "", do: Cluster.search(query, q) + defp maybe_search(query, _), do: query + + defp cluster_brief(%Cluster{} = c) do + %{ + id: c.id, + handle: c.handle, + name: c.name, + distro: c.distro, + self: c.self, + virtual: c.virtual, + installed: c.installed, + current_version: c.current_version, + version: c.version, + upgrade_plan: upgrade_plan_brief(c.upgrade_plan), + extended_support: extended_support_brief(Clusters.extended_support(c)), + project: c.project && %{id: c.project.id, name: c.project.name}, + tags: Enum.map(c.tags || [], &%{name: &1.name, value: &1.value}) + } + end + + defp upgrade_plan_brief(nil), do: nil + + defp upgrade_plan_brief(plan) do + %{ + compatibilities: plan.compatibilities, + deprecations: plan.deprecations, + incompatibilities: plan.incompatibilities, + kubelet_skew: plan.kubelet_skew + } + end + + defp extended_support_brief(nil), do: nil + + defp extended_support_brief(%{extended: _, extended_from: _} = info) do + %{extended: info.extended, extended_from: info.extended_from} + end +end diff --git a/lib/console/ai/tools/workbench/infrastructure/cluster_services.ex b/lib/console/ai/tools/workbench/infrastructure/cluster_services.ex new file mode 100644 index 0000000000..8897ea9832 --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/cluster_services.ex @@ -0,0 +1,58 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.ClusterServices do + use Console.AI.Tools.Agent.Base + alias Console.Deployments.{Clusters, Policies} + alias Console.Repo + alias Console.Schema.{Cluster, Service, User} + + embedded_schema do + field :user, :map, virtual: true + field :cluster, :string + field :q, :string + end + + @valid ~w(cluster q)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> validate_required(~w(cluster)a) + end + + @json_schema Console.priv_file!("tools/workbench/infrastructure/cluster_services.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_cluster_services" + def description(_), do: "List Plural services deployed to a cluster. Pass the cluster handle from plrl_clusters; use plrl_service with a service id for details." + + def implement(_, %__MODULE__{user: %User{} = user, cluster: handle, q: q}) do + with %Cluster{} = cluster <- Clusters.get_cluster_by_handle(handle), + {:ok, _} <- Policies.allow(cluster, user, :read) do + Service.for_user(user) + |> Service.for_cluster(cluster.id) + |> Service.ordered() + |> maybe_search(q) + |> Repo.all() + |> Repo.preload([:cluster]) + |> Enum.map(&service_brief/1) + |> Jason.encode() + else + nil -> {:error, "could not find cluster with handle #{handle}"} + error -> error + end + end + + defp maybe_search(query, q) when is_binary(q) and q != "", do: Service.search(query, q) + defp maybe_search(query, _), do: query + + defp service_brief(%Service{} = s) do + %{ + id: s.id, + name: s.name, + namespace: s.namespace, + status: s.status, + sha: s.sha, + cluster_id: s.cluster_id, + cluster_handle: s.cluster && s.cluster.handle + } + end +end diff --git a/lib/console/ai/tools/workbench/infrastructure/kube_get.ex b/lib/console/ai/tools/workbench/infrastructure/kube_get.ex new file mode 100644 index 0000000000..ecd93c16f5 --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/kube_get.ex @@ -0,0 +1,59 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.KubeGet do + use Console.AI.Tools.Agent.Base + import Console.GraphQl.Resolvers.Kubernetes, only: [get_kind: 4] + alias Console.Schema.{Cluster} + alias Console.Deployments.Clusters + + embedded_schema do + field :user, :map, virtual: true + field :cluster, :string + field :group, :string + field :version, :string + field :kind, :string + field :name, :string + field :namespace, :string + end + + @valid ~w(cluster group version kind name namespace)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> validate_required([:cluster, :version, :kind, :name]) + end + + @json_schema Console.priv_file!("tools/workbench/infrastructure/kube_get.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "describe_k8s_resource" + def description(_) do + """ + Fetches a resource from kubernetes and returns its full definition as a json object. This is only possible if the user has necessary RBAC permissions + """ + end + + @kind_blacklist ~w(secrets) + + def implement(_, %__MODULE__{user: user, cluster: handle, group: g, version: v, kind: k} = comp) do + with {:cluster, %Cluster{} = cluster} <- {:cluster, Clusters.get_cluster_by_handle(handle)}, + {:kind, kind} when kind not in @kind_blacklist <- {:kind, get_kind(cluster, g, v, k)}, + path <- Kube.Client.Base.path(g, v, kind, comp.namespace, comp.name), + {:ok, res} <- kube_request(cluster, user, path) do + Jason.encode(res) + else + {:kind, _} -> {:ok, "I cannot fetch the details of secrets for you"} + {:cluster, _} -> {:ok, "No cluster found matching handle=#{handle}"} + err -> {:error, "Error fetching resource: #{inspect(err)}"} + end + end + + def kube_request(cluster, user, path) do + %Kazan.Request{ + method: "get", + path: path, + query_params: %{}, + response_model: Kube.Client.EchoModel + } + |> Kazan.run(server: Clusters.control_plane(cluster, user)) + end +end diff --git a/lib/console/ai/tools/workbench/infrastructure/kube_list.ex b/lib/console/ai/tools/workbench/infrastructure/kube_list.ex new file mode 100644 index 0000000000..0d7f605779 --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/kube_list.ex @@ -0,0 +1,49 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.KubeList do + use Console.AI.Tools.Agent.Base + import Console.GraphQl.Resolvers.Kubernetes, only: [get_kind: 4] + import Console.AI.Tools.Workbench.Infrastructure.KubeGet, only: [kube_request: 3] + alias Console.Deployments.Clusters + alias Console.Schema.{Cluster} + + embedded_schema do + field :user, :map, virtual: true + field :cluster, :string + field :group, :string + field :version, :string + field :kind, :string + field :namespace, :string + end + + @valid ~w(cluster group version kind namespace)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> validate_required([:cluster, :version, :kind]) + end + + @json_schema Console.priv_file!("tools/workbench/infrastructure/kube_list.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "list_k8s_resources" + def description(_) do + """ + Lists resources from kubernetes and returns their full definitions as a json object. This is only possible if the user has necessary RBAC permissions + """ + end + + @kind_blacklist ~w(secrets) + + def implement(_, %__MODULE__{user: user, cluster: handle, group: g, version: v, kind: k} = comp) do + with {:cluster, %Cluster{} = cluster} <- {:cluster, Clusters.get_cluster_by_handle(handle)}, + {:kind, kind} when kind not in @kind_blacklist <- {:kind, get_kind(cluster, g, v, k)}, + path <- Kube.Client.Base.path(g, v, kind, comp.namespace), + {:ok, res} <- kube_request(cluster, user, path) do + Jason.encode(res) + else + {:kind, _} -> {:ok, "I cannot list secrets for you"} + {:cluster, _} -> {:ok, "No cluster found matching handle=#{handle}"} + err -> {:error, "Error fetching resource: #{inspect(err)}"} + end + end +end diff --git a/lib/console/ai/tools/workbench/infrastructure/service_files.ex b/lib/console/ai/tools/workbench/infrastructure/service_files.ex new file mode 100644 index 0000000000..57a16fcaab --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/service_files.ex @@ -0,0 +1,38 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.ServiceFiles do + use Console.AI.Tools.Agent.Base + alias Console.Schema.{Service, User} + alias Console.Deployments.Services + alias Console.AI.Fixer.Service, as: ServiceFixer + + embedded_schema do + field :service_id, :string + end + + @valid ~w(service_id)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> check_uuid(:service_id) + |> validate_required(@valid) + end + + @json_schema Console.priv_file!("tools/agent/coding/service_files.json") |> Jason.decode!() + + def json_schema(), do: @json_schema + def name(), do: plrl_tool("service_files") + def description(), do: "Finds the terraform files for a Plural service and renders them as a sequence of messages" + + def implement(%__MODULE__{service_id: id}) do + Console.AI.Fixer.Base.raw() + with %Service{} = service <- Services.get_service(id) |> Console.Repo.preload([:repository]), + %User{} = user <- Tool.actor(), + {:ok, service} <- Policies.allow(service, user, :write), + {:ok, result} <- ServiceFixer.file_contents(service, ctx_window_scale: 0.5) do + Jason.encode(result) + else + {:error, err} -> {:error, "failed to get service files, reason: #{inspect(err)}"} + nil -> {:error, "could not find service with id #{id}"} + end + end +end diff --git a/lib/console/ai/tools/workbench/infrastructure/service_inspect.ex b/lib/console/ai/tools/workbench/infrastructure/service_inspect.ex new file mode 100644 index 0000000000..6c63502731 --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/service_inspect.ex @@ -0,0 +1,47 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.ServiceInspect do + use Console.AI.Tools.Agent.Base + alias Console.Repo + alias Console.Deployments.{Policies, Services} + alias Console.Schema.{User} + + require EEx + + embedded_schema do + field :user, :map, virtual: true + field :service_id, :string + end + + @valid ~w(service_id)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> check_uuid(:service_id) + |> validate_required(@valid) + end + + @json_schema Console.priv_file!("tools/workbench/infrastructure/service.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_service" + def description(_), do: "Get detailed information about a Plural service by id (from plrl_cluster_services)." + + def implement(_, %__MODULE__{user: %User{} = user, service_id: id}) do + Services.get_service(id) + |> Repo.preload([:repository, :cluster, :errors]) + |> Policies.allow(user, :read) + |> case do + {:ok, svc} -> {:ok, service_prompt(service: svc)} + nil -> {:error, "could not find service with id #{id}"} + error -> error + end + end + + EEx.function_from_file( + :defp, + :service_prompt, + Path.join(:code.priv_dir(:console), "prompts/workbench/infrastructure/service.md.eex"), + [:assigns], + trim: true + ) +end diff --git a/lib/console/ai/tools/workbench/infrastructure/stack_files.ex b/lib/console/ai/tools/workbench/infrastructure/stack_files.ex new file mode 100644 index 0000000000..db31e1ce15 --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/stack_files.ex @@ -0,0 +1,48 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.StackFiles do + use Console.AI.Tools.Agent.Base + alias Console.Schema.{Stack, User} + alias Console.Deployments.Stacks + + require EEx + + embedded_schema do + field :stack_id, :string + end + + @valid ~w(stack_id)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> check_uuid(:stack_id) + |> validate_required(@valid) + end + + @json_schema Console.priv_file!("tools/agent/coding/stack_files.json") |> Jason.decode!() + + def json_schema(), do: @json_schema + def name(), do: plrl_tool("stack_files") + def description(), do: "Finds the terraform files for a stack and renders them as a yaml list" + + def implement(%__MODULE__{stack_id: id}) do + Console.AI.Fixer.Base.raw() + with %Stack{} = stack <- Stacks.get_stack(id) |> Console.Repo.preload([:repository, parent: [:cluster]]), + %User{} = user <- Tool.actor(), + {:ok, stack} <- Policies.allow(stack, user, :write) do + {:ok, stack_prompt(stack: stack, failed_run_diagnostics: nil)} + else + {:error, err} -> + {:error, "failed to get stack files, reason: #{inspect(err)}"} + nil -> + {:error, "could not find stack with id #{id}"} + end + end + + EEx.function_from_file( + :defp, + :stack_prompt, + Path.join(:code.priv_dir(:console), "prompts/workbench/infrastructure/stack.md.eex"), + [:assigns], + trim: true + ) +end diff --git a/lib/console/ai/tools/workbench/infrastructure/stack_inspect.ex b/lib/console/ai/tools/workbench/infrastructure/stack_inspect.ex new file mode 100644 index 0000000000..e21326efb0 --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/stack_inspect.ex @@ -0,0 +1,71 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.StackInspect do + use Console.AI.Tools.Agent.Base + alias Console.Repo + alias Console.Deployments.{Policies, Stacks} + alias Console.Schema.{Stack, StackRun, User, RunStep} + + require EEx + + embedded_schema do + field :user, :map, virtual: true + field :stack_id, :string + end + + @valid ~w(stack_id)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> check_uuid(:stack_id) + |> validate_required(@valid) + end + + @json_schema Console.priv_file!("tools/workbench/infrastructure/stack.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_stack" + def description(_), do: "Get detailed information about an infrastructure stack by id (from plrl_stacks)." + + def implement(_, %__MODULE__{user: %User{} = user, stack_id: id}) do + Stacks.get_stack(id) + |> Repo.preload([:repository, :cluster, :project, parent: [:cluster]]) + |> Policies.allow(user, :read) + |> case do + {:ok, stack} -> {:ok, stack_prompt(stack: stack, failed_run: failed_run(stack))} + nil -> {:error, "could not find stack with id #{id}"} + error -> error + end + end + + defp failed_run(%Stack{status: :failed, id: sid}) do + with %StackRun{} = run <- Stacks.last_failed_run(sid), + %RunStep{} = step <- failing_step(run) do + build_failed_run(run, step) + end + end + defp failed_run(_), do: nil + + defp build_failed_run(%StackRun{} = run, %RunStep{} = step) do + %{ + run_id: run.id, + run_message: run.message, + run_errors: Enum.map(run.errors || [], & %{source: &1.source, message: &1.message}), + failing_step: step + } + end + + defp failing_step(%StackRun{steps: steps}) when is_list(steps) do + steps + |> Enum.filter(&(&1.status == :failed)) + |> Enum.sort_by(& &1.index, :desc) + |> List.first() + end + + EEx.function_from_file( + :defp, + :stack_prompt, + Path.join(:code.priv_dir(:console), "prompts/workbench/infrastructure/stack.md.eex"), + [:assigns], + trim: true + ) +end diff --git a/lib/console/ai/tools/workbench/infrastructure/stack_list.ex b/lib/console/ai/tools/workbench/infrastructure/stack_list.ex new file mode 100644 index 0000000000..d997f06efe --- /dev/null +++ b/lib/console/ai/tools/workbench/infrastructure/stack_list.ex @@ -0,0 +1,67 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.StackList do + use Console.AI.Tools.Agent.Base + alias Console.Repo + alias Console.Schema.{Stack, User} + + embedded_schema do + field :user, :map, virtual: true + field :q, :string + field :project_id, :string + field :status, Console.Schema.Stack.Status + end + + @valid ~w(q project_id status)a + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + end + + @json_schema Console.priv_file!("tools/workbench/infrastructure/stack_list.json") |> Jason.decode!() + + def json_schema(_), do: @json_schema + def name(_), do: "plrl_stacks" + def description(_), do: "List infrastructure stacks the user can read. Returns compact JSON; use plrl_stack with a stack id for full details." + + def implement(_, %__MODULE__{user: %User{} = user} = args) do + Stack.for_user(user) + |> stack_filters(args) + |> maybe_search(args.q) + |> Stack.distinct() + |> Stack.ordered() + |> Repo.all() + |> Repo.preload([:cluster, :project]) + |> Enum.map(&stack_brief/1) + |> Jason.encode() + end + + defp stack_filters(query, %__MODULE__{project_id: pid, status: st}) do + query + |> maybe_project(pid) + |> maybe_status(st) + end + + defp maybe_project(q, pid) when is_binary(pid) and pid != "", do: Stack.for_project(q, pid) + defp maybe_project(q, _), do: q + + defp maybe_status(q, s) when not is_nil(s), do: Stack.for_status(q, s) + defp maybe_status(q, _), do: q + + defp maybe_search(query, q) when is_binary(q) and byte_size(q) > 0, + do: Stack.search(query, q) + defp maybe_search(query, _), do: query + + defp stack_brief(%Stack{} = s) do + %{ + id: s.id, + name: s.name, + type: s.type, + status: s.status, + approval: s.approval, + cluster_id: s.cluster_id, + cluster_handle: s.cluster && s.cluster.handle, + project_id: s.project_id, + project_name: s.project && s.project.name + } + end +end diff --git a/lib/console/ai/tools/workbench/observability/logs.ex b/lib/console/ai/tools/workbench/observability/logs.ex index dadd44c79a..416063b7ca 100644 --- a/lib/console/ai/tools/workbench/observability/logs.ex +++ b/lib/console/ai/tools/workbench/observability/logs.ex @@ -1,6 +1,6 @@ defmodule Console.AI.Tools.Workbench.Observability.Logs do use Console.AI.Tools.Workbench.Base - alias Console.AI.Tools.Workbench.Observability.TimeRange + alias Console.AI.Tools.Workbench.Observability.{TimeRange, Metrics} alias CloudQuery.Client alias Toolquery.ToolQuery.{Stub} alias Toolquery.{LogsQueryInput, LogsQueryOutput, LogsQueryFacet} @@ -23,7 +23,7 @@ defmodule Console.AI.Tools.Workbench.Observability.Logs do def json_schema(_), do: Console.priv_file!("tools/workbench/observability/logs.json") |> Jason.decode!() def name(%__MODULE__{tool: %{name: n}}), do: "workbench_observability_logs_#{n}" - def description(%__MODULE__{tool: %{name: n}}), do: "Gather logs from the #{n} observability connection" + def description(%__MODULE__{tool: %{name: n} = t}), do: String.trim("Gather logs from the #{n} observability connection. #{Metrics.provider_hint(t)}") def changeset(model, attrs) do model diff --git a/lib/console/ai/tools/workbench/observability/metrics.ex b/lib/console/ai/tools/workbench/observability/metrics.ex index 09211e3fc9..01cde51277 100644 --- a/lib/console/ai/tools/workbench/observability/metrics.ex +++ b/lib/console/ai/tools/workbench/observability/metrics.ex @@ -18,7 +18,7 @@ defmodule Console.AI.Tools.Workbench.Observability.Metrics do def json_schema(_), do: Console.priv_file!("tools/workbench/observability/metrics.json") |> Jason.decode!() def name(%__MODULE__{tool: %{name: n}}), do: "workbench_observability_metrics_#{n}" - def description(%__MODULE__{tool: %{name: n}}), do: "Gather metrics from the #{n} observability connection" + def description(%__MODULE__{tool: %{name: n} = t}), do: String.trim("Gather metrics from the #{n} observability connection. #{provider_hint(t)}") def changeset(model, attrs) do model @@ -45,4 +45,11 @@ defmodule Console.AI.Tools.Workbench.Observability.Metrics do }} end end + + + @known_providers ~w(prometheus datadog elastic loki splunk tempo dynatrace newrelic)a + + def provider_hint(%Console.Schema.WorkbenchTool{tool: type}) when type in @known_providers, + do: "This tool is configured against #{type}, and so you should be able to use its documented query format as needed." + def provider_hint(_), do: "" end diff --git a/lib/console/ai/tools/workbench/observability/plrl_logs.ex b/lib/console/ai/tools/workbench/observability/plrl_logs.ex new file mode 100644 index 0000000000..c4d29de537 --- /dev/null +++ b/lib/console/ai/tools/workbench/observability/plrl_logs.ex @@ -0,0 +1,62 @@ +defmodule Console.AI.Tools.Workbench.Observability.Plrl.Logs do + use Console.AI.Tools.Workbench.Base + import Piazza.Ecto.Schema + alias Console.Logs.{Query, Provider, Time} + + embedded_schema do + field :user, :map, virtual: true + field :service_id, :string + field :cluster_id, :string + field :query, :string + field :limit, :integer + + embeds_many :facets, Facet, on_replace: :delete, primary_key: false do + field :name, :string + field :value, :string + end + + embeds_one :time_range, TimeRange, on_replace: :update + end + + @valid ~w(service_id cluster_id query limit)a + + def json_schema(_), do: Console.priv_file!("tools/workbench/observability/plrl_logs.json") |> Jason.decode!() + def name(_), do: "plrl_logs" + def description(_), do: "Gather logs from the Plural's built-in log aggregation integration. This requires either a Plural service_id or Plural cluster_id to be provided to authorize log extraction" + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> cast_embed(:time_range) + |> cast_embed(:facets, with: &facet_changeset/2) + |> validate_one_present([:service_id, :cluster_id]) + end + + defp facet_changeset(model, attrs) do + model + |> cast(attrs, [:name, :value]) + |> validate_required([:name, :value]) + end + + def implement(_, %__MODULE__{user: user} = logs) do + query = logs_query(logs) + with {:ok, query} <- Query.accessible(query, user), + {:ok, logs} <- Provider.query(query) do + Jason.encode(logs) + end + end + + def logs_query(%{query: q, limit: l, facets: f, time_range: tr}) do + Query.new( + query: q, + limit: l, + facets: Enum.map(f || [], & %{key: &1.name, value: &1.value}), + time: to_time(tr) + ) + end + + defp to_time(%{start: %{} = start_ts, end: %{} = end_ts}) do + %Time{before: end_ts, after: start_ts} + end + defp to_time(_), do: %Time{before: Timex.now(), after: Timex.now() |> Timex.shift(minutes: -30)} +end diff --git a/lib/console/ai/tools/workbench/observability/plrl_logs_aggregate.ex b/lib/console/ai/tools/workbench/observability/plrl_logs_aggregate.ex new file mode 100644 index 0000000000..8caa02fe58 --- /dev/null +++ b/lib/console/ai/tools/workbench/observability/plrl_logs_aggregate.ex @@ -0,0 +1,49 @@ +defmodule Console.AI.Tools.Workbench.Observability.Plrl.LogsAggregate do + use Console.AI.Tools.Workbench.Base + import Piazza.Ecto.Schema + alias Console.AI.Tools.Workbench.Observability.Plrl.Logs + alias Console.Logs.{Query, Provider} + + embedded_schema do + field :user, :map, virtual: true + field :service_id, :string + field :cluster_id, :string + field :query, :string + field :limit, :integer + + embeds_many :facets, Facet, on_replace: :delete, primary_key: false do + field :name, :string + field :value, :string + end + + embeds_one :time_range, TimeRange, on_replace: :update + end + + @valid ~w(service_id cluster_id query limit)a + + def json_schema(_), do: Console.priv_file!("tools/workbench/observability/plrl_logs.json") |> Jason.decode!() + def name(_), do: "plrl_logs_aggregate" + def description(_), do: "Gather log count metrics from Plural's built-in log aggregation integration, especially useful for detecting spikes in logs during certain time periods. This requires either a Plural service_id or Plural cluster_id to be provided to authorize log extraction" + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> cast_embed(:time_range) + |> cast_embed(:facets, with: &facet_changeset/2) + |> validate_one_present([:service_id, :cluster_id]) + end + + defp facet_changeset(model, attrs) do + model + |> cast(attrs, [:name, :value]) + |> validate_required([:name, :value]) + end + + def implement(_, %__MODULE__{user: user} = logs) do + query = Logs.logs_query(logs) + with {:ok, query} <- Query.accessible(query, user), + {:ok, logs} <- Provider.aggregate(query) do + Jason.encode(logs) + end + end +end diff --git a/lib/console/ai/tools/workbench/observability/plrl_logs_labels.ex b/lib/console/ai/tools/workbench/observability/plrl_logs_labels.ex new file mode 100644 index 0000000000..29a0cd0879 --- /dev/null +++ b/lib/console/ai/tools/workbench/observability/plrl_logs_labels.ex @@ -0,0 +1,50 @@ +defmodule Console.AI.Tools.Workbench.Observability.Plrl.LogLabels do + use Console.AI.Tools.Workbench.Base + import Piazza.Ecto.Schema + alias Console.AI.Tools.Workbench.Observability.Plrl.Logs + alias Console.Logs.{Query, Provider} + + embedded_schema do + field :user, :map, virtual: true + field :service_id, :string + field :cluster_id, :string + field :query, :string + field :field, :string + field :limit, :integer + + embeds_many :facets, Facet, on_replace: :delete, primary_key: false do + field :name, :string + field :value, :string + end + + embeds_one :time_range, TimeRange, on_replace: :update + end + + @valid ~w(service_id cluster_id query limit field)a + + def json_schema(_), do: Console.priv_file!("tools/workbench/observability/plrl_logs_labels.json") |> Jason.decode!() + def name(_), do: "plrl_logs_facets" + def description(_), do: "Gather log facets from Plural's built-in log aggregation integration. If you want to find the values for a specific facet, provide the `field` parameter. This requires either a Plural service_id or Plural cluster_id to be provided to authorize log extraction" + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> cast_embed(:time_range) + |> cast_embed(:facets, with: &facet_changeset/2) + |> validate_one_present([:service_id, :cluster_id]) + end + + defp facet_changeset(model, attrs) do + model + |> cast(attrs, [:name, :value]) + |> validate_required([:name, :value]) + end + + def implement(_, %__MODULE__{user: user, field: field} = logs) do + query = Logs.logs_query(logs) |> Map.put(:field, field) + with {:ok, query} <- Query.accessible(query, user), + {:ok, labels} <- Provider.labels(query) do + Jason.encode(labels) + end + end +end diff --git a/lib/console/ai/tools/workbench/observability/plrl_metrics.ex b/lib/console/ai/tools/workbench/observability/plrl_metrics.ex new file mode 100644 index 0000000000..a18f777ea6 --- /dev/null +++ b/lib/console/ai/tools/workbench/observability/plrl_metrics.ex @@ -0,0 +1,44 @@ +defmodule Console.AI.Tools.Workbench.Observability.Plrl.Metrics do + use Console.AI.Tools.Workbench.Base + alias Console.Deployments.Settings + alias Console.Schema.DeploymentSettings + alias Console.AI.Tools.Workbench.Observability.{TimeRange, Metrics} + alias Toolquery.{ToolConnection, PrometheusConnection} + + embedded_schema do + field :query, :string + field :step, :string + + embeds_one :time_range, TimeRange, on_replace: :update + end + + @valid ~w(query step)a + + def json_schema(), do: Console.priv_file!("tools/workbench/observability/metrics.json") |> Jason.decode!() + def name(), do: "plrl_metrics" + def description(), do: "Gather metrics from the (prometheus-compatible) Plural observability connection" + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> cast_embed(:time_range) + |> validate_required([:query]) + end + + def implement(%__MODULE__{query: q, step: s, time_range: tr}) do + with {:ok, conn} <- build_tool_connection() do + metrics = %Metrics{tool: conn, query: q, step: s, time_range: tr} + Metrics.implement(metrics, metrics) + end + end + + def build_tool_connection() do + case Settings.fetch() do + %DeploymentSettings{prometheus_connection: %{url: url, user: user, password: pass}} + when is_binary(url) and is_binary(user) and is_binary(pass) -> + {:ok, %ToolConnection{connection: {:prometheus, %PrometheusConnection{url: url, username: user, password: pass}}}} + _ -> + {:error, "No prometheus connection configured in Plural's DeploymentSettings"} + end + end +end diff --git a/lib/console/ai/tools/workbench/observability/plrl_metrics_search.ex b/lib/console/ai/tools/workbench/observability/plrl_metrics_search.ex new file mode 100644 index 0000000000..78fb11671f --- /dev/null +++ b/lib/console/ai/tools/workbench/observability/plrl_metrics_search.ex @@ -0,0 +1,29 @@ +defmodule Console.AI.Tools.Workbench.Observability.Plrl.MetricsSearch do + use Console.AI.Tools.Workbench.Base + import Console.AI.Tools.Workbench.Observability.Plrl.Metrics, only: [build_tool_connection: 0] + alias Console.AI.Tools.Workbench.Observability.MetricsSearch + + embedded_schema do + field :query, :string + field :limit, :integer + end + + @valid ~w(query limit)a + + def json_schema(), do: Console.priv_file!("tools/workbench/observability/metrics_search.json") |> Jason.decode!() + def name(), do: "plrl_metrics_search" + def description(), do: "Search for metric names in the (prometheus-compatible) Plural observability connection" + + def changeset(model, attrs) do + model + |> cast(attrs, @valid) + |> validate_required([:query]) + end + + def implement(%__MODULE__{query: q, limit: l}) do + with {:ok, conn} <- build_tool_connection() do + tool = %MetricsSearch{tool: conn, query: q, limit: l} + MetricsSearch.implement(tool, tool) + end + end +end diff --git a/lib/console/ai/tools/workbench/skills.ex b/lib/console/ai/tools/workbench/skills.ex index 785637d905..d53d758090 100644 --- a/lib/console/ai/tools/workbench/skills.ex +++ b/lib/console/ai/tools/workbench/skills.ex @@ -9,7 +9,7 @@ defmodule Console.AI.Tools.Workbench.Skills do def name(_), do: "workbench_skills" def json_schema(_), do: @json_schema - def description(_), do: "Get the skills available to you." + def description(_), do: "Get the skills available to you. This will only list skills and descriptions, you need to call the workbench_skill tool to get the full skill contents." def changeset(model, attrs) do model diff --git a/lib/console/ai/tools/workbench/subagents.ex b/lib/console/ai/tools/workbench/subagents.ex index 6d7a951ec2..3356fc3b41 100644 --- a/lib/console/ai/tools/workbench/subagents.ex +++ b/lib/console/ai/tools/workbench/subagents.ex @@ -23,7 +23,7 @@ defmodule Console.AI.Tools.Workbench.Subagents do |> Jason.encode() end - defp subagent_description(:coding, _), do: "Invoke a coding subagent to analyze or modify code, generating a pull request." + defp subagent_description(:coding, _), do: "Invoke a coding subagent to analyze or modify code, generating a pull request. This subagent will use either direct repository urls or container image urls to determine where to make code changes, so include either or both to guide it appropriately, alongside a clear spec of the change or analysis needed." defp subagent_description(:infrastructure, _), do: "Invoke an infrastructure subagent to determine infrastructure state and configuration." defp subagent_description(:observability, categories), do: "Invoke an observability subagent to query and analyze observability data. Supported tool capabilities are: #{observability_categories(categories)}" defp subagent_description(:integration, _), do: "Invoke an integration subagent to interact with enterprise systems, usually not directly related to devops infrastructure. Often Task tracking tools, knowledge bases or internal compliance software that's not SRE related." diff --git a/lib/console/ai/tools/workbench/summarize_component.ex b/lib/console/ai/tools/workbench/summarize_component.ex index f7807dace9..d4ea153fc1 100644 --- a/lib/console/ai/tools/workbench/summarize_component.ex +++ b/lib/console/ai/tools/workbench/summarize_component.ex @@ -23,7 +23,7 @@ defmodule Console.AI.Tools.Workbench.SummarizeComponent do def json_schema(), do: @json_schema def name(), do: "summarize_component" - def description(), do: "Deep introspects a given service component in kubernetes and answers any questions you might have about it. Use this as a kubernetes oracle fo this specific resource" + def description(), do: "Deep introspects a given service component in kubernetes and answers any questions you might have about it. Use this as a kubernetes oracle fo this specific resource, if you don't have the ability to use this, fall back to the k8s_get and k8s_list tools available to you" def implement(%__MODULE__{prompt: prompt, component_id: component_id}) do with {:actor, %User{} = user} <- {:actor, Tool.actor()}, diff --git a/lib/console/ai/workbench/engine.ex b/lib/console/ai/workbench/engine.ex index f426ca5586..cfdeea02ad 100644 --- a/lib/console/ai/workbench/engine.ex +++ b/lib/console/ai/workbench/engine.ex @@ -9,7 +9,7 @@ defmodule Console.AI.Workbench.Engine do message history to the memory engine to inform the next iteration of the loop. 3. A complete tool is used to mark the conclusion of the job. """ - import Console.AI.Workbench.Subagents.Base, only: [drop_empty: 1] + import Console.AI.Workbench.Subagents.Base, only: [drop_empty: 1, stream_callbacks: 1, log_error: 2] alias Console.Repo alias Console.AI.Chat.MemoryEngine alias Console.Deployments.Workbenches @@ -38,7 +38,7 @@ defmodule Console.AI.Workbench.Engine do def new(%WorkbenchJob{} = job) do %{user: user, workbench: workbench} = job = - Repo.preload(job, [:user, workbench: [:repository, :agent_runtime, [tools: :mcp_server]]]) + Repo.preload(job, [user: [:groups], workbench: [:workbench_skills, :repository, :agent_runtime, [tools: :mcp_server]]]) user = Console.Services.Rbac.preload(user) with {:ok, _} <- Heartbeat.start_link(job), @@ -55,6 +55,7 @@ defmodule Console.AI.Workbench.Engine do end def run(%__MODULE__{job: job} = engine) do + stream_callbacks(job) with {:ok, job} <- SA.Plan.run(job, engine.environment) do loop(%{engine | activities: list_activities(job)}) end @@ -71,7 +72,7 @@ defmodule Console.AI.Workbench.Engine do acc: %{}, tool_fmt: &tool_fmt/1 ) - |> MemoryEngine.reduce(Enum.reverse([{:user, continue_prompt(engine)} | messages]), &reducer/2) + |> MemoryEngine.reduce(Enum.reverse([{:user, continue_prompt(engine: engine)} | messages]), &reducer/2) |> case do {:ok, %Complete{conclusion: conclusion, metrics: metrics, logs: logs, todos: todos, topology: topology}} -> drop_empty(%{ @@ -123,8 +124,12 @@ defmodule Console.AI.Workbench.Engine do module = subagent_module(type) Console.AI.Tool.context(runtime: job.workbench.agent_runtime, user: job.user) with {:ok, activity} <- Workbenches.create_job_activity(%{type: type, prompt: prompt, tool_call: tool_attrs(call)}, job) do - module.run(activity, job, %{environment | activities: activities}) + stream_callbacks(activity) + Console.safely(fn -> + module.run(activity, job, %{environment | activities: activities}) + end, &crash_fallback/1) |> Workbenches.update_job_activity(activity) + |> log_error("Failed to update job activity") end end @@ -142,6 +147,10 @@ defmodule Console.AI.Workbench.Engine do defp spawn_activity(_, _), do: :ignore + defp crash_fallback(err) do + %{status: :failed, result: %{error: "error running subagent: #{inspect(err)}, feel free to try again if it is still necessary"}} + end + defp subagent_module(:infrastructure), do: SA.Infrastructure defp subagent_module(:integration), do: SA.Integration defp subagent_module(:coding), do: SA.Coding @@ -181,9 +190,7 @@ defmodule Console.AI.Workbench.Engine do defp maybe_add_memory(subagents, activities) when length(activities) > 5, do: [:memory | subagents] defp maybe_add_memory(subagents, _), do: subagents - defp continue_prompt(%__MODULE__{activities: [_, _ | _]}), do: "Ok, let's keep working" # the first activity is the plan - defp continue_prompt(_), do: "Ok, let's start working" - + EEx.function_from_file(:defp, :continue_prompt, Console.priv_filename(["prompts", "workbench", "continue.md.eex"]), [:assigns], trim: true) EEx.function_from_file(:defp, :notes_message, Console.priv_filename(["prompts", "workbench", "notes_message.md.eex"]), [:assigns], trim: true) EEx.function_from_file(:defp, :system_prompt, Console.priv_filename(["prompts", "workbench", "job.md.eex"]), [:assigns], trim: true) end diff --git a/lib/console/ai/workbench/environment.ex b/lib/console/ai/workbench/environment.ex index f3fd300852..1a30cf0925 100644 --- a/lib/console/ai/workbench/environment.ex +++ b/lib/console/ai/workbench/environment.ex @@ -1,8 +1,9 @@ defmodule Console.AI.Workbench.Environment do - alias Console.Schema.{WorkbenchJob, Workbench, WorkbenchTool, WorkbenchJobActivity} + alias Console.Schema.{WorkbenchJob, Workbench, WorkbenchTool, WorkbenchJobActivity, User} alias Console.AI.Workbench.Skill @type t :: %__MODULE__{ + user: User.t, job: WorkbenchJob.t, tools: %{binary => WorkbenchTool.t}, skills: %{binary => Skill.t}, @@ -11,10 +12,11 @@ defmodule Console.AI.Workbench.Environment do defguardp is_map_or_list(m) when is_map(m) or is_list(m) - defstruct [:job, :tools, :skills, :activities] + defstruct [:job, :tools, :skills, :activities, :user] def new(%WorkbenchJob{} = job, tools, skills) when is_map_or_list(tools) and is_map_or_list(skills) do %__MODULE__{ + user: job.user, job: job, tools: to_map(tools), skills: to_map(skills) diff --git a/lib/console/ai/workbench/heartbeat.ex b/lib/console/ai/workbench/heartbeat.ex index 72b9e82711..9fb01fb0a8 100644 --- a/lib/console/ai/workbench/heartbeat.ex +++ b/lib/console/ai/workbench/heartbeat.ex @@ -3,7 +3,7 @@ defmodule Console.AI.Workbench.Heartbeat do alias Console.Schema.WorkbenchJob alias Console.Deployments.Workbenches - @poll :timer.seconds(30) + @poll :timer.seconds(15) def start_link(%WorkbenchJob{} = job) do GenServer.start_link(__MODULE__, job) @@ -12,11 +12,14 @@ defmodule Console.AI.Workbench.Heartbeat do def init(job) do :timer.send_interval(@poll, :heartbeat) send self(), :heartbeat - {:ok, job} + {:ok, {job, true}} end - def handle_info(:heartbeat, job) do - Workbenches.heartbeat(job) - {:noreply, job} + def handle_info(:heartbeat, {job, booted}) do + case Workbenches.heartbeat(job, booted) do + {:ok, %WorkbenchJob{status: :cancelled}} -> {:stop, :normal, {job, false}} + {:ok, %WorkbenchJob{} = job} -> {:noreply, {job, false}} + _ -> {:noreply, {job, false}} + end end end diff --git a/lib/console/ai/workbench/skills.ex b/lib/console/ai/workbench/skills.ex index 0b03369fc5..b35daa8c75 100644 --- a/lib/console/ai/workbench/skills.ex +++ b/lib/console/ai/workbench/skills.ex @@ -3,12 +3,16 @@ defmodule Console.AI.Workbench.Skill do end defmodule Console.AI.Workbench.Skills do - alias Console.Schema.{Workbench, GitRepository} + alias Console.Schema.{Workbench, GitRepository, WorkbenchSkill} alias Console.Deployments.Git alias Console.AI.Workbench.Skill @spec skills(Workbench.t) :: {:ok, [Skill.t]} | Console.error - def skills(%Workbench{repository: %GitRepository{} = repository, skills: %Workbench.Skills{ref: ref, files: [_ | _] = files}}) do + def skills(%Workbench{ + repository: %GitRepository{} = repository, + skills: %Workbench.Skills{ref: ref, files: [_ | _] = files}, + workbench_skills: db_skills + }) do with {:ok, contents} <- Git.fetch(repository, ref) do Enum.filter(contents, fn {k, _} -> k in files end) |> Enum.reduce_while([], fn {file, skill}, acc -> @@ -18,12 +22,21 @@ defmodule Console.AI.Workbench.Skills do end end) |> case do - skills when is_list(skills) -> {:ok, skills} + skills when is_list(skills) -> + Enum.concat(skills, convert_db_skills(db_skills)) + |> then(&{:ok, &1}) {:error, _} = err -> err end end end - def skills(_), do: {:ok, []} + def skills(%Workbench{workbench_skills: db_skills}), do: {:ok, convert_db_skills(db_skills)} + + defp convert_db_skills(db_skills) when is_list(db_skills) do + Enum.map(db_skills, fn %WorkbenchSkill{name: name, description: description, contents: contents} -> + %Skill{name: name, description: description, contents: contents} + end) + end + defp convert_db_skills(_), do: [] @regex ~r/---(.*)---(.*)/s diff --git a/lib/console/ai/workbench/subagents/base.ex b/lib/console/ai/workbench/subagents/base.ex index b0d1992331..b3ef81a954 100644 --- a/lib/console/ai/workbench/subagents/base.ex +++ b/lib/console/ai/workbench/subagents/base.ex @@ -1,7 +1,9 @@ defmodule Console.AI.Workbench.Subagents.Base do import Console.AI.Agents.Base, only: [publish_absinthe: 2] alias Console.Repo - alias Console.Schema.{AgentRun, WorkbenchJobThought} + alias Console.AI.Stream + alias Console.Schema.{AgentRun, WorkbenchJobThought, WorkbenchJob, WorkbenchJobActivity} + require Logger defmacro __using__(_) do quote do @@ -21,6 +23,20 @@ defmodule Console.AI.Workbench.Subagents.Base do |> Map.new() end + def stream_callbacks(%WorkbenchJob{id: id}) do + Stream.stream_callbacks( + on_result: &callback(%{text: &1}, workbench_text_stream: "workbench_jobs:#{id}:text_stream"), + on_thinking: &callback(%{text: &1}, workbench_text_stream: "workbench_jobs:#{id}:text_stream") + ) + end + + def stream_callbacks(%WorkbenchJobActivity{workbench_job_id: jid, id: id}) do + Stream.stream_callbacks( + on_result: &callback(%{text: &1, activity_id: id}, workbench_text_stream: "workbench_jobs:#{jid}:text_stream"), + on_thinking: &callback(%{text: &1, activity_id: id}, workbench_text_stream: "workbench_jobs:#{jid}:text_stream") + ) + end + def callback(%{id: id, workbench_job_id: workbench_job_id}, {:content, content}) when is_binary(content), do: publish_absinthe(%{activity_id: id, text: content}, workbench_job_progress: "workbench_jobs:#{workbench_job_id}:progress") def callback(%{id: id, workbench_job_id: workbench_job_id}, {:tool, content, %{name: name, arguments: args} = tool}) @@ -68,6 +84,12 @@ defmodule Console.AI.Workbench.Subagents.Base do end def save_thought(_, _, _), do: :ok + def log_error({:error, error}, context) do + Logger.error("#{context}: #{inspect(error)}") + {:error, error} + end + def log_error(pass, _), do: pass + defp jitter_sleep() do time = :timer.seconds(5) :timer.sleep(time + Console.jitter(time)) diff --git a/lib/console/ai/workbench/subagents/coding.ex b/lib/console/ai/workbench/subagents/coding.ex index 7f852dc3a8..d6c5bcc819 100644 --- a/lib/console/ai/workbench/subagents/coding.ex +++ b/lib/console/ai/workbench/subagents/coding.ex @@ -1,7 +1,7 @@ defmodule Console.AI.Workbench.Subagents.Coding do use Console.AI.Workbench.Subagents.Base alias Console.Schema.{WorkbenchJob, WorkbenchJobActivity, AgentRun, PullRequest} - alias Console.AI.Tools.Workbench.{Skills, Skill, CodingAgent} + alias Console.AI.Tools.Workbench.{Skills, Skill, CodingAgent, Result} alias Console.Deployments.Workbenches alias Console.AI.Workbench.Environment @@ -13,46 +13,53 @@ defmodule Console.AI.Workbench.Subagents.Coding do |> MemoryEngine.reduce([{:user, prompt}], &reducer(activity, &1, &2)) |> case do {:ok, attrs} -> attrs - {:error, error} -> %{status: :failed, error: "error running infrastructure subagent: #{inspect(error)}"} + {:error, error} -> %{status: :failed, result: %{error: "error running infrastructure subagent: #{inspect(error)}"}} end end defp reducer(activity, messages, _) do - case Enum.find(messages, &match?(%AgentRun{}, &1)) do - %AgentRun{} = run -> {:halt, persist_and_poll_run(activity,run)} + case Enum.find(messages, &stop_msg/1) do + %AgentRun{} = run -> {:message, persist_and_poll_run(activity, run)} + %Result{output: output} -> {:halt, %{status: :successful, result: %{output: output}}} _ -> last_message(messages, & {:cont, %{status: :failed, result: %{error: &1}}}) end end + defp stop_msg(%AgentRun{}), do: true + defp stop_msg(%Result{}), do: true + defp stop_msg(_), do: false + defp persist_and_poll_run(activity, %AgentRun{id: id} = run) do - Workbenches.update_job_activity(%{ - status: :running, - agent_run_id: id - }, activity) + Workbenches.associate_agent_run(activity, id) case poll_run(run) do - {:timeout, _} -> %{status: :failed, error: "agent run #{id} timed out"} - {:failed, %AgentRun{error: error}} -> %{status: :failed, error: "Agent run failed: #{error}"} + {:timeout, _} -> {:user, "agent run #{id} timed out"} + {:failed, %AgentRun{error: error}} -> {:user, "Agent run failed: #{error}"} {:success, %AgentRun{mode: :write, pull_requests: [_ | _] = prs}} -> mark_prs(prs, activity) - %{status: :successful, agent_run_id: id, result: %{output: String.trim(analysis_prompt(analysis: nil, pull_requests: prs))}} + tool_msg(analysis_prompt(analysis: nil, pull_requests: prs), run) {:success, %AgentRun{mode: :analyze, analysis: %AgentRun.Analysis{} = analysis}} -> - %{status: :successful, agent_run_id: id, result: %{output: String.trim(analysis_prompt(pull_requests: nil, analysis: analysis))}} - {:success, _} -> %{status: :successful, agent_run_id: id, result: %{output: "Agent run completed successfully"}} + tool_msg(analysis_prompt(pull_requests: nil, analysis: analysis), run) + {:success, _} -> {:user, "Agent run completed successfully, but no output was generated"} end end + defp tool_msg(content, %AgentRun{tool: %Console.AI.Tool{id: id, name: name, arguments: args}}), + do: {:tool, content, %{call_id: id, name: name, arguments: args}} + defp tool_msg(content, _), do: {:user, content} + defp mark_prs(prs, %WorkbenchJobActivity{workbench_job_id: id}) do Enum.map(prs, & &1.id) |> PullRequest.for_ids() |> Repo.update_all(set: [workbench_job_id: id]) end - defp tools(%Environment{skills: skills}) do + defp tools(%Environment{skills: skills, job: job}) do [ - CodingAgent, + %CodingAgent{workbench: job.workbench}, %Skills{skills: skills}, %Skill{skills: skills}, + Result ] end diff --git a/lib/console/ai/workbench/subagents/infrastructure.ex b/lib/console/ai/workbench/subagents/infrastructure.ex index 9f6202dda1..3002652354 100644 --- a/lib/console/ai/workbench/subagents/infrastructure.ex +++ b/lib/console/ai/workbench/subagents/infrastructure.ex @@ -1,7 +1,22 @@ defmodule Console.AI.Workbench.Subagents.Infrastructure do use Console.AI.Workbench.Subagents.Base - alias Console.Schema.{WorkbenchJob, WorkbenchJobActivity, Workbench} - alias Console.AI.Tools.Workbench.{SummarizeComponent, Result, Skills, Skill} + alias Console.Schema.{WorkbenchJob, WorkbenchJobActivity, Workbench, User} + alias Console.AI.Tools.Workbench.{ + SummarizeComponent, + Result, + Skills, + Skill, + Infrastructure.KubeGet, + Infrastructure.KubeList, + Infrastructure.ServiceFiles, + Infrastructure.StackFiles, + Infrastructure.Cluster, + Infrastructure.ClusterList, + Infrastructure.ClusterServices, + Infrastructure.ServiceInspect, + Infrastructure.StackList, + Infrastructure.StackInspect + } alias Console.AI.Tools.Agent.{ServiceComponent, Stack} alias Console.AI.Workbench.Environment @@ -9,11 +24,11 @@ defmodule Console.AI.Workbench.Subagents.Infrastructure do def run(%WorkbenchJobActivity{prompt: prompt} = activity, %WorkbenchJob{prompt: jprompt} = job, %Environment{} = environment) do tools(job, environment) - |> MemoryEngine.new(20, system_prompt: system_prompt(prompt: jprompt), acc: %{}, callback: &callback(activity, &1)) + |> MemoryEngine.new(50, system_prompt: system_prompt(prompt: jprompt), acc: %{}, callback: &callback(activity, &1)) |> MemoryEngine.reduce([{:user, prompt}], &reducer/2) |> case do {:ok, attrs} -> attrs - {:error, error} -> %{status: :failed, error: "error running infrastructure subagent: #{inspect(error)}"} + {:error, error} -> %{status: :failed, result: %{error: "error running infrastructure subagent: #{inspect(error)}"}} end end @@ -27,10 +42,10 @@ defmodule Console.AI.Workbench.Subagents.Infrastructure do end end - defp tools(%WorkbenchJob{workbench: bench}, %Environment{skills: skills}) do - svc_tools(bench) - |> Enum.concat(stack_tools(bench)) - |> Enum.concat(k8s_tools(bench)) + defp tools(%WorkbenchJob{workbench: bench, user: user}, %Environment{skills: skills}) do + svc_tools(bench, user) + |> Enum.concat(stack_tools(bench, user)) + |> Enum.concat(k8s_tools(bench, user)) |> Enum.concat([ %Skills{skills: skills}, %Skill{skills: skills}, @@ -38,14 +53,36 @@ defmodule Console.AI.Workbench.Subagents.Infrastructure do ]) end - defp svc_tools(%Workbench{configuration: %{infrastructure: %{services: true}}}), do: [ServiceComponent] - defp svc_tools(_), do: [] + defp svc_tools(%Workbench{configuration: %{infrastructure: %{services: true}}}, user) do + [ + ServiceComponent, + ServiceFiles, + %ServiceInspect{user: user}, + %ClusterServices{user: user}, + %Cluster{user: user}, + %ClusterList{user: user} + ] + end + defp svc_tools(_, _), do: [] - defp stack_tools(%Workbench{configuration: %{infrastructure: %{stacks: true}}}), do: [Stack] - defp stack_tools(_), do: [] + defp stack_tools(%Workbench{configuration: %{infrastructure: %{stacks: true}}}, user) do + [ + Stack, + StackFiles, + %StackInspect{user: user}, + %StackList{user: user} + ] + end + defp stack_tools(_, _), do: [] - defp k8s_tools(%Workbench{configuration: %{infrastructure: %{kubernetes: true}}}), do: [SummarizeComponent] - defp k8s_tools(_), do: [] + defp k8s_tools(%Workbench{configuration: %{infrastructure: %{kubernetes: true}}}, %User{} = user) do + [ + SummarizeComponent, + %KubeGet{user: user}, + %KubeList{user: user} + ] + end + defp k8s_tools(_, _), do: [] EEx.function_from_file(:defp, :system_prompt, Console.priv_filename(["prompts", "workbench", "infrastructure.md.eex"]), [:assigns], trim: true) end diff --git a/lib/console/ai/workbench/subagents/integration.ex b/lib/console/ai/workbench/subagents/integration.ex index 9bba761247..39c2ce3781 100644 --- a/lib/console/ai/workbench/subagents/integration.ex +++ b/lib/console/ai/workbench/subagents/integration.ex @@ -12,7 +12,7 @@ defmodule Console.AI.Workbench.Subagents.Integration do |> MemoryEngine.reduce([{:user, prompt}], &reducer/2) |> case do {:ok, attrs} -> attrs - {:error, error} -> %{status: :failed, error: "error running infrastructure subagent: #{inspect(error)}"} + {:error, error} -> %{status: :failed, result: %{error: "error running infrastructure subagent: #{inspect(error)}"}} end end diff --git a/lib/console/ai/workbench/subagents/memory.ex b/lib/console/ai/workbench/subagents/memory.ex index 592aebd98a..4ba882b39f 100644 --- a/lib/console/ai/workbench/subagents/memory.ex +++ b/lib/console/ai/workbench/subagents/memory.ex @@ -12,7 +12,7 @@ defmodule Console.AI.Workbench.Subagents.Memory do |> MemoryEngine.reduce([{:user, prompt}], &reducer/2) |> case do {:ok, attrs} -> attrs - {:error, error} -> %{status: :failed, error: "error running memory subagent: #{inspect(error)}"} + {:error, error} -> %{status: :failed, result: %{error: "error running memory subagent: #{inspect(error)}"}} end end diff --git a/lib/console/ai/workbench/subagents/observability.ex b/lib/console/ai/workbench/subagents/observability.ex index 0f5c11ee77..4e40b002ee 100644 --- a/lib/console/ai/workbench/subagents/observability.ex +++ b/lib/console/ai/workbench/subagents/observability.ex @@ -1,19 +1,19 @@ defmodule Console.AI.Workbench.Subagents.Observability do use Console.AI.Workbench.Subagents.Base - alias Console.Schema.{WorkbenchJob, WorkbenchJobActivity, WorkbenchTool} + alias Console.Schema.{Workbench, WorkbenchJob, WorkbenchJobActivity, WorkbenchTool} alias Console.AI.Tools.Workbench.{ObservabilityResult, Skills, Skill} - alias Console.AI.Tools.Workbench.Observability.{Metrics, MetricsSearch, Logs, Traces, Time} + alias Console.AI.Tools.Workbench.Observability.{Metrics, MetricsSearch, Logs, Traces, Time, Plrl} alias Console.AI.Workbench.{Environment, MCP} require EEx def run(%WorkbenchJobActivity{prompt: prompt} = activity, %WorkbenchJob{prompt: jprompt}, %Environment{} = environment) do tools(environment) - |> MemoryEngine.new(20, system_prompt: system_prompt(prompt: jprompt), acc: %{}, callback: &callback(activity, &1)) + |> MemoryEngine.new(50, system_prompt: system_prompt(prompt: jprompt), acc: %{}, callback: &callback(activity, &1)) |> MemoryEngine.reduce([{:user, prompt}], &reducer/2) |> case do {:ok, attrs} -> attrs - {:error, error} -> %{status: :failed, error: "error running observability subagent: #{inspect(error)}"} + {:error, error} -> %{status: :failed, result: %{error: "error running observability subagent: #{inspect(error)}"}} end end @@ -30,6 +30,8 @@ defmodule Console.AI.Workbench.Subagents.Observability do defp tools(%Environment{skills: skills, tools: tools, job: job}) do obs_tools(tools) |> Enum.concat(MCP.expand_tools(Environment.subagent_tools(tools, :observability), job)) + |> Enum.concat(plrl_log_tools(job)) + |> Enum.concat(plrl_metric_tools(job)) |> Enum.concat([ %Skills{skills: skills}, %Skill{skills: skills}, @@ -38,12 +40,26 @@ defmodule Console.AI.Workbench.Subagents.Observability do ]) end + defp plrl_log_tools(%WorkbenchJob{user: user, workbench: %Workbench{configuration: %{observability: %{logs: true}}}}) do + [ + %Plrl.Logs{user: user}, + %Plrl.LogsAggregate{user: user}, + %Plrl.LogLabels{user: user} + ] + end + defp plrl_log_tools(_), do: [] + + defp plrl_metric_tools(%WorkbenchJob{workbench: %Workbench{configuration: %{observability: %{metrics: true}}}}), + do: [Plrl.Metrics, Plrl.MetricsSearch] + defp plrl_metric_tools(_), do: [] + @allowed_tools MapSet.new(~w(metrics logs traces)a) defp obs_tools(tools) do Enum.map(tools, &elem(&1, 1)) |> Enum.filter(fn - %WorkbenchTool{categories: [_ | _] = categories} -> MapSet.subset?(MapSet.new(categories), @allowed_tools) + %WorkbenchTool{tool: t, categories: [_ | _] = categories} when t != :mcp -> + MapSet.subset?(MapSet.new(categories), @allowed_tools) _ -> false end) |> Enum.flat_map(fn diff --git a/lib/console/ai/workbench/subagents/plan.ex b/lib/console/ai/workbench/subagents/plan.ex index 44f4b322d4..feaa000a09 100644 --- a/lib/console/ai/workbench/subagents/plan.ex +++ b/lib/console/ai/workbench/subagents/plan.ex @@ -15,7 +15,7 @@ defmodule Console.AI.Workbench.Subagents.Plan do |> MemoryEngine.reduce([{:user, prompt}], &reducer/2) |> case do {:ok, attrs} -> attrs - {:error, error} -> %{status: :failed, error: "error planning job: #{inspect(error)}"} + {:error, error} -> %{status: :failed, result: %{error: "error planning job: #{inspect(error)}"}} end |> then(&WorkbenchJob.changeset(job, &1)) |> Repo.update() diff --git a/lib/console/ai/workbench/supervisor.ex b/lib/console/ai/workbench/supervisor.ex index 140159910b..a96face831 100644 --- a/lib/console/ai/workbench/supervisor.ex +++ b/lib/console/ai/workbench/supervisor.ex @@ -1,7 +1,7 @@ defmodule Console.AI.Workbench.Supervisor do use Supervisor alias Console.AI.Workbench.{Environment, MCP} - alias Console.Schema.{WorkbenchJob, WorkbenchTool} + alias Console.Schema.{WorkbenchJob, WorkbenchTool, McpServer} alias Console.AI.MCP.Agent def start_link(%Environment{} = env) do @@ -17,11 +17,16 @@ defmodule Console.AI.Workbench.Supervisor do end def client_child(%WorkbenchTool{} = t, %WorkbenchJob{} = job) do - Console.AI.MCP.Client.child_spec([ + module = client_module(t) + + module.child_spec([ client_name: Agent.name(:client, t, job), transport_name: Agent.name(:transport, t, job), transport: MCP.transport(t, job) ]) |> Map.put(:restart, :transient) end + + def client_module(%WorkbenchTool{mcp_server: %McpServer{protocol: :sse}}), do: Console.AI.MCP.LegacyClient + def client_module(_), do: Console.AI.MCP.Client end diff --git a/lib/console/application.ex b/lib/console/application.ex index de5c9da7ae..7b9ff5d0c5 100644 --- a/lib/console/application.ex +++ b/lib/console/application.ex @@ -56,7 +56,6 @@ defmodule Console.Application do Console.Plural.Pinger, Console.AI.GothManager, Console.PromEx, - Console.AI.Graph.Indexer.Supervisor, Console.Chat.Supervisor, {GRPC.Server.Supervisor, endpoint: Console.GRPC.Endpoint, diff --git a/lib/console/deployments/events.ex b/lib/console/deployments/events.ex index 0f7bc80302..86bc2d3d87 100644 --- a/lib/console/deployments/events.ex +++ b/lib/console/deployments/events.ex @@ -127,6 +127,12 @@ defmodule Console.PubSub.WorkbenchToolDeleted, do: use Piazza.PubSub.Event defmodule Console.PubSub.WorkbenchCronCreated, do: use Piazza.PubSub.Event defmodule Console.PubSub.WorkbenchCronUpdated, do: use Piazza.PubSub.Event defmodule Console.PubSub.WorkbenchCronDeleted, do: use Piazza.PubSub.Event +defmodule Console.PubSub.WorkbenchPromptCreated, do: use Piazza.PubSub.Event +defmodule Console.PubSub.WorkbenchPromptUpdated, do: use Piazza.PubSub.Event +defmodule Console.PubSub.WorkbenchPromptDeleted, do: use Piazza.PubSub.Event +defmodule Console.PubSub.WorkbenchSkillCreated, do: use Piazza.PubSub.Event +defmodule Console.PubSub.WorkbenchSkillUpdated, do: use Piazza.PubSub.Event +defmodule Console.PubSub.WorkbenchSkillDeleted, do: use Piazza.PubSub.Event defmodule Console.PubSub.WorkbenchWebhookCreated, do: use Piazza.PubSub.Event defmodule Console.PubSub.WorkbenchWebhookUpdated, do: use Piazza.PubSub.Event defmodule Console.PubSub.WorkbenchWebhookDeleted, do: use Piazza.PubSub.Event diff --git a/lib/console/deployments/issues/webhook.ex b/lib/console/deployments/issues/webhook.ex index eeba5e1184..5a357774dd 100644 --- a/lib/console/deployments/issues/webhook.ex +++ b/lib/console/deployments/issues/webhook.ex @@ -8,6 +8,7 @@ defmodule Console.Deployments.Issues.Webhook do def payload(%IssueWebhook{provider: :linear} = webhook, %{"type" => "Issue", "data" => payload}) do build_attributes(Linear, payload) |> Map.put(:provider, :linear) + |> with_payload(payload) |> workbench_association(webhook) |> ok() end @@ -15,6 +16,7 @@ defmodule Console.Deployments.Issues.Webhook do def payload(%IssueWebhook{provider: :jira} = webhook, %{"issue" => _} = payload) do build_attributes(Jira, payload) |> Map.put(:provider, :jira) + |> with_payload(payload) |> workbench_association(webhook) |> ok() end @@ -22,6 +24,7 @@ defmodule Console.Deployments.Issues.Webhook do def payload(%IssueWebhook{provider: :asana} = webhook, %{"task" => _} = payload) do build_attributes(Asana, payload) |> Map.put(:provider, :asana) + |> with_payload(payload) |> workbench_association(webhook) |> ok() end @@ -29,6 +32,7 @@ defmodule Console.Deployments.Issues.Webhook do def payload(%IssueWebhook{provider: :asana} = webhook, %{"events" => [_ | _]} = payload) do build_attributes(Asana, payload) |> Map.put(:provider, :asana) + |> with_payload(payload) |> workbench_association(webhook) |> ok() end @@ -36,6 +40,7 @@ defmodule Console.Deployments.Issues.Webhook do def payload(%IssueWebhook{provider: :github} = webhook, %{"issue" => _} = payload) do build_attributes(Github, payload) |> Map.put(:provider, :github) + |> with_payload(payload) |> workbench_association(webhook) |> ok() end @@ -43,6 +48,7 @@ defmodule Console.Deployments.Issues.Webhook do def payload(%IssueWebhook{provider: :gitlab} = webhook, %{"object_attributes" => _, "object_kind" => "issue"} = payload) do build_attributes(Gitlab, payload) |> Map.put(:provider, :gitlab) + |> with_payload(payload) |> workbench_association(webhook) |> ok() end @@ -78,4 +84,7 @@ defmodule Console.Deployments.Issues.Webhook do end end) end + + defp with_payload(attrs, payload) when is_map(attrs) and is_map(payload), + do: Map.put(attrs, :payload, payload) end diff --git a/lib/console/deployments/observability/webhook.ex b/lib/console/deployments/observability/webhook.ex index 373db675fd..0fd7ea96d9 100644 --- a/lib/console/deployments/observability/webhook.ex +++ b/lib/console/deployments/observability/webhook.ex @@ -31,6 +31,7 @@ defmodule Console.Deployments.Observability.Webhook do tags: tags(alert["labels"]), }, add_associations(Grafana, alert)) |> workbench_association(hook) + |> with_payload(payload) |> backfill_raw() end) |> ok() @@ -51,6 +52,7 @@ defmodule Console.Deployments.Observability.Webhook do message: Pagerduty.summary(payload), tags: tags(data["custom_details"] || %{}), }, add_associations(Pagerduty, payload)) + |> with_payload(payload) |> backfill_raw() |> workbench_association(hook) |> listify() @@ -71,6 +73,7 @@ defmodule Console.Deployments.Observability.Webhook do message: Sentry.summary(payload), annotations: tags, }, add_associations(Sentry, tags)) + |> with_payload(payload) |> workbench_association(hook) |> listify() |> ok() @@ -92,6 +95,7 @@ defmodule Console.Deployments.Observability.Webhook do message: Datadog.summary(payload), tags: tags(Datadog.datadog_tag_map(payload)), }, add_associations(Datadog, payload)) + |> with_payload(payload) |> backfill_raw() |> workbench_association(hook) |> listify() @@ -110,6 +114,7 @@ defmodule Console.Deployments.Observability.Webhook do message: Newrelic.summary(payload), tags: %{}, }, add_associations(Newrelic, payload)) + |> with_payload(payload) |> workbench_association(hook) |> backfill_raw() |> listify() @@ -152,4 +157,7 @@ defmodule Console.Deployments.Observability.Webhook do defp listify(l) when is_list(l), do: l defp listify(v), do: [v] + + defp with_payload(data, payload) when is_map(data) and is_map(payload), + do: Map.put(data, :payload, payload) end diff --git a/lib/console/deployments/policies/policies.ex b/lib/console/deployments/policies/policies.ex index 00d4635db8..dd3aa8b4b2 100644 --- a/lib/console/deployments/policies/policies.ex +++ b/lib/console/deployments/policies/policies.ex @@ -21,7 +21,8 @@ defmodule Console.Deployments.Policies do BootstrapToken, AgentRuntime, AgentRun, - SentinelRunJob + SentinelRunJob, + WorkbenchJob } def can?(%User{bootstrap: %BootstrapToken{}} = user, res, action), do: BootstrapPolicies.can?(user, res, action) @@ -50,6 +51,8 @@ defmodule Console.Deployments.Policies do do: can?(user, Map.merge(resource, %{read_bindings: [], write_bindings: []}), :create) + def can?(%User{id: id}, %WorkbenchJob{user_id: id}, :edit), do: :pass + def can?(%User{} = user, %WorkbenchJob{} = job, :edit), do: can?(user, job, :write) def can?(%Cluster{id: id}, %SentinelRunJob{cluster_id: id}, _), do: :pass def can?(%Cluster{id: id}, %AgentRuntime{cluster_id: id}, _), do: :pass diff --git a/lib/console/deployments/policies/rbac.ex b/lib/console/deployments/policies/rbac.ex index 6b25e7283b..c070e5fe7c 100644 --- a/lib/console/deployments/policies/rbac.ex +++ b/lib/console/deployments/policies/rbac.ex @@ -51,6 +51,8 @@ defmodule Console.Deployments.Policies.Rbac do WorkbenchJob, WorkbenchTool, WorkbenchCron, + WorkbenchPrompt, + WorkbenchSkill, WorkbenchWebhook, IssueWebhook, Monitor @@ -141,6 +143,10 @@ defmodule Console.Deployments.Policies.Rbac do do: recurse(job, user, action, & &1.workbench) def evaluate(%WorkbenchCron{} = cron, user, action), do: recurse(cron, user, action, & &1.workbench) + def evaluate(%WorkbenchPrompt{} = prompt, user, action), + do: recurse(prompt, user, action, & &1.workbench) + def evaluate(%WorkbenchSkill{} = skill, user, action), + do: recurse(skill, user, action, & &1.workbench) def evaluate(%WorkbenchWebhook{} = webhook, user, action), do: recurse(webhook, user, action, & &1.workbench) def evaluate(%Monitor{} = monitor, %User{} = user, action), @@ -278,6 +284,10 @@ defmodule Console.Deployments.Policies.Rbac do do: Repo.preload(job, [workbench: [:read_bindings, :write_bindings, project: @bindings]]) def preload(%WorkbenchCron{} = cron), do: Repo.preload(cron, [workbench: [:read_bindings, :write_bindings, project: @bindings]]) + def preload(%WorkbenchPrompt{} = prompt), + do: Repo.preload(prompt, [workbench: [:read_bindings, :write_bindings, project: @bindings]]) + def preload(%WorkbenchSkill{} = skill), + do: Repo.preload(skill, [workbench: [:read_bindings, :write_bindings, project: @bindings]]) def preload(%WorkbenchWebhook{} = webhook), do: Repo.preload(webhook, [workbench: [:read_bindings, :write_bindings, project: @bindings]]) def preload(%Monitor{} = monitor), diff --git a/lib/console/deployments/pubsub/recurse.ex b/lib/console/deployments/pubsub/recurse.ex index d17e146536..780f590ea8 100644 --- a/lib/console/deployments/pubsub/recurse.ex +++ b/lib/console/deployments/pubsub/recurse.ex @@ -324,9 +324,10 @@ defimpl Console.PubSub.Recurse, for: [Console.PubSub.IssueCreated, Console.PubSu end defimpl Console.PubSub.Recurse, for: Console.PubSub.WorkbenchJobActivityCreated do - def process(%{item: %{type: :user}}), do: Console.Pipelines.AI.Workbench.Producer.kick() + def process(%{item: %{type: :user} = activity}), do: Console.Pipelines.AI.Workbench.Producer.kick(activity) + def process(_), do: :ok end defimpl Console.PubSub.Recurse, for: Console.PubSub.WorkbenchJobCreated do - def process(_), do: Console.Pipelines.AI.Workbench.Producer.kick() + def process(%{item: job}), do: Console.Pipelines.AI.Workbench.Producer.kick(job) end diff --git a/lib/console/deployments/stacks.ex b/lib/console/deployments/stacks.ex index 08fbf3f173..490baafce4 100644 --- a/lib/console/deployments/stacks.ex +++ b/lib/console/deployments/stacks.ex @@ -76,6 +76,23 @@ defmodule Console.Deployments.Stacks do |> Repo.one() end + @doc """ + The most recent failed `StackRun` for a stack (same selection as `Console.AI.Fixer.Stack`). + Preloads `errors` and `steps` with `logs` for each step. + """ + @spec last_failed_run(binary) :: StackRun.t() | nil + def last_failed_run(stack_id) do + StackRun.for_stack(stack_id) + |> StackRun.for_status(:failed) + |> StackRun.ordered(desc: :id) + |> StackRun.limit(1) + |> Repo.one() + |> case do + %StackRun{} = run -> Repo.preload(run, [:errors, steps: :logs]) + nil -> nil + end + end + def preloaded(%Stack{} = stack), do: Repo.preload(stack, @preloads) @spec authorized(binary, Cluster.t) :: run_resp diff --git a/lib/console/deployments/workbenches.ex b/lib/console/deployments/workbenches.ex index 3ed1dfa855..ac2a9c7fc7 100644 --- a/lib/console/deployments/workbenches.ex +++ b/lib/console/deployments/workbenches.ex @@ -10,7 +10,10 @@ defmodule Console.Deployments.Workbenches do WorkbenchJobActivity, WorkbenchJobResult, WorkbenchCron, + WorkbenchPrompt, + WorkbenchSkill, WorkbenchWebhook, + WorkbenchJobActivityAgentRun } alias Console.Deployments.Settings alias Console.PubSub @@ -21,6 +24,8 @@ defmodule Console.Deployments.Workbenches do @type job_resp :: {:ok, WorkbenchJob.t()} | error @type activity_resp :: {:ok, WorkbenchJobActivity.t()} | error @type cron_resp :: {:ok, WorkbenchCron.t()} | error + @type prompt_resp :: {:ok, WorkbenchPrompt.t()} | error + @type skill_resp :: {:ok, WorkbenchSkill.t()} | error @type webhook_resp :: {:ok, WorkbenchWebhook.t()} | error @cache_adapter Console.conf(:cache_adapter) @@ -42,6 +47,10 @@ defmodule Console.Deployments.Workbenches do def get_workbench_cron!(id), do: Repo.get!(WorkbenchCron, id) def get_workbench_cron(id), do: Repo.get(WorkbenchCron, id) + def get_workbench_prompt!(id), do: Repo.get!(WorkbenchPrompt, id) + def get_workbench_prompt(id), do: Repo.get(WorkbenchPrompt, id) + def get_workbench_skill!(id), do: Repo.get!(WorkbenchSkill, id) + def get_workbench_skill(id), do: Repo.get(WorkbenchSkill, id) def get_workbench_webhook!(id), do: Repo.get!(WorkbenchWebhook, id) def get_workbench_webhook(id), do: Repo.get(WorkbenchWebhook, id) @@ -124,8 +133,8 @@ defmodule Console.Deployments.Workbenches do Requires write permission on the workbench. """ @spec create_workbench_cron(map, binary, User.t()) :: cron_resp - def create_workbench_cron(attrs, workbench_id, %User{} = user) do - %WorkbenchCron{workbench_id: workbench_id} + def create_workbench_cron(attrs, workbench_id, %User{id: uid} = user) do + %WorkbenchCron{workbench_id: workbench_id, user_id: uid} |> WorkbenchCron.changeset(attrs) |> allow(user, :write) |> when_ok(:insert) @@ -138,8 +147,8 @@ defmodule Console.Deployments.Workbenches do @spec update_workbench_cron(map, binary, User.t()) :: cron_resp def update_workbench_cron(attrs, id, %User{} = user) do get_workbench_cron!(id) + |> WorkbenchCron.changeset(attrs) |> allow(user, :write) - |> when_ok(&WorkbenchCron.changeset(&1, attrs)) |> when_ok(:update) |> notify(:update, user) end @@ -155,6 +164,85 @@ defmodule Console.Deployments.Workbenches do |> notify(:delete, user) end + @doc """ + Fetches a workbench cron by id. Requires read permission on the workbench. + """ + @spec fetch_workbench_cron(binary, User.t()) :: cron_resp + def fetch_workbench_cron(id, %User{} = user) do + get_workbench_cron!(id) + |> allow(user, :read) + end + + @doc """ + Creates a saved prompt for a workbench. Requires read access to the workbench. + """ + @spec create_workbench_prompt(map, binary, User.t()) :: prompt_resp + def create_workbench_prompt(attrs, workbench_id, %User{} = user) do + %WorkbenchPrompt{workbench_id: workbench_id} + |> WorkbenchPrompt.changeset(attrs) + |> allow(user, :read) + |> when_ok(:insert) + |> notify(:create, user) + end + + @doc """ + Updates a saved workbench prompt. Requires read access to the workbench. + """ + @spec update_workbench_prompt(map, binary, User.t()) :: prompt_resp + def update_workbench_prompt(attrs, id, %User{} = user) do + get_workbench_prompt!(id) + |> WorkbenchPrompt.changeset(attrs) + |> allow(user, :read) + |> when_ok(:update) + |> notify(:update, user) + end + + @doc """ + Deletes a saved workbench prompt. Requires read access to the workbench. + """ + @spec delete_workbench_prompt(binary, User.t()) :: prompt_resp + def delete_workbench_prompt(id, %User{} = user) do + get_workbench_prompt!(id) + |> allow(user, :read) + |> when_ok(:delete) + |> notify(:delete, user) + end + + @doc """ + Creates a saved workbench skill. Requires write access to the workbench. + """ + @spec create_workbench_skill(map, binary, User.t()) :: skill_resp + def create_workbench_skill(attrs, workbench_id, %User{} = user) do + %WorkbenchSkill{workbench_id: workbench_id} + |> WorkbenchSkill.changeset(attrs) + |> allow(user, :write) + |> when_ok(:insert) + |> notify(:create, user) + end + + @doc """ + Updates a saved workbench skill. Requires write access to the workbench. + """ + @spec update_workbench_skill(map, binary, User.t()) :: skill_resp + def update_workbench_skill(attrs, id, %User{} = user) do + get_workbench_skill!(id) + |> WorkbenchSkill.changeset(attrs) + |> allow(user, :write) + |> when_ok(:update) + |> notify(:update, user) + end + + @doc """ + Deletes a saved workbench skill. Requires write access to the workbench. + """ + @spec delete_workbench_skill(binary, User.t()) :: skill_resp + def delete_workbench_skill(id, %User{} = user) do + get_workbench_skill!(id) + |> allow(user, :write) + |> when_ok(:delete) + |> notify(:delete, user) + end + @doc """ Lists all webhooks for a workbench, this view is cached. """ @@ -220,6 +308,35 @@ defmodule Console.Deployments.Workbenches do |> notify(:create, user) end + @doc """ + Updates a workbench job. Requires read access to the workbench. + """ + @spec update_workbench_job(map, WorkbenchJob.t() | binary, User.t()) :: job_resp + def update_workbench_job(attrs, %WorkbenchJob{} = job, %User{} = user) do + Repo.preload(job, :result) + |> WorkbenchJob.update_changeset(attrs) + |> allow(user, :edit) + |> when_ok(:update) + |> notify(:update, user) + end + def update_workbench_job(attrs, id, user) when is_binary(id) do + get_workbench_job!(id) + |> then(&update_workbench_job(attrs, &1, user)) + end + def update_workbench_job(_, _, _), do: {:error, "you can only update your own jobs"} + + @doc """ + Cancels a workbench job. Requires write access to the job, or for the user to be the owner of the job. + """ + @spec cancel_workbench_job(binary, User.t()) :: job_resp + def cancel_workbench_job(id, %User{} = user) do + get_workbench_job!(id) + |> WorkbenchJob.changeset(%{status: :cancelled}) + |> allow(user, :edit) + |> when_ok(:update) + |> notify(:update, user) + end + @doc """ Kicks a job by updating the updated_at timestamp to 20 minutes ago. """ @@ -237,12 +354,20 @@ defmodule Console.Deployments.Workbenches do def kick_job(_, _), do: {:error, "you can only kick your own jobs"} @doc """ - Heartbeats a job by updating the updated_at timestamp to the current time. + Heartbeats a job by setting status to running and updating the updated_at timestamp to the current time. """ - @spec heartbeat(WorkbenchJob.t()) :: job_resp - def heartbeat(%WorkbenchJob{} = job) do + @spec heartbeat(WorkbenchJob.t(), boolean) :: job_resp + def heartbeat(%WorkbenchJob{id: id}, boot \\ false) do + case {get_workbench_job!(id), boot} do + {job, true} -> mark_running(job) + {%WorkbenchJob{status: s} = job, _} when s in ~w(successful failed cancelled)a -> {:ok, job} + {job, _} -> mark_running(job) + end + end + + defp mark_running(%WorkbenchJob{} = job) do job - |> Ecto.Changeset.change(%{updated_at: Timex.now()}) + |> Ecto.Changeset.change(%{status: :running, updated_at: Timex.now()}) |> Repo.update(allow_stale: true) end @@ -250,9 +375,13 @@ defmodule Console.Deployments.Workbenches do Creates a new message for a job. Requires read access to the job. """ @spec create_message(map, binary, User.t()) :: activity_resp - def create_message(attrs, %WorkbenchJob{user_id: id} = job, %User{id: id} = user) do + def create_message(attrs, %WorkbenchJob{} = job, %User{} = user) do start_transaction() |> add_operation(:job, fn _ -> + allow(job, user, :edit) + |> error("you can only create messages for your own jobs") + end) + |> add_operation(:idle, fn _ -> case WorkbenchJob.idle?(job) do true -> {:ok, job} false -> {:error, "job is currently active, please wait for it to complete before prompting"} @@ -270,7 +399,6 @@ defmodule Console.Deployments.Workbenches do get_workbench_job!(id) |> then(&create_message(attrs, &1, user)) end - def create_message(_, _, _), do: {:error, "you can only create messages for your own jobs"} @doc """ Creates a new activity for a job, and bookkeeps job status and timestamp. @@ -292,18 +420,34 @@ defmodule Console.Deployments.Workbenches do end @doc """ - Updates an existing activity for a job, and bookkeeps job status and timestamp. + Updates an existing activity for a job. """ @spec update_job_activity(map, WorkbenchJobActivity.t()) :: activity_resp def update_job_activity(attrs, %WorkbenchJobActivity{} = activity) do - %{workbench_job: job} = Repo.preload(activity, :workbench_job) + activity + |> WorkbenchJobActivity.changeset(attrs) + |> Repo.update() + |> notify(:update) + end + + @doc """ + Associates an agent run with a workbench activity: inserts the join row (idempotent) and sets + `agent_run_id` and `status: :running` on the activity. + """ + @spec associate_agent_run(WorkbenchJobActivity.t(), binary) :: activity_resp + def associate_agent_run(%WorkbenchJobActivity{} = activity, run_id) when is_binary(run_id) do start_transaction() - |> add_operation(:activity, fn _ -> - WorkbenchJobActivity.changeset(activity, attrs) - |> Repo.update() + |> add_operation(:association, fn _ -> + %WorkbenchJobActivityAgentRun{} + |> WorkbenchJobActivityAgentRun.changeset(%{ + workbench_job_activity_id: activity.id, + agent_run_id: run_id + }) + |> Repo.insert(on_conflict: :nothing, conflict_target: [:workbench_job_activity_id, :agent_run_id]) end) - |> add_operation(:job, fn _ -> - Ecto.Changeset.change(job, %{status: :running, updated_at: DateTime.utc_now()}) + |> add_operation(:activity, fn _ -> + activity + |> WorkbenchJobActivity.changeset(%{status: :running, agent_run_id: run_id}) |> Repo.update() end) |> execute(extract: :activity) @@ -342,13 +486,25 @@ defmodule Console.Deployments.Workbenches do @spec complete_job(map, WorkbenchJob.t()) :: job_resp def complete_job(attrs, %WorkbenchJob{} = job) do - Repo.preload(job, :result) - |> WorkbenchJob.changeset(%{ - status: :successful, - completed_at: DateTime.utc_now(), - result: Console.mapify(attrs) - }) - |> Repo.update() + start_transaction() + |> add_operation(:activity, fn _ -> + create_job_activity(%{ + status: :successful, + type: :conclusion, + prompt: "completing job...", + result: %{output: attrs[:conclusion] || "no conclusion provided"} + }, job) + end) + |> add_operation(:job, fn _ -> + Repo.preload(job, :result) + |> WorkbenchJob.changeset(%{ + status: :successful, + completed_at: DateTime.utc_now(), + result: Console.mapify(attrs) + }) + |> Console.Repo.update() + end) + |> execute(extract: :job) |> notify(:update) end @@ -386,6 +542,18 @@ defmodule Console.Deployments.Workbenches do do: handle_notify(PubSub.WorkbenchCronUpdated, cron, actor: user) defp notify({:ok, %WorkbenchCron{} = cron}, :delete, user), do: handle_notify(PubSub.WorkbenchCronDeleted, cron, actor: user) + defp notify({:ok, %WorkbenchPrompt{} = prompt}, :create, user), + do: handle_notify(PubSub.WorkbenchPromptCreated, prompt, actor: user) + defp notify({:ok, %WorkbenchPrompt{} = prompt}, :update, user), + do: handle_notify(PubSub.WorkbenchPromptUpdated, prompt, actor: user) + defp notify({:ok, %WorkbenchPrompt{} = prompt}, :delete, user), + do: handle_notify(PubSub.WorkbenchPromptDeleted, prompt, actor: user) + defp notify({:ok, %WorkbenchSkill{} = skill}, :create, user), + do: handle_notify(PubSub.WorkbenchSkillCreated, skill, actor: user) + defp notify({:ok, %WorkbenchSkill{} = skill}, :update, user), + do: handle_notify(PubSub.WorkbenchSkillUpdated, skill, actor: user) + defp notify({:ok, %WorkbenchSkill{} = skill}, :delete, user), + do: handle_notify(PubSub.WorkbenchSkillDeleted, skill, actor: user) defp notify({:ok, %WorkbenchWebhook{} = webhook}, :create, user), do: handle_notify(PubSub.WorkbenchWebhookCreated, webhook, actor: user) defp notify({:ok, %WorkbenchWebhook{} = webhook}, :update, user), diff --git a/lib/console/external_graphql.ex b/lib/console/external_graphql.ex index c56d2f601f..658ad4dc5d 100644 --- a/lib/console/external_graphql.ex +++ b/lib/console/external_graphql.ex @@ -92,7 +92,7 @@ defmodule Console.ExternalGraphQl do end def plugins do - [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults() + [Absinthe.Middleware.Dataloader, Console.GraphQl.Introspection] ++ Absinthe.Plugin.defaults() end def middleware(middleware, _field, %{identifier: type}) when type in [:query, :mutation] do diff --git a/lib/console/graphql.ex b/lib/console/graphql.ex index c1f436d8e7..4185c7daf1 100644 --- a/lib/console/graphql.ex +++ b/lib/console/graphql.ex @@ -51,7 +51,7 @@ defmodule Console.GraphQl do end def plugins do - [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults() + [Absinthe.Middleware.Dataloader, Console.GraphQl.Introspection] ++ Absinthe.Plugin.defaults() end def middleware(middleware, _field, %{identifier: type}) when type in [:query, :mutation] do diff --git a/lib/console/graphql/deployments/integration.ex b/lib/console/graphql/deployments/integration.ex index f7a8a8b5b1..9437d0502e 100644 --- a/lib/console/graphql/deployments/integration.ex +++ b/lib/console/graphql/deployments/integration.ex @@ -90,6 +90,9 @@ defmodule Console.GraphQl.Deployments.Integration do field :body, non_null(:string), description: "the detailed description or body content of the issue" + field :payload, :map, + description: "raw webhook payload received for this issue" + field :flow, :flow, resolve: dataloader(Deployments), description: "the flow this issue is associated with" field :workbench, :workbench, resolve: dataloader(Deployments), description: "the workbench this issue is associated with" field :workbench_job, :workbench_job, resolve: dataloader(Deployments), description: "the workbench job this issue is associated with" diff --git a/lib/console/graphql/deployments/observability.ex b/lib/console/graphql/deployments/observability.ex index dd7fc98349..f95faf1f25 100644 --- a/lib/console/graphql/deployments/observability.ex +++ b/lib/console/graphql/deployments/observability.ex @@ -308,6 +308,9 @@ defmodule Console.GraphQl.Deployments.Observability do field :annotations, :map, description: "Arbitrary key/value annotations attached to this alert" + field :payload, :map, + description: "Raw webhook payload received for this alert" + field :url, :string, description: "Link back to the originating dashboard or alert view in the provider" diff --git a/lib/console/graphql/deployments/workbench.ex b/lib/console/graphql/deployments/workbench.ex index e1a1394c36..c76c7c4765 100644 --- a/lib/console/graphql/deployments/workbench.ex +++ b/lib/console/graphql/deployments/workbench.ex @@ -14,6 +14,15 @@ defmodule Console.GraphQl.Deployments.Workbench do field :prompt, :string, description: "the prompt for this job" end + input_object :workbench_job_update_attributes do + field :result, :workbench_result_attributes, description: "the result for this job" + end + + input_object :workbench_result_attributes do + field :topology, non_null(:string), + description: "mermaid diagram text for the job result topology (only field clients may set via this mutation)" + end + input_object :workbench_attributes do field :name, non_null(:string), description: "the name of the workbench (must be unique)" field :description, :string, description: "the description of the workbench" @@ -64,6 +73,16 @@ defmodule Console.GraphQl.Deployments.Workbench do field :prompt, :string, description: "the prompt to run when the cron triggers" end + input_object :workbench_prompt_attributes do + field :prompt, non_null(:string), description: "the saved prompt text" + end + + input_object :workbench_skill_attributes do + field :name, non_null(:string), description: "the saved skill name" + field :description, :string, description: "the saved skill description" + field :contents, non_null(:string), description: "the saved skill contents" + end + input_object :workbench_webhook_matches_attributes do field :regex, :string, description: "regex pattern to match in webhook body" field :substring, :string, description: "substring to match in webhook body" @@ -193,6 +212,14 @@ defmodule Console.GraphQl.Deployments.Workbench do resolve &Deployments.list_workbench_crons/3 end + connection field :prompts, node_type: :workbench_prompt do + resolve &Deployments.list_workbench_prompts/3 + end + + connection field :workbench_skills, node_type: :workbench_skill do + resolve &Deployments.list_workbench_skills/3 + end + connection field :webhooks, node_type: :workbench_webhook do resolve &Deployments.list_workbench_webhooks/3 end @@ -241,6 +268,7 @@ defmodule Console.GraphQl.Deployments.Workbench do field :workbench_job, :workbench_job, resolve: dataloader(Deployments), description: "the job this activity belongs to" field :agent_run, :agent_run, resolve: dataloader(Deployments), description: "the agent run that executed this activity" + field :agent_runs, list_of(:agent_run), resolve: dataloader(Deployments), description: "all agent runs associated with this activity (sideloadable)" timestamps() end @@ -352,6 +380,26 @@ defmodule Console.GraphQl.Deployments.Workbench do timestamps() end + object :workbench_prompt do + field :id, non_null(:string), description: "the id of the saved prompt" + field :prompt, :string, description: "the saved prompt text" + + field :workbench, :workbench, resolve: dataloader(Deployments), description: "the workbench this prompt belongs to" + + timestamps() + end + + object :workbench_skill do + field :id, non_null(:string), description: "the id of the saved skill" + field :name, :string, description: "the saved skill name" + field :description, :string, description: "the saved skill description" + field :contents, :string, description: "the saved skill contents" + + field :workbench, :workbench, resolve: dataloader(Deployments), description: "the workbench this skill belongs to" + + timestamps() + end + object :workbench_webhook_matches do field :regex, :string, description: "regex pattern to match" field :substring, :string, description: "substring to match" @@ -458,12 +506,19 @@ defmodule Console.GraphQl.Deployments.Workbench do field :arguments, :map end + object :workbench_text_stream do + field :activity_id, :id + field :text, :string + end + connection node_type: :workbench connection node_type: :workbench_tool connection node_type: :workbench_job connection node_type: :workbench_job_activity connection node_type: :workbench_job_thought connection node_type: :workbench_cron + connection node_type: :workbench_prompt + connection node_type: :workbench_skill connection node_type: :workbench_webhook delta :workbench_job @@ -541,6 +596,7 @@ defmodule Console.GraphQl.Deployments.Workbench do resolve &Deployments.all_workbench_issues/2 end + end object :workbench_mutations do @@ -638,6 +694,87 @@ defmodule Console.GraphQl.Deployments.Workbench do resolve &Deployments.delete_workbench_cron/2 end + @desc "Fetches a workbench cron by id. Requires read access to the workbench." + field :workbench_cron, :workbench_cron do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :read + arg :id, non_null(:id) + + resolve &Deployments.workbench_cron/2 + end + + @desc "Creates a saved prompt for a workbench. Requires read access to the workbench." + field :create_workbench_prompt, :workbench_prompt do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :write + arg :workbench_id, non_null(:id), description: "the workbench to save a prompt for" + arg :attributes, non_null(:workbench_prompt_attributes) + + resolve &Deployments.create_workbench_prompt/2 + end + + @desc "Updates a saved workbench prompt. Requires read access to the workbench." + field :update_workbench_prompt, :workbench_prompt do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :write + arg :id, non_null(:id) + arg :attributes, non_null(:workbench_prompt_attributes) + + resolve &Deployments.update_workbench_prompt/2 + end + + @desc "Deletes a saved workbench prompt. Requires read access to the workbench." + field :delete_workbench_prompt, :workbench_prompt do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :write + arg :id, non_null(:id) + + resolve &Deployments.delete_workbench_prompt/2 + end + + @desc "Creates a saved skill for a workbench. Requires write access to the workbench." + field :create_workbench_skill, :workbench_skill do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :write + arg :workbench_id, non_null(:id), description: "the workbench to save a skill for" + arg :attributes, non_null(:workbench_skill_attributes) + + resolve &Deployments.create_workbench_skill/2 + end + + @desc "Updates a saved workbench skill. Requires write access to the workbench." + field :update_workbench_skill, :workbench_skill do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :write + arg :id, non_null(:id) + arg :attributes, non_null(:workbench_skill_attributes) + + resolve &Deployments.update_workbench_skill/2 + end + + @desc "Deletes a saved workbench skill. Requires write access to the workbench." + field :delete_workbench_skill, :workbench_skill do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :write + arg :id, non_null(:id) + + resolve &Deployments.delete_workbench_skill/2 + end + field :create_workbench_webhook, :workbench_webhook do middleware Authenticated middleware Scope, @@ -649,6 +786,17 @@ defmodule Console.GraphQl.Deployments.Workbench do resolve &Deployments.create_workbench_webhook/2 end + @desc "Fetches a workbench webhook by id. Requires read access to the workbench." + field :get_workbench_webhook, :workbench_webhook do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :read + arg :id, non_null(:id) + + resolve &Deployments.get_workbench_webhook/2 + end + field :update_workbench_webhook, :workbench_webhook do middleware Authenticated middleware Scope, @@ -675,7 +823,7 @@ defmodule Console.GraphQl.Deployments.Workbench do middleware Authenticated middleware Scope, resource: :workbench, - action: :read + action: :write arg :workbench_id, non_null(:id), description: "the workbench to create a job for" arg :attributes, non_null(:workbench_job_attributes), description: "job attributes (e.g. prompt)" @@ -686,12 +834,36 @@ defmodule Console.GraphQl.Deployments.Workbench do middleware Authenticated middleware Scope, resource: :workbench, - action: :read + action: :write arg :job_id, non_null(:id), description: "the job to create a message for" arg :attributes, non_null(:workbench_message_attributes), description: "message attributes (e.g. prompt)" resolve &Deployments.create_workbench_message/2 end + + @desc "Updates only the topology field on the job's result. Requires read access to the job's workbench; only the job owner may update." + field :update_workbench_job, :workbench_job do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :write + arg :job_id, non_null(:id), description: "the workbench job to update" + arg :attributes, non_null(:workbench_job_update_attributes), + description: "attributes to update on the job (only the result topology is accepted)" + + resolve &Deployments.update_workbench_job/2 + end + + @desc "Cancels a workbench job. Allowed for the job owner or users with write access to the workbench." + field :cancel_workbench_job, :workbench_job do + middleware Authenticated + middleware Scope, + resource: :workbench, + action: :write + arg :job_id, non_null(:id), description: "the workbench job to cancel" + + resolve &Deployments.cancel_workbench_job/2 + end end object :workbench_subscriptions do @@ -721,5 +893,14 @@ defmodule Console.GraphQl.Deployments.Workbench do do: {:ok, topic: "workbench_jobs:#{job_id}:progress"} end end + + field :workbench_text_stream, :workbench_text_stream do + arg :job_id, non_null(:id) + + config fn %{job_id: job_id}, ctx -> + with {:ok, _} <- Deployments.workbench_job(%{id: job_id}, ctx), + do: {:ok, topic: "workbench_jobs:#{job_id}:text_stream"} + end + end end end diff --git a/lib/console/graphql/introspection.ex b/lib/console/graphql/introspection.ex new file mode 100644 index 0000000000..f11e7439ac --- /dev/null +++ b/lib/console/graphql/introspection.ex @@ -0,0 +1,25 @@ +defmodule Console.GraphQl.Introspection do + @moduledoc """ + Disable or restrict schema introspection to authorized requests + """ + @behaviour Absinthe.Plugin + + @prod Mix.env() != :test + + @impl Absinthe.Plugin + def before_resolution(%{context: %{current_user: %Console.Schema.User{}}} = exec), do: exec + def before_resolution(%{result: %{emitter: %{selections: [_ | _] = selections}}} = exec) do + (@prod && Enum.any?(selections, fn %{name: field_name} -> Macro.underscore(field_name) in ["__schema", "__type"] end)) + |> case do + true -> %{exec | validation_errors: [%Absinthe.Phase.Error{message: "Unauthorized", phase: __MODULE__}]} + false -> exec + end + end + def before_resolution(exec), do: exec + + @impl Absinthe.Plugin + def after_resolution(exec), do: exec + + @impl Absinthe.Plugin + def pipeline(pipeline, _exec), do: pipeline +end diff --git a/lib/console/graphql/resolvers/deployments.ex b/lib/console/graphql/resolvers/deployments.ex index bd7fba3aa6..b1e38e1cd4 100644 --- a/lib/console/graphql/resolvers/deployments.ex +++ b/lib/console/graphql/resolvers/deployments.ex @@ -114,6 +114,8 @@ defmodule Console.GraphQl.Resolvers.Deployments do WorkbenchJobResult, WorkbenchTool, WorkbenchCron, + WorkbenchPrompt, + WorkbenchSkill, WorkbenchWebhook, ObservabilityWebhook, IssueWebhook, @@ -221,6 +223,8 @@ defmodule Console.GraphQl.Resolvers.Deployments do def query(WorkbenchJobResult, _), do: WorkbenchJobResult.ordered() def query(WorkbenchTool, _), do: WorkbenchTool def query(WorkbenchCron, _), do: WorkbenchCron.ordered() + def query(WorkbenchPrompt, _), do: WorkbenchPrompt.ordered() + def query(WorkbenchSkill, _), do: WorkbenchSkill.ordered() def query(WorkbenchWebhook, _), do: WorkbenchWebhook def query(ObservabilityWebhook, _), do: ObservabilityWebhook.ordered() def query(IssueWebhook, _), do: IssueWebhook.ordered() diff --git a/lib/console/graphql/resolvers/deployments/workbench.ex b/lib/console/graphql/resolvers/deployments/workbench.ex index ab16937fa0..9048f73fa1 100644 --- a/lib/console/graphql/resolvers/deployments/workbench.ex +++ b/lib/console/graphql/resolvers/deployments/workbench.ex @@ -9,6 +9,8 @@ defmodule Console.GraphQl.Resolvers.Deployments.Workbench do WorkbenchJobActivity, WorkbenchTool, WorkbenchCron, + WorkbenchPrompt, + WorkbenchSkill, WorkbenchWebhook } @@ -49,6 +51,18 @@ defmodule Console.GraphQl.Resolvers.Deployments.Workbench do |> paginate(args) end + def list_workbench_prompts(workbench, args, _) do + WorkbenchPrompt.for_workbench(workbench.id) + |> WorkbenchPrompt.ordered() + |> paginate(args) + end + + def list_workbench_skills(workbench, args, _) do + WorkbenchSkill.for_workbench(workbench.id) + |> WorkbenchSkill.ordered() + |> paginate(args) + end + def list_workbench_webhooks(workbench, args, _) do WorkbenchWebhook.for_workbench(workbench.id) |> WorkbenchWebhook.ordered() @@ -119,9 +133,35 @@ defmodule Console.GraphQl.Resolvers.Deployments.Workbench do def delete_workbench_cron(%{id: id}, %{context: %{current_user: user}}), do: Workbenches.delete_workbench_cron(id, user) + def workbench_cron(%{id: id}, %{context: %{current_user: user}}), + do: Workbenches.fetch_workbench_cron(id, user) + + def create_workbench_prompt(%{workbench_id: workbench_id, attributes: attrs}, %{context: %{current_user: user}}), + do: Workbenches.create_workbench_prompt(attrs, workbench_id, user) + + def update_workbench_prompt(%{id: id, attributes: attrs}, %{context: %{current_user: user}}), + do: Workbenches.update_workbench_prompt(attrs, id, user) + + def delete_workbench_prompt(%{id: id}, %{context: %{current_user: user}}), + do: Workbenches.delete_workbench_prompt(id, user) + + def create_workbench_skill(%{workbench_id: workbench_id, attributes: attrs}, %{context: %{current_user: user}}), + do: Workbenches.create_workbench_skill(attrs, workbench_id, user) + + def update_workbench_skill(%{id: id, attributes: attrs}, %{context: %{current_user: user}}), + do: Workbenches.update_workbench_skill(attrs, id, user) + + def delete_workbench_skill(%{id: id}, %{context: %{current_user: user}}), + do: Workbenches.delete_workbench_skill(id, user) + def create_workbench_webhook(%{workbench_id: workbench_id, attributes: attrs}, %{context: %{current_user: user}}), do: Workbenches.create_workbench_webhook(attrs, workbench_id, user) + def get_workbench_webhook(%{id: id}, %{context: %{current_user: user}}) do + Workbenches.get_workbench_webhook!(id) + |> allow(user, :read) + end + def update_workbench_webhook(%{id: id, attributes: attrs}, %{context: %{current_user: user}}), do: Workbenches.update_workbench_webhook(attrs, id, user) @@ -131,6 +171,12 @@ defmodule Console.GraphQl.Resolvers.Deployments.Workbench do def create_workbench_message(%{job_id: job_id, attributes: attrs}, %{context: %{current_user: user}}), do: Workbenches.create_message(attrs, job_id, user) + def update_workbench_job(%{job_id: id, attributes: attributes}, %{context: %{current_user: user}}), + do: Workbenches.update_workbench_job(attributes, id, user) + + def cancel_workbench_job(%{job_id: id}, %{context: %{current_user: user}}), + do: Workbenches.cancel_workbench_job(id, user) + defp workbench_filters(query, args) do Enum.reduce(args, query, fn {:project_id, project_id}, q when is_binary(project_id) -> Workbench.for_project(q, project_id) diff --git a/lib/console/logs/aggregation_bucket.ex b/lib/console/logs/aggregation_bucket.ex index 5f3eb39d88..81372096cd 100644 --- a/lib/console/logs/aggregation_bucket.ex +++ b/lib/console/logs/aggregation_bucket.ex @@ -1,6 +1,7 @@ defmodule Console.Logs.AggregationBucket do - defstruct [:timestamp, :count] + @derive Jason.Encoder @type t :: %__MODULE__{timestamp: DateTime.t(), count: non_neg_integer()} + defstruct [:timestamp, :count] end diff --git a/lib/console/logs/line.ex b/lib/console/logs/line.ex index 97e15a7541..18343a115a 100644 --- a/lib/console/logs/line.ex +++ b/lib/console/logs/line.ex @@ -1,6 +1,7 @@ defmodule Console.Logs.Line do @type t :: %__MODULE__{facets: [%{key: binary, value: binary}]} + @derive Jason.Encoder defstruct [:timestamp, :log, :facets] def new(map) do diff --git a/lib/console/pipelines/ai/workbench_cron/pipeline.ex b/lib/console/pipelines/ai/workbench_cron/pipeline.ex index 81701fa7a5..e5ce42dcfb 100644 --- a/lib/console/pipelines/ai/workbench_cron/pipeline.ex +++ b/lib/console/pipelines/ai/workbench_cron/pipeline.ex @@ -1,22 +1,19 @@ defmodule Console.Pipelines.AI.WorkbenchCron.Pipeline do use Console.Pipelines.Consumer alias Console.Repo - alias Console.Schema.{WorkbenchCron, Workbench} + alias Console.Schema.{WorkbenchCron, Workbench, User} alias Console.Deployments.Workbenches - alias Console.Services.Users require Logger def handle_event(%WorkbenchCron{prompt: p} = cron) do mark_last_run(cron) - case Repo.preload(cron, [:workbench]) do - %WorkbenchCron{workbench: %Workbench{} = workbench} -> - Workbenches.create_workbench_job(%{prompt: p}, workbench.id, bot()) + case Repo.preload(cron, [:workbench, user: [:groups]]) do + %WorkbenchCron{workbench: %Workbench{} = workbench, user: %User{} = user} -> + Workbenches.create_workbench_job(%{prompt: p}, workbench.id, user) _ -> :ok end end - defp bot(), do: %{Users.get_bot!("console") | roles: %{admin: true}} - defp mark_last_run(%WorkbenchCron{} = cron) do cron |> WorkbenchCron.changeset(%{last_run_at: DateTime.utc_now()}) diff --git a/lib/console/pipelines/poll_producer.ex b/lib/console/pipelines/poll_producer.ex index 82a6928d6e..e568ff5d2a 100644 --- a/lib/console/pipelines/poll_producer.ex +++ b/lib/console/pipelines/poll_producer.ex @@ -38,7 +38,13 @@ defmodule Console.Pipelines.PollProducer do {:producer, %State{}} end - def kick(), do: GenStage.cast(self(), :kick) + def kick(%{id: id}) do + me = node() + case worker_node(id) do + ^me -> GenStage.cast(__MODULE__, :kick) + node -> GenStage.cast({__MODULE__, node}, :kick) + end + end def handle_cast(:kick, state), do: handle_info(:poll, state) @@ -81,7 +87,7 @@ defmodule Console.Pipelines.PollProducer do {:noreply, events, %{state | buffer: buffer, demand: demand - length(events)}} end - defp worker_node(id), do: Console.ClusterRing.node(id) + def worker_node(id), do: Console.ClusterRing.node(id) defp local?(%{id: id}), do: worker_node(id) == node() end diff --git a/lib/console/schema/agent_run.ex b/lib/console/schema/agent_run.ex index 45c8551ac8..2f3d91db4e 100644 --- a/lib/console/schema/agent_run.ex +++ b/lib/console/schema/agent_run.ex @@ -28,6 +28,8 @@ defmodule Console.Schema.AgentRun do field :branch, :string field :error, :binary + field :tool, :map, virtual: true + embeds_one :pod_reference, NamespacedName, on_replace: :update embeds_many :todos, Todo, on_replace: :delete do diff --git a/lib/console/schema/alert.ex b/lib/console/schema/alert.ex index 81f277a7b2..1d37446c2f 100644 --- a/lib/console/schema/alert.ex +++ b/lib/console/schema/alert.ex @@ -29,6 +29,7 @@ defmodule Console.Schema.Alert do field :message, :string field :fingerprint, :string field :annotations, :map + field :payload, :map field :url, :string field :ai_poll_at, :utc_datetime_usec @@ -151,6 +152,7 @@ defmodule Console.Schema.Alert do message fingerprint annotations + payload url project_id flow_id diff --git a/lib/console/schema/git_repository.ex b/lib/console/schema/git_repository.ex index 5669874773..7de7b65b59 100644 --- a/lib/console/schema/git_repository.ex +++ b/lib/console/schema/git_repository.ex @@ -56,7 +56,7 @@ defmodule Console.Schema.GitRepository do |> foreign_key_constraint(:id, name: :stacks, match: :prefix, message: "there is an active stack using this repository") |> foreign_key_constraint(:id, name: :deployment_settings, match: :prefix, message: "This git repository is currently used in your global deployment settings") |> foreign_key_constraint(:connection_id) - |> validate_format(:url, ~r/((git|ssh|http(s)?)|(git@[\w\.-]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)?(\/)?/, message: "must provide a valid git url") + |> validate_format(:url, ~r/((git|ssh|http(s)?)|([a-z\-\_]+@[\w\.-]+))(:(\/\/)?)([\w\.@\:\/\-~]+)(\.git)?(\/)?/i, message: "must provide a valid git url") |> add_auth_method() |> validate_required([:url]) |> add_https_path() diff --git a/lib/console/schema/issue.ex b/lib/console/schema/issue.ex index 24ef70c21a..fc478c4784 100644 --- a/lib/console/schema/issue.ex +++ b/lib/console/schema/issue.ex @@ -12,6 +12,7 @@ defmodule Console.Schema.Issue do field :url, :string field :title, :string field :body, :string + field :payload, :map belongs_to :workbench, Workbench belongs_to :flow, Flow @@ -47,7 +48,7 @@ defmodule Console.Schema.Issue do from(i in query, where: i.status == ^status) end - @valid ~w(provider status external_id url title body workbench_id flow_id)a + @valid ~w(provider status external_id url title body payload workbench_id flow_id)a def changeset(model, attrs \\ %{}) do model diff --git a/lib/console/schema/workbench.ex b/lib/console/schema/workbench.ex index 2a1c97f3bc..a40af4f86a 100644 --- a/lib/console/schema/workbench.ex +++ b/lib/console/schema/workbench.ex @@ -9,6 +9,8 @@ defmodule Console.Schema.Workbench do WorkbenchToolAssociation, WorkbenchWebhook, WorkbenchCron, + WorkbenchPrompt, + WorkbenchSkill, PolicyBinding, User, AgentRun, @@ -62,10 +64,12 @@ defmodule Console.Schema.Workbench do has_many :tool_associations, WorkbenchToolAssociation, on_replace: :delete has_many :tools, through: [:tool_associations, :tool] - has_many :jobs, WorkbenchJob, on_replace: :delete - has_many :webhooks, WorkbenchWebhook, on_replace: :delete - has_many :crons, WorkbenchCron, on_replace: :delete - has_many :alerts, Alert + has_many :jobs, WorkbenchJob, on_replace: :delete + has_many :webhooks, WorkbenchWebhook, on_replace: :delete + has_many :crons, WorkbenchCron, on_replace: :delete + has_many :prompts, WorkbenchPrompt, on_replace: :delete + has_many :workbench_skills, WorkbenchSkill, on_replace: :delete + has_many :alerts, Alert timestamps() end diff --git a/lib/console/schema/workbench_cron.ex b/lib/console/schema/workbench_cron.ex index 07df1a158e..a22c4d149b 100644 --- a/lib/console/schema/workbench_cron.ex +++ b/lib/console/schema/workbench_cron.ex @@ -1,6 +1,6 @@ defmodule Console.Schema.WorkbenchCron do use Console.Schema.Base - alias Console.Schema.Workbench + alias Console.Schema.{Workbench, User} schema "workbench_crons" do field :crontab, :string @@ -10,6 +10,7 @@ defmodule Console.Schema.WorkbenchCron do field :last_run_at, :utc_datetime_usec belongs_to :workbench, Workbench + belongs_to :user, User timestamps() end @@ -26,17 +27,18 @@ defmodule Console.Schema.WorkbenchCron do from(c in query, where: c.workbench_id == ^workbench_id) end - def preloaded(query \\ __MODULE__, preloads \\ [:workbench]) do + def preloaded(query \\ __MODULE__, preloads \\ [:workbench, :user]) do from(c in query, preload: ^preloads) end - @valid ~w(crontab prompt last_run_at workbench_id)a + @valid ~w(crontab prompt last_run_at workbench_id user_id)a def changeset(model, attrs \\ %{}) do model |> cast(attrs, @valid) |> add_next_run() |> foreign_key_constraint(:workbench_id) + |> foreign_key_constraint(:user_id) |> validate_required([:crontab, :next_run_at]) end diff --git a/lib/console/schema/workbench_job.ex b/lib/console/schema/workbench_job.ex index cde5603e09..c89b878fe2 100644 --- a/lib/console/schema/workbench_job.ex +++ b/lib/console/schema/workbench_job.ex @@ -82,4 +82,10 @@ defmodule Console.Schema.WorkbenchJob do |> foreign_key_constraint(:issue_id) |> validate_required([:status, :workbench_id, :user_id]) end + + def update_changeset(model, attrs \\ %{}) do + model + |> cast(attrs, []) + |> cast_assoc(:result) + end end diff --git a/lib/console/schema/workbench_job_activity.ex b/lib/console/schema/workbench_job_activity.ex index 9f64634fb6..16f5dd4cef 100644 --- a/lib/console/schema/workbench_job_activity.ex +++ b/lib/console/schema/workbench_job_activity.ex @@ -1,9 +1,9 @@ defmodule Console.Schema.WorkbenchJobActivity do use Console.Schema.Base - alias Console.Schema.{WorkbenchJob, WorkbenchJobThought, AgentRun, WorkbenchJobResult} + alias Console.Schema.{WorkbenchJob, WorkbenchJobThought, AgentRun, WorkbenchJobResult, WorkbenchJobActivityAgentRun} defenum Status, pending: 0, running: 1, successful: 2, failed: 3, cancelled: 4 - defenum Type, coding: 0, observability: 1, integration: 2, ticketing: 3, infrastructure: 4, memo: 5, plan: 6, user: 7, memory: 8 + defenum Type, coding: 0, observability: 1, integration: 2, ticketing: 3, infrastructure: 4, memo: 5, plan: 6, user: 7, memory: 8, conclusion: 9 schema "workbench_job_activities" do field :status, Status, default: :pending @@ -46,6 +46,11 @@ defmodule Console.Schema.WorkbenchJobActivity do belongs_to :workbench_job, WorkbenchJob belongs_to :agent_run, AgentRun + has_many :agent_run_associations, WorkbenchJobActivityAgentRun, + on_replace: :delete, + foreign_key: :workbench_job_activity_id + has_many :agent_runs, through: [:agent_run_associations, :agent_run] + has_many :thoughts, WorkbenchJobThought, on_replace: :delete, foreign_key: :activity_id diff --git a/lib/console/schema/workbench_job_activity_agent_run.ex b/lib/console/schema/workbench_job_activity_agent_run.ex new file mode 100644 index 0000000000..c64cdd7fb8 --- /dev/null +++ b/lib/console/schema/workbench_job_activity_agent_run.ex @@ -0,0 +1,30 @@ +defmodule Console.Schema.WorkbenchJobActivityAgentRun do + use Console.Schema.Base + alias Console.Schema.{WorkbenchJobActivity, AgentRun} + + schema "workbench_job_activity_agent_runs" do + belongs_to :workbench_job_activity, WorkbenchJobActivity + belongs_to :agent_run, AgentRun + + timestamps() + end + + def for_activity(query \\ __MODULE__, activity_id) do + from(a in query, where: a.workbench_job_activity_id == ^activity_id) + end + + def for_agent_run(query \\ __MODULE__, agent_run_id) do + from(a in query, where: a.agent_run_id == ^agent_run_id) + end + + @valid ~w(workbench_job_activity_id agent_run_id)a + + def changeset(model, attrs \\ %{}) do + model + |> cast(attrs, @valid) + |> foreign_key_constraint(:workbench_job_activity_id) + |> foreign_key_constraint(:agent_run_id) + |> unique_constraint([:workbench_job_activity_id, :agent_run_id]) + |> validate_required([:workbench_job_activity_id, :agent_run_id]) + end +end diff --git a/lib/console/schema/workbench_prompt.ex b/lib/console/schema/workbench_prompt.ex new file mode 100644 index 0000000000..489feacf12 --- /dev/null +++ b/lib/console/schema/workbench_prompt.ex @@ -0,0 +1,29 @@ +defmodule Console.Schema.WorkbenchPrompt do + use Console.Schema.Base + alias Console.Schema.Workbench + + schema "workbench_prompts" do + field :prompt, :binary + + belongs_to :workbench, Workbench + + timestamps() + end + + def ordered(query \\ __MODULE__, order \\ [asc: :inserted_at]) do + from(p in query, order_by: ^order) + end + + def for_workbench(query \\ __MODULE__, workbench_id) do + from(p in query, where: p.workbench_id == ^workbench_id) + end + + @valid ~w(prompt workbench_id)a + + def changeset(model, attrs \\ %{}) do + model + |> cast(attrs, @valid) + |> foreign_key_constraint(:workbench_id) + |> validate_required([:prompt]) + end +end diff --git a/lib/console/schema/workbench_skill.ex b/lib/console/schema/workbench_skill.ex new file mode 100644 index 0000000000..2a6e08ab42 --- /dev/null +++ b/lib/console/schema/workbench_skill.ex @@ -0,0 +1,32 @@ +defmodule Console.Schema.WorkbenchSkill do + use Console.Schema.Base + alias Console.Schema.Workbench + + schema "workbench_skills" do + field :name, :string + field :description, :string + field :contents, :binary + + belongs_to :workbench, Workbench + + timestamps() + end + + def ordered(query \\ __MODULE__, order \\ [asc: :inserted_at]) do + from(s in query, order_by: ^order) + end + + def for_workbench(query \\ __MODULE__, workbench_id) do + from(s in query, where: s.workbench_id == ^workbench_id) + end + + @valid ~w(name description contents workbench_id)a + + def changeset(model, attrs \\ %{}) do + model + |> cast(attrs, @valid) + |> foreign_key_constraint(:workbench_id) + |> unique_constraint([:workbench_id, :name]) + |> validate_required([:name, :contents]) + end +end diff --git a/lib/console/services/base.ex b/lib/console/services/base.ex index a706bb01d8..92c45c2c25 100644 --- a/lib/console/services/base.ex +++ b/lib/console/services/base.ex @@ -22,6 +22,9 @@ defmodule Console.Services.Base do end) end + def error({:error, _}, msg) when is_binary(msg), do: {:error, msg} + def error(pass, _), do: pass + def ok(val), do: {:ok, val} def bang!({:ok, val}), do: val diff --git a/lib/console_web/controllers/webhook_controller.ex b/lib/console_web/controllers/webhook_controller.ex index 9f3d519a22..cdde3c953f 100644 --- a/lib/console_web/controllers/webhook_controller.ex +++ b/lib/console_web/controllers/webhook_controller.ex @@ -71,6 +71,13 @@ defmodule ConsoleWeb.WebhookController do end end + defp verify(conn, %ScmWebhook{type: :azure_devops, hmac: hmac}) do + case Plug.BasicAuth.parse_basic_auth(conn) do + {_, ^hmac} -> :ok + _ -> :reject + end + end + defp verify(conn, %ObservabilityWebhook{type: :grafana, secret: secret}) do with {_, password} <- Plug.BasicAuth.parse_basic_auth(conn), true <- Plug.Crypto.secure_compare(secret, password) do diff --git a/lib/kube/client/base.ex b/lib/kube/client/base.ex index 847a8bc208..4861b92b13 100644 --- a/lib/kube/client/base.ex +++ b/lib/kube/client/base.ex @@ -17,6 +17,11 @@ defmodule Kube.Client.Base do |> Kube.Utils.run() end + def path(nil, v, k, nil), do: "/api/#{v}/#{k}" + def path(nil, v, k, ns), do: "/api/#{v}/namespaces/#{ns}/#{k}" + def path(g, v, k, nil), do: path_builder(g, v, k) + def path(g, v, k, ns), do: path_builder(g, v, k, ns) + def path(nil, v, k, nil, name), do: "/api/#{v}/#{k}/#{name}" def path(nil, v, k, ns, name), do: "/api/#{v}/namespaces/#{ns}/#{k}/#{name}" def path(g, v, k, nil, name), do: "#{path_builder(g, v, k)}/#{name}" diff --git a/priv/prompts/workbench/alert.md.eex b/priv/prompts/workbench/alert.md.eex index 281ab21470..f00d06bc7d 100644 --- a/priv/prompts/workbench/alert.md.eex +++ b/priv/prompts/workbench/alert.md.eex @@ -18,3 +18,13 @@ There's also an associated stacktrace, which I'll provide in JSON format: <%= Jason.encode!(Console.mapify(@alert.stacktrace)) %> ``` <% end %> + +<%= if @alert.payload do %> +# Payload + +For reference, this is the webhook result from the original <%= @alert.type %> webhook: + +```json +<%= Jason.encode!(@alert.payload) %> +``` +<% end %> diff --git a/priv/prompts/workbench/coding.md.eex b/priv/prompts/workbench/coding.md.eex index 28779f2f0c..630d1a2a06 100644 --- a/priv/prompts/workbench/coding.md.eex +++ b/priv/prompts/workbench/coding.md.eex @@ -5,9 +5,38 @@ The agent has two main modes: 1. Analyze: This mode is used to gather information about a codebase. You'll be given a prompt and can use any of the tools available to gather needed intelligence. 2. Write: This mode is used to make a code change. You'll be given a prompt and can use any of the tools available to make a code change. +# Coding Agent Usage Guidance + +* You can create multiple runs of the coding agent, so if you want to run an analyze before a write (because you're not confident yet that a change is needed), that is supported. +* Coding agent runs are expensive, so you shouldn't just cycle through them without a good reason. If you've done both an analyze and a write, you should call the `subagent_result` tool to wrap up the task. +* You should always call the `subagent_result` tool, otherwise the task will considered to have terminated in error. + In addition, you have access to a bag of skills that can help you know how to make the right prompt to send to the coding agent. Be sure to check them before you delegate work. They will train you on what repository to use for the given task, alongside other useful guidance. +## Tone of Voice Gudiance + +You are producing output for a human user, and should expect them to want to read as little as possible and mostly be scanning. You should be: + +* as concise as possible, the user will not want to read much and be annoyed by verbosity. +* still provide as much critical information as needed to conveigh the result of your work +* use supporting markdown formatting where needed to improve readability in a way that supports scanning. + +## Coding Guidance + +Some rules you should communicate to the coding agent to help it make high quality code changes: + +1. For Helm/K8s changes, bias towards modifying existing helm values files, inline configuration against CRs or whatever might be present in the repo. Engineers are going to want to minimize change, and you shouldn't bring a sledge hammer when a scalpel is fine. +2. For Terraform changes, you'll likely need to navigate between both modifying terraform code in-line but also looking to the InfrastructureStack custom resource that defines how terraform is instantiated, including setting variables for a stack. +3. If you're working within the same repo and want to make multiple changes (even if disconnected), still try to make them a single PR to minimize the number of PRs users need to review. + +## Tool Usage Guidance + +* You *should* leverage the workbench skill tools to determine the appropriate repository. Often users will provide the needed repo in one of them, and include mappings from container image names to application code repositories there. +* *DO NOT* proceeed to calling a `coding_agent` before consulting the relevant skills to ensure you have the correct repository. +* Don't prematurely select a repository without consulting skills if the guidance in your prompt is ambiguous. +* There are not many tasks to perform besides selecting the appropriate coding agent to run, so you can be liberal in consulting skills to ensure you call it in the correct manner. + The overarching task is as follows: <%= @prompt %> diff --git a/priv/prompts/workbench/continue.md.eex b/priv/prompts/workbench/continue.md.eex new file mode 100644 index 0000000000..a300072455 --- /dev/null +++ b/priv/prompts/workbench/continue.md.eex @@ -0,0 +1,5 @@ +<%= if length(@engine.activities) > 1 do %> +Ok we've made some progress, determine if you've done everything you need to complete the task, or keep working until its finished! +<% else %> +Ok, let's start working! +<% end %> diff --git a/priv/prompts/workbench/infrastructure.md.eex b/priv/prompts/workbench/infrastructure.md.eex index da28264065..b0e2323d65 100644 --- a/priv/prompts/workbench/infrastructure.md.eex +++ b/priv/prompts/workbench/infrastructure.md.eex @@ -18,4 +18,26 @@ It can also be useful to include a mermaid diagram of the topology of the infras If you've identified any Plural Service or Plural Stack related to this infrastructure being investigated, be sure to explicitly call them out by name in your conclusion. This will be useful in future iterations, especially for providing `Plural Stack: {stack-name}` and `Plural Service: {cluster-handle}/{service-name}` tags to the PR body when creating a pull request. +## Useful Heuristics + +Plural establishes a few conventions you can use to provide answers to the user invoking this subagent, in particular: + +* Kubernetes resources can be annotated with a `deployments.plural.sh/repository-url` annotation, indicating that the images for that resource are built from application code in that repository. + + **IMPORTANT**: Once you've completed all the work you plan to do, always call the `subagent_result` tool to complete the subagent session. + +## Tone of Voice Gudiance + +You are producing output for a human user, and should expect them to want to read as little as possible and mostly be scanning. You should be: + +* as concise as possible, the user will not want to read much and be annoyed by verbosity. +* still provide as much critical information as needed to conveigh the result of your work +* use supporting markdown formatting where needed to improve readability in a way that supports scanning. + + +## Tool Guidance + +1. If you don't have enough specifics to use a precise tool like a direct kubernetes get or list, you should use the `service_search` or `stack_search` tools to gather more information. These support semantic search and work well with fuzzy inputs. +2. In general summarize_component has the ability to dive deep into a k8s object, but you'll need the Plural-specific context from a service_search or stack_search to gather them. +3. We can offer the ability to directly query kubernetes, but you need to be precise with your inputs and *always* include a Plural cluster handle to make it work. It's also possible user RBAC policies block your query. diff --git a/priv/prompts/workbench/infrastructure/cluster.md.eex b/priv/prompts/workbench/infrastructure/cluster.md.eex new file mode 100644 index 0000000000..012f7b5265 --- /dev/null +++ b/priv/prompts/workbench/infrastructure/cluster.md.eex @@ -0,0 +1,15 @@ +Here is some of the useful information about the following cluster: + +ID: <%= @cluster.id %> +Name: <%= @cluster.name %> +Handle: <%= @cluster.handle %> +Kubernetes distribution: <%= @cluster.distro %> +Metadata: <%= Jason.encode!(@cluster.metadata || %{}) %> +Tags: <%= Enum.map(@cluster.tags, & %{name: &1.name, value: &1.value}) |> Jason.encode!() %> +Project: <%= @cluster.project.name %> + +# Upgrade plan + +```json +<%= Console.mapify(@upgrade_plan) |> Jason.encode!(pretty: true) %> +``` diff --git a/priv/prompts/workbench/infrastructure/service.md.eex b/priv/prompts/workbench/infrastructure/service.md.eex new file mode 100644 index 0000000000..8c1187e024 --- /dev/null +++ b/priv/prompts/workbench/infrastructure/service.md.eex @@ -0,0 +1,67 @@ +Here are the details for the Plural service **<%= @service.name %>** (namespace: `<%= @service.namespace %>`): + +# Identity + +```json +<%= Jason.encode!(%{ + id: @service.id, + name: @service.name, + namespace: @service.namespace, + status: @service.status, + protect: @service.protect, + errors: Enum.map(@service.errors, & %{message: &1.message}) +}, pretty: true) %> +``` + +# Cluster + +```json +<%= Jason.encode!(%{ + id: @service.cluster.id, + handle: @service.cluster.handle, + name: @service.cluster.name +}, pretty: true) %> +``` + +<%= if @service.repository do %> +# Repository + +```json +<%= Jason.encode!(%{url: @service.repository.url}, pretty: true) %> +``` +<% end %> + +<%= if @service.git && @service.git.ref do %> +## Git Location + +```json +<%= Jason.encode!(Map.from_struct(@service.git) |> Map.take([:ref, :folder, :files]), pretty: true) %> +``` +<% end %> + +<%= if @service.helm do %> +## Helm + +```json +<%= Console.drop_nils(%{ + chart: @service.helm.chart, + version: @service.helm.version, + release: @service.helm.release, + values_files: @service.helm.values_files, + values: @service.helm.values, + url: @service.helm.url +}) |> Jason.encode!(pretty: true) %> +``` +<% end %> + +<%= if @service.kustomize do %> +## Kustomize Configuration + +```json +<%= Jason.encode!(%{ + path: @service.kustomize.path, + enable_helm: @service.kustomize.enable_helm, + envsubst: @service.kustomize.envsubst +}, pretty: true) %> +``` +<% end %> diff --git a/priv/prompts/workbench/infrastructure/stack.md.eex b/priv/prompts/workbench/infrastructure/stack.md.eex new file mode 100644 index 0000000000..b0b1d641d2 --- /dev/null +++ b/priv/prompts/workbench/infrastructure/stack.md.eex @@ -0,0 +1,70 @@ +Here's the GitOps structure of the following stack + +# Attributes + +```json +<%= Map.take(@stack, [:id, :name, :type, :approval, :workdir]) |> Jason.encode!(pretty: true) %> +``` + +# Repository + +This is where the <%= @stack.type %> code lives. + +```json +<%= Jason.encode!(%{ + url: @stack.repository.url, + ref: @stack.git.ref, + folder: @stack.git.folder +}, pretty: true) %> +``` +<%= if @stack.workdir do %> +Working directory: <%= @stack.workdir %> +<% end %> + +<%= if @stack.parent do %> +# Parent + +The stack is managed as an InfrastructureStack kubernetes custom resource in the following Plural Service: + +(this is often useful for understanding how terraform variables or other configuration is managed, since they'll be added at the CR level here) + +```json +<%= Jason.encode!(%{ + id: @stack.parent.id, + name: @stack.parent.name, + cluster: %{ + name: @stack.parent.cluster.name, + handle: @stack.parent.cluster.handle + } +}, pretty: true) %> +``` + +You can always query details about the parent service to investigate further. +<% end %> + +<%= if @failed_run do %> +# Latest failed run + +The stack status is **failed**. This section summarizes the most recent **failed** stack run including log output + +Run id: `<%= @failed_run.run_id %>` +Failed command: `<%= @failed_run.failing_step.cmd %> <%= Enum.join(@failed_run.failing_step.args, " ") %>` +<%= if !Enum.empty?(@failed_run.run_errors) do %> +## Run-level errors +<%= for error <- @failed_run.run_errors do %> +* <%= error.message %> +<% end %> +<% end %> + +<%= if !Enum.empty?(@failed_run.failing_step.logs) do %> +## Logs (failing step) + +``` +<%= @failed_run.failing_step.logs + |> Enum.map(& &1.logs) + |> Enum.reject(&is_nil/1) + |> Enum.join("\n") + |> Console.truncate(48_000) %> +``` +<% end %> +<% end %> diff --git a/priv/prompts/workbench/integration.md.eex b/priv/prompts/workbench/integration.md.eex index 4e6dd2fd17..0e72db0827 100644 --- a/priv/prompts/workbench/integration.md.eex +++ b/priv/prompts/workbench/integration.md.eex @@ -8,3 +8,11 @@ The overarching task is as follows: <%= @prompt %> Once you've completed all the work you plan to do, always call the `subagent_result` tool to complete the subagent session. + +## Tone of Voice Gudiance + +You are producing output for a human user, and should expect them to want to read as little as possible and mostly be scanning. You should be: + +* as concise as possible, the user will not want to read much and be annoyed by verbosity. +* still provide as much critical information as needed to conveigh the result of your work +* use supporting markdown formatting where needed to improve readability in a way that supports scanning. diff --git a/priv/prompts/workbench/issue.md.eex b/priv/prompts/workbench/issue.md.eex index 063c42a7a4..3cb93643a3 100644 --- a/priv/prompts/workbench/issue.md.eex +++ b/priv/prompts/workbench/issue.md.eex @@ -3,3 +3,13 @@ You've been assigned the following issue from <%= @issue.provider %>, please inv # <%= @issue.title %> <%= @issue.body %> + +<%= if @issue.payload do %> +# Full Webhook Payload + +For reference, this is the webhook result from the original <%= @issue.provider %> webhook: + +```json +<%= Jason.encode!(@issue.payload) %> +``` +<% end %> diff --git a/priv/prompts/workbench/job.md.eex b/priv/prompts/workbench/job.md.eex index 4cd66a1477..285f78e0ce 100644 --- a/priv/prompts/workbench/job.md.eex +++ b/priv/prompts/workbench/job.md.eex @@ -29,6 +29,14 @@ Once you've completed all the work you believe is necessary, call the `workbench As work is completed, be sure to call the `workbench_notes` tool to record progress and update your working theory for future reference and transparency to users. +## Tone of Voice Guidance + +You are producing output for a human user, and should expect them to want to read as little as possible and mostly be scanning. You should be: + +* as concise as possible, the user will not want to read much and be annoyed by verbosity. +* still provide as much critical information as needed to conveigh the result of your work +* use supporting markdown formatting where needed to improve readability in a way that supports scanning. + ## Final Result As work is being done, you can use the `working_theory` field to record the current best guess at the solution to your task. This is useful for tracking work for users, and as a memo to track your work for future investigation. @@ -60,9 +68,13 @@ Two agents can give useful information about the codebase defining the system: 1. `coding` is effective for introspecting application code, and modifying it. It can also be used to modify GitOps/IaC, but you likely need additional information since those codebases live separately from application code in many organizations. -2. `infrastructure` can both live introspect infra state in kubernetes and IaC defined IaaS services. But it can also be used to gather the gitops structure that defines that +2. You should not call the `coding` subagent to modify GitOps/IaC/etc without first determining what repo and location in git is appropriate. This can be done in one of two ways: + a. Consult the `infrastructure` agent which has the ability to traverse GitOps configuration for you. Ask it explicitly for this information + b. Leverage skills, which users will provide and help with mapping these where Plural's native intelligence has gaps. +3. Any code change made should prefer fixing the root cause of the issue. If it's an application issue that could be turned off with a GitOps/IaC change, you should prefer fixing the application issue (or generate prs to do both). +4. `infrastructure` can both live introspect infra state in kubernetes and IaC defined IaaS services. But it can also be used to gather the gitops structure that defines that infra state. If you think a gitops change is necessary, use it to gather that intel, then feed the result into the coding agent to make whatever modification is necessary. -3. If you are making a GitOps change, **it's important to follow the PR body tagging guidelines within Plural**, in particular: +5. If you are making a GitOps change, **it's important to follow the PR body tagging guidelines within Plural**, in particular: * identify the owning Plural Service or Plural Stack (a service is related to k8s, stack related to IaC like terraform) * Be sure to add `Plural Stack: {stack-name}` to the PR body if the change is related to a stack. diff --git a/priv/prompts/workbench/memory.md.eex b/priv/prompts/workbench/memory.md.eex index a67a9d6c61..5ea8b7f794 100644 --- a/priv/prompts/workbench/memory.md.eex +++ b/priv/prompts/workbench/memory.md.eex @@ -10,3 +10,11 @@ You should avoid being unnecessarily verbose in your response, since we want to In addition, the overarching task this investigation is related to is as follows: <%= @query %> + +## Tone of Voice Gudiance + +You are producing output for a human user, and should expect them to want to read as little as possible and mostly be scanning. You should be: + +* as concise as possible, the user will not want to read much and be annoyed by verbosity. +* still provide as much critical information as needed to conveigh the result of your work +* use supporting markdown formatting where needed to improve readability in a way that supports scanning. diff --git a/priv/prompts/workbench/observability.md.eex b/priv/prompts/workbench/observability.md.eex index 0a4c0139a7..8e583c09e6 100644 --- a/priv/prompts/workbench/observability.md.eex +++ b/priv/prompts/workbench/observability.md.eex @@ -16,7 +16,16 @@ in the skills. To determine the authoritative current time, use the `current_time` tool. Don't guess timestamps when making observability queries, either base it off hard data or use queries relative to the current time. -**IDEAL OUTPUT** +## Tone of Voice Gudiance + +You are producing output for a human user, and should expect them to want to read as little as possible and mostly be scanning. You should be: + +* as concise as possible, the user will not want to read much and be annoyed by verbosity. +* still provide as much critical information as needed to conveigh the result of your work +* use supporting markdown formatting where needed to improve readability in a way that supports scanning. + +## Ideal output + You should always return a markdown formatted conclusion based on the investigation. That said, it's also extremely important to include either: * the time series data that proves that conclusion diff --git a/priv/prompts/workbench/plan.md b/priv/prompts/workbench/plan.md index 58bbe0bcdf..8e7847a5cb 100644 --- a/priv/prompts/workbench/plan.md +++ b/priv/prompts/workbench/plan.md @@ -10,4 +10,12 @@ the task. You'll be given the following: 3. an observability agent, to probe observability systems and analyze the outputs 4. an integration tooling agent, to interact with business tools via apis and known interfaces. Useful for things like reporting or interacting with internal systems if present. -Once you've gathered enough intel, come up with an initial implementation plan, it can be revised later as well. \ No newline at end of file +Once you've gathered enough intel, come up with an initial implementation plan, it can be revised later as well. + +## Tone of Voice Guidance + +You are producing output for a human user, and should expect them to want to read as little as possible and mostly be scanning. You should be: + +* as concise as possible, the user will not want to read much and be annoyed by verbosity. +* still provide as much critical information as needed to conveigh the result of your work +* use supporting markdown formatting where needed to improve readability in a way that supports scanning. \ No newline at end of file diff --git a/priv/repo/migrations/20260405194233_add_workbench_prompts.exs b/priv/repo/migrations/20260405194233_add_workbench_prompts.exs new file mode 100644 index 0000000000..e7ed471068 --- /dev/null +++ b/priv/repo/migrations/20260405194233_add_workbench_prompts.exs @@ -0,0 +1,21 @@ +defmodule Console.Repo.Migrations.AddWorkbenchPrompts do + use Ecto.Migration + + def change do + alter table(:workbench_crons) do + add :user_id, references(:watchman_users, type: :uuid, on_delete: :delete_all) + end + + create index(:workbench_crons, [:user_id]) + + create table(:workbench_prompts, primary_key: false) do + add :id, :uuid, primary_key: true + add :workbench_id, references(:workbenches, type: :uuid, on_delete: :delete_all) + add :prompt, :binary + + timestamps() + end + + create index(:workbench_prompts, [:workbench_id]) + end +end diff --git a/priv/repo/migrations/20260406151621_add_workbench_skills.exs b/priv/repo/migrations/20260406151621_add_workbench_skills.exs new file mode 100644 index 0000000000..b0a476adf6 --- /dev/null +++ b/priv/repo/migrations/20260406151621_add_workbench_skills.exs @@ -0,0 +1,18 @@ +defmodule Console.Repo.Migrations.AddWorkbenchSkills do + use Ecto.Migration + + def change do + create table(:workbench_skills, primary_key: false) do + add :id, :uuid, primary_key: true + add :name, :string + add :description, :string, size: 1024 + add :contents, :binary + add :workbench_id, references(:workbenches, type: :uuid, on_delete: :delete_all) + + timestamps() + end + + create index(:workbench_skills, [:workbench_id]) + create unique_index(:workbench_skills, [:workbench_id, :name]) + end +end diff --git a/priv/repo/migrations/20260408152459_more_workbench_schemas.exs b/priv/repo/migrations/20260408152459_more_workbench_schemas.exs new file mode 100644 index 0000000000..c678c5ddf7 --- /dev/null +++ b/priv/repo/migrations/20260408152459_more_workbench_schemas.exs @@ -0,0 +1,25 @@ +defmodule Console.Repo.Migrations.MoreWorkbenchSchemas do + use Ecto.Migration + + def change do + create table(:workbench_job_activity_agent_runs, primary_key: false) do + add :id, :uuid, primary_key: true + add :workbench_job_activity_id, references(:workbench_job_activities, type: :uuid, on_delete: :delete_all) + add :agent_run_id, references(:agent_runs, type: :uuid, on_delete: :delete_all) + + timestamps() + end + + create unique_index(:workbench_job_activity_agent_runs, [:workbench_job_activity_id, :agent_run_id]) + create index(:workbench_job_activity_agent_runs, [:workbench_job_activity_id]) + create index(:workbench_job_activity_agent_runs, [:agent_run_id]) + + alter table(:alerts) do + add :payload, :map + end + + alter table(:issues) do + add :payload, :map + end + end +end diff --git a/priv/tools/workbench/infrastructure/cluster.json b/priv/tools/workbench/infrastructure/cluster.json new file mode 100644 index 0000000000..e50e4d806c --- /dev/null +++ b/priv/tools/workbench/infrastructure/cluster.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "handle": { + "type": "string", + "description": "The cluster handle (as shown in cluster lists)" + } + }, + "required": ["handle"] +} diff --git a/priv/tools/workbench/infrastructure/cluster_list.json b/priv/tools/workbench/infrastructure/cluster_list.json new file mode 100644 index 0000000000..fc59f34ac0 --- /dev/null +++ b/priv/tools/workbench/infrastructure/cluster_list.json @@ -0,0 +1,9 @@ +{ + "type": "object", + "properties": { + "q": { + "type": "string", + "description": "Optional prefix search on cluster name" + } + } +} diff --git a/priv/tools/workbench/infrastructure/cluster_services.json b/priv/tools/workbench/infrastructure/cluster_services.json new file mode 100644 index 0000000000..777b6ca265 --- /dev/null +++ b/priv/tools/workbench/infrastructure/cluster_services.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "properties": { + "cluster": { + "type": "string", + "description": "Plural cluster handle to list services for" + }, + "q": { + "type": "string", + "description": "Optional prefix search on service name" + } + }, + "required": ["cluster"] +} diff --git a/priv/tools/workbench/infrastructure/kube_get.json b/priv/tools/workbench/infrastructure/kube_get.json new file mode 100644 index 0000000000..ccffd48e80 --- /dev/null +++ b/priv/tools/workbench/infrastructure/kube_get.json @@ -0,0 +1,30 @@ +{ + "type": "object", + "properties": { + "cluster": { + "type": "string", + "description": "The Plural cluster handle identifying which cluster to query" + }, + "group": { + "type": "string", + "description": "The kubernetes API group (e.g. apps, networking.k8s.io). Use an empty string for the core API group" + }, + "version": { + "type": "string", + "description": "The kubernetes API version for this resource within the group (e.g. v1)" + }, + "kind": { + "type": "string", + "description": "The kubernetes resource kind" + }, + "name": { + "type": "string", + "description": "The kubernetes resource name" + }, + "namespace": { + "type": "string", + "description": "the kubernetes namespace this resource is in, ignore if this is a cluster scoped resource" + } + }, + "required": ["cluster", "version", "kind", "name"] +} diff --git a/priv/tools/workbench/infrastructure/kube_list.json b/priv/tools/workbench/infrastructure/kube_list.json new file mode 100644 index 0000000000..d77a6c5ce3 --- /dev/null +++ b/priv/tools/workbench/infrastructure/kube_list.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "properties": { + "cluster": { + "type": "string", + "description": "The Plural cluster handle identifying which cluster to query" + }, + "group": { + "type": "string", + "description": "The kubernetes API group (e.g. apps, networking.k8s.io). Use an empty string for the core API group" + }, + "version": { + "type": "string", + "description": "The kubernetes API version for this resource within the group (e.g. v1)" + }, + "kind": { + "type": "string", + "description": "The kubernetes resource kind" + }, + "namespace": { + "type": "string", + "description": "the kubernetes namespace this resource is in, ignore if this is a cluster scoped resource" + } + }, + "required": ["cluster", "version", "kind"] +} diff --git a/priv/tools/workbench/infrastructure/service.json b/priv/tools/workbench/infrastructure/service.json new file mode 100644 index 0000000000..20f566747d --- /dev/null +++ b/priv/tools/workbench/infrastructure/service.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "service_id": { + "type": "string", + "description": "UUID of the Plural service (from plrl_cluster_services or the API)" + } + }, + "required": ["service_id"] +} diff --git a/priv/tools/workbench/infrastructure/stack.json b/priv/tools/workbench/infrastructure/stack.json new file mode 100644 index 0000000000..852757fcba --- /dev/null +++ b/priv/tools/workbench/infrastructure/stack.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "stack_id": { + "type": "string", + "description": "UUID of the stack (from plrl_stacks)" + } + }, + "required": ["stack_id"] +} diff --git a/priv/tools/workbench/infrastructure/stack_list.json b/priv/tools/workbench/infrastructure/stack_list.json new file mode 100644 index 0000000000..34cc32f73d --- /dev/null +++ b/priv/tools/workbench/infrastructure/stack_list.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "properties": { + "q": { + "type": "string", + "description": "Optional prefix search on stack name" + }, + "project_id": { + "type": "string", + "description": "Optional filter by project UUID" + }, + "status": { + "type": "string", + "description": "Optional filter by stack status (e.g. running, failed, successful)" + } + } +} diff --git a/priv/tools/workbench/observability/plrl_logs.json b/priv/tools/workbench/observability/plrl_logs.json new file mode 100644 index 0000000000..cf58f52309 --- /dev/null +++ b/priv/tools/workbench/observability/plrl_logs.json @@ -0,0 +1,52 @@ +{ + "type": "object", + "properties": { + "service_id": { + "type": "string", + "description": "Plural service ID used to authorize log access and scope results to that service. Provide this or cluster_id." + }, + "cluster_id": { + "type": "string", + "description": "Plural cluster ID used to authorize log access and scope results to that cluster. Provide this or service_id." + }, + "query": { + "type": "string", + "description": "Query string to filter log lines (syntax depends on the configured log backend)." + }, + "facets": { + "type": "array", + "description": "Facet filters to narrow logs by structured fields (e.g. namespace, pod).", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the facet" + }, + "value": { + "type": "string", + "description": "The value of the facet" + } + } + } + }, + "limit": { + "type": "integer", + "description": "The maximum number of logs to return" + }, + "time_range": { + "type": "object", + "description": "Optional time window; if omitted, defaults to roughly the last 30 minutes.", + "properties": { + "start": { + "type": "string", + "description": "The start time to use to filter logs. Provide in ISO8601 format." + }, + "end": { + "type": "string", + "description": "The end time to use to filter logs. Provide in ISO8601 format." + } + } + } + } +} diff --git a/priv/tools/workbench/observability/plrl_logs_labels.json b/priv/tools/workbench/observability/plrl_logs_labels.json new file mode 100644 index 0000000000..42dd7b5b1e --- /dev/null +++ b/priv/tools/workbench/observability/plrl_logs_labels.json @@ -0,0 +1,56 @@ +{ + "type": "object", + "properties": { + "service_id": { + "type": "string", + "description": "Plural service ID used to authorize log access and scope results to that service. Provide this or cluster_id." + }, + "cluster_id": { + "type": "string", + "description": "Plural cluster ID used to authorize log access and scope results to that cluster. Provide this or service_id." + }, + "query": { + "type": "string", + "description": "Query string to filter log lines before aggregating facet values (syntax depends on the configured log backend)." + }, + "field": { + "type": "string", + "description": "Facet or label field name. When set, returns distinct values for that field; when omitted, behavior follows the labels API for discovering available facets." + }, + "facets": { + "type": "array", + "description": "Facet filters to narrow the log set used when listing facet values.", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the facet" + }, + "value": { + "type": "string", + "description": "The value of the facet" + } + } + } + }, + "limit": { + "type": "integer", + "description": "The maximum number of label/facet values to return" + }, + "time_range": { + "type": "object", + "description": "Optional time window; if omitted, defaults to roughly the last 30 minutes.", + "properties": { + "start": { + "type": "string", + "description": "The start time to use to filter logs. Provide in ISO8601 format." + }, + "end": { + "type": "string", + "description": "The end time to use to filter logs. Provide in ISO8601 format." + } + } + } + } +} diff --git a/schema/schema.graphql b/schema/schema.graphql index d38a929284..ee46c2d34d 100644 --- a/schema/schema.graphql +++ b/schema/schema.graphql @@ -1229,6 +1229,37 @@ type RootMutationType { deleteWorkbenchCron(id: ID!): WorkbenchCron + "Fetches a workbench cron by id. Requires read access to the workbench." + workbenchCron(id: ID!): WorkbenchCron + + "Creates a saved prompt for a workbench. Requires read access to the workbench." + createWorkbenchPrompt( + "the workbench to save a prompt for" + workbenchId: ID! + + attributes: WorkbenchPromptAttributes! + ): WorkbenchPrompt + + "Updates a saved workbench prompt. Requires read access to the workbench." + updateWorkbenchPrompt(id: ID!, attributes: WorkbenchPromptAttributes!): WorkbenchPrompt + + "Deletes a saved workbench prompt. Requires read access to the workbench." + deleteWorkbenchPrompt(id: ID!): WorkbenchPrompt + + "Creates a saved skill for a workbench. Requires write access to the workbench." + createWorkbenchSkill( + "the workbench to save a skill for" + workbenchId: ID! + + attributes: WorkbenchSkillAttributes! + ): WorkbenchSkill + + "Updates a saved workbench skill. Requires write access to the workbench." + updateWorkbenchSkill(id: ID!, attributes: WorkbenchSkillAttributes!): WorkbenchSkill + + "Deletes a saved workbench skill. Requires write access to the workbench." + deleteWorkbenchSkill(id: ID!): WorkbenchSkill + createWorkbenchWebhook( "the workbench to create a webhook for" workbenchId: ID! @@ -1236,6 +1267,9 @@ type RootMutationType { attributes: WorkbenchWebhookAttributes! ): WorkbenchWebhook + "Fetches a workbench webhook by id. Requires read access to the workbench." + getWorkbenchWebhook(id: ID!): WorkbenchWebhook + updateWorkbenchWebhook(id: ID!, attributes: WorkbenchWebhookAttributes!): WorkbenchWebhook deleteWorkbenchWebhook(id: ID!): WorkbenchWebhook @@ -1257,6 +1291,21 @@ type RootMutationType { attributes: WorkbenchMessageAttributes! ): WorkbenchJobActivity + "Updates only the topology field on the job's result. Requires read access to the job's workbench; only the job owner may update." + updateWorkbenchJob( + "the workbench job to update" + jobId: ID! + + "attributes to update on the job (only the result topology is accepted)" + attributes: WorkbenchJobUpdateAttributes! + ): WorkbenchJob + + "Cancels a workbench job. Allowed for the job owner or users with write access to the workbench." + cancelWorkbenchJob( + "the workbench job to cancel" + jobId: ID! + ): WorkbenchJob + cancelAgentRun(id: ID!): AgentRun createAgentRun(runtimeId: ID!, attributes: AgentRunAttributes!): AgentRun @@ -1416,6 +1465,8 @@ type RootSubscriptionType { workbenchJobProgress(jobId: ID!): WorkbenchJobProgress + workbenchTextStream(jobId: ID!): WorkbenchTextStream + clusterUpgradeProgress(upgradeId: ID!): ClusterUpgradeProgress } @@ -1603,6 +1654,9 @@ type Issue { "the detailed description or body content of the issue" body: String! + "raw webhook payload received for this issue" + payload: Map + "the flow this issue is associated with" flow: Flow @@ -2187,6 +2241,7 @@ enum WorkbenchJobActivityType { PLAN USER MEMORY + CONCLUSION } input WorkbenchJobAttributes { @@ -2194,6 +2249,16 @@ input WorkbenchJobAttributes { prompt: String } +input WorkbenchJobUpdateAttributes { + "the result for this job" + result: WorkbenchResultAttributes +} + +input WorkbenchResultAttributes { + "mermaid diagram text for the job result topology (only field clients may set via this mutation)" + topology: String! +} + input WorkbenchAttributes { "the name of the workbench (must be unique)" name: String! @@ -2288,6 +2353,22 @@ input WorkbenchCronAttributes { prompt: String } +input WorkbenchPromptAttributes { + "the saved prompt text" + prompt: String! +} + +input WorkbenchSkillAttributes { + "the saved skill name" + name: String! + + "the saved skill description" + description: String + + "the saved skill contents" + contents: String! +} + input WorkbenchWebhookMatchesAttributes { "regex pattern to match in webhook body" regex: String @@ -2536,6 +2617,10 @@ type Workbench { crons(after: String, first: Int, before: String, last: Int): WorkbenchCronConnection + prompts(after: String, first: Int, before: String, last: Int): WorkbenchPromptConnection + + workbenchSkills(after: String, first: Int, before: String, last: Int): WorkbenchSkillConnection + webhooks(after: String, first: Int, before: String, last: Int): WorkbenchWebhookConnection alerts(after: String, first: Int, before: String, last: Int): AlertConnection @@ -2616,6 +2701,9 @@ type WorkbenchJobActivity { "the agent run that executed this activity" agentRun: AgentRun + "all agent runs associated with this activity (sideloadable)" + agentRuns: [AgentRun] + insertedAt: DateTime updatedAt: DateTime @@ -2800,6 +2888,42 @@ type WorkbenchCron { updatedAt: DateTime } +type WorkbenchPrompt { + "the id of the saved prompt" + id: String! + + "the saved prompt text" + prompt: String + + "the workbench this prompt belongs to" + workbench: Workbench + + insertedAt: DateTime + + updatedAt: DateTime +} + +type WorkbenchSkill { + "the id of the saved skill" + id: String! + + "the saved skill name" + name: String + + "the saved skill description" + description: String + + "the saved skill contents" + contents: String + + "the workbench this skill belongs to" + workbench: Workbench + + insertedAt: DateTime + + updatedAt: DateTime +} + type WorkbenchWebhookMatches { "regex pattern to match" regex: String @@ -2990,6 +3114,11 @@ type WorkbenchJobProgress { arguments: Map } +type WorkbenchTextStream { + activityId: ID + text: String +} + type WorkbenchConnection { pageInfo: PageInfo! edges: [WorkbenchEdge] @@ -3015,6 +3144,16 @@ type WorkbenchCronConnection { edges: [WorkbenchCronEdge] } +type WorkbenchPromptConnection { + pageInfo: PageInfo! + edges: [WorkbenchPromptEdge] +} + +type WorkbenchSkillConnection { + pageInfo: PageInfo! + edges: [WorkbenchSkillEdge] +} + type WorkbenchWebhookConnection { pageInfo: PageInfo! edges: [WorkbenchWebhookEdge] @@ -5139,6 +5278,9 @@ type Alert { "Arbitrary key\/value annotations attached to this alert" annotations: Map + "Raw webhook payload received for this alert" + payload: Map + "Link back to the originating dashboard or alert view in the provider" url: String @@ -14563,6 +14705,16 @@ type WorkbenchWebhookEdge { cursor: String } +type WorkbenchSkillEdge { + node: WorkbenchSkill + cursor: String +} + +type WorkbenchPromptEdge { + node: WorkbenchPrompt + cursor: String +} + type WorkbenchCronEdge { node: WorkbenchCron cursor: String diff --git a/test/console/ai/tools/workbench/infrastructure/catalog_tools_test.exs b/test/console/ai/tools/workbench/infrastructure/catalog_tools_test.exs new file mode 100644 index 0000000000..f2db6ad607 --- /dev/null +++ b/test/console/ai/tools/workbench/infrastructure/catalog_tools_test.exs @@ -0,0 +1,180 @@ +defmodule Console.AI.Tools.Workbench.Infrastructure.CatalogToolsTest do + use Console.DataCase, async: true + + alias Console.AI.Tool + alias Console.AI.Tools.Workbench.Infrastructure.{ + Cluster, + ClusterList, + ClusterServices, + ServiceInspect, + StackInspect, + StackList + } + + describe "ClusterList (plrl_clusters)" do + test "returns {:ok, json} including clusters the user can read" do + user = insert(:user) + cluster = insert(:cluster, read_bindings: [%{user_id: user.id}]) + + assert {:ok, parsed} = Tool.validate(%ClusterList{user: user}, %{}) + assert {:ok, json} = ClusterList.implement(nil, parsed) + assert {:ok, list} = Jason.decode(json) + assert is_list(list) + assert Enum.any?(list, &(&1["id"] == cluster.id)) + end + + test "returns {:ok, empty list} when the user has no cluster access" do + user = insert(:user) + insert(:cluster) + + assert {:ok, parsed} = Tool.validate(%ClusterList{user: user}, %{}) + assert {:ok, json} = ClusterList.implement(nil, parsed) + assert {:ok, []} = Jason.decode(json) + end + end + + describe "Cluster (plrl_cluster)" do + test "returns {:ok, _} when the user can read the cluster" do + user = insert(:user) + cluster = insert(:cluster, read_bindings: [%{user_id: user.id}]) + + assert {:ok, parsed} = Tool.validate(%Cluster{user: user}, %{"handle" => cluster.handle}) + assert {:ok, content} = Cluster.implement(nil, parsed) + assert is_binary(content) + end + + test "returns {:error, _} when the user cannot read the cluster" do + owner = insert(:user) + other = insert(:user) + cluster = insert(:cluster, read_bindings: [%{user_id: owner.id}]) + + assert {:ok, parsed} = Tool.validate(%Cluster{user: other}, %{"handle" => cluster.handle}) + assert {:error, _} = Cluster.implement(nil, parsed) + end + end + + describe "ClusterServices (plrl_cluster_services)" do + test "returns {:ok, json} listing services when the user can read the cluster" do + user = insert(:user) + cluster = insert(:cluster, read_bindings: [%{user_id: user.id}]) + service = insert(:service, cluster: cluster) + + assert {:ok, parsed} = Tool.validate(%ClusterServices{user: user}, %{"cluster" => cluster.handle}) + assert {:ok, json} = ClusterServices.implement(nil, parsed) + assert {:ok, list} = Jason.decode(json) + assert Enum.any?(list, &(&1["id"] == service.id)) + end + + test "returns {:error, _} when the user cannot read the cluster" do + owner = insert(:user) + other = insert(:user) + cluster = insert(:cluster, read_bindings: [%{user_id: owner.id}]) + insert(:service, cluster: cluster) + + assert {:ok, parsed} = Tool.validate(%ClusterServices{user: other}, %{"cluster" => cluster.handle}) + assert {:error, _} = ClusterServices.implement(nil, parsed) + end + end + + describe "ServiceInspect (plrl_service)" do + test "returns {:ok, _} when the user can read the service" do + user = insert(:user) + cluster = insert(:cluster, read_bindings: [%{user_id: user.id}]) + service = insert(:service, cluster: cluster) + + assert {:ok, parsed} = + Tool.validate(%ServiceInspect{user: user}, %{"service_id" => service.id}) + + assert {:ok, content} = ServiceInspect.implement(nil, parsed) + assert is_binary(content) + end + + test "returns {:error, _} when the user cannot read the service" do + owner = insert(:user) + other = insert(:user) + cluster = insert(:cluster, read_bindings: [%{user_id: owner.id}]) + service = insert(:service, cluster: cluster) + + assert {:ok, parsed} = + Tool.validate(%ServiceInspect{user: other}, %{"service_id" => service.id}) + + assert {:error, _} = ServiceInspect.implement(nil, parsed) + end + end + + describe "StackList (plrl_stacks)" do + test "returns {:ok, json} including stacks the user can read" do + user = insert(:user) + stack = insert(:stack, read_bindings: [%{user_id: user.id}]) + + assert {:ok, parsed} = Tool.validate(%StackList{user: user}, %{}) + assert {:ok, json} = StackList.implement(nil, parsed) + assert {:ok, list} = Jason.decode(json) + assert Enum.any?(list, &(&1["id"] == stack.id)) + end + + test "returns {:ok, empty list} when the user has no stack access" do + user = insert(:user) + insert(:stack) + + assert {:ok, parsed} = Tool.validate(%StackList{user: user}, %{}) + assert {:ok, json} = StackList.implement(nil, parsed) + assert {:ok, []} = Jason.decode(json) + end + end + + describe "StackInspect (plrl_stack)" do + test "returns {:ok, _} when the user can read the stack" do + user = insert(:user) + stack = insert(:stack, read_bindings: [%{user_id: user.id}]) + + assert {:ok, parsed} = Tool.validate(%StackInspect{user: user}, %{"stack_id" => stack.id}) + assert {:ok, content} = StackInspect.implement(nil, parsed) + assert is_binary(content) + end + + test "returns {:error, _} when the user cannot read the stack" do + owner = insert(:user) + other = insert(:user) + stack = insert(:stack, read_bindings: [%{user_id: owner.id}]) + + assert {:ok, parsed} = Tool.validate(%StackInspect{user: other}, %{"stack_id" => stack.id}) + assert {:error, _} = StackInspect.implement(nil, parsed) + end + + test "when status is failed, includes latest failed run and failing step logs" do + user = insert(:user) + stack = insert(:stack, status: :failed, read_bindings: [%{user_id: user.id}]) + + run = + insert(:stack_run, + stack: stack, + cluster: stack.cluster, + repository: stack.repository, + status: :failed, + message: "apply failed", + git: stack.git + ) + + step = + insert(:run_step, + run: run, + status: :failed, + stage: :apply, + cmd: "terraform", + args: ["apply"], + index: 1 + ) + + insert(:run_log, step: step, logs: "Error: something broke") + + assert {:ok, parsed} = Tool.validate(%StackInspect{user: user}, %{"stack_id" => stack.id}) + assert {:ok, content} = StackInspect.implement(nil, parsed) + + assert content =~ "Latest failed run" + assert content =~ run.id + assert content =~ "terraform" + assert content =~ "Error: something broke" + end + end +end diff --git a/test/console/ai/workbench/subagents/coding_test.exs b/test/console/ai/workbench/subagents/coding_test.exs index 6e485fa8af..fc905fe326 100644 --- a/test/console/ai/workbench/subagents/coding_test.exs +++ b/test/console/ai/workbench/subagents/coding_test.exs @@ -33,6 +33,12 @@ defmodule Console.AI.Workbench.Subagents.CodingTest do ]} end) + expect(Provider, :completion, fn _, _ -> + {:ok, "complete", [ + %Tool{name: "subagent_result", arguments: %{"output" => "some workbench result"}} + ]} + end) + runtime = insert(:agent_runtime) workbench = insert(:workbench, agent_runtime: runtime, configuration: %{infrastructure: %{services: true, stacks: true, kubernetes: true}}) job = insert(:workbench_job, workbench: workbench, user: admin_user()) @@ -57,7 +63,7 @@ defmodule Console.AI.Workbench.Subagents.CodingTest do assert_receive {:result, result}, :timer.seconds(10) assert result[:status] == :successful - assert is_binary(result[:result][:output]) + assert result[:result][:output] == "some workbench result" end end end diff --git a/test/console/deployments/git_test.exs b/test/console/deployments/git_test.exs index e74bf9f482..b6d09fe24a 100644 --- a/test/console/deployments/git_test.exs +++ b/test/console/deployments/git_test.exs @@ -40,6 +40,15 @@ defmodule Console.Deployments.GitTest do assert_receive {:event, %PubSub.GitRepositoryCreated{item: ^git}} end + test "it will support weird ado urls" do + user = admin_user() + + {:ok, _} = Git.create_repository(%{ + url: "SOME-ORG@vs-ssh.visualstudio.com:v3/SOME-ORG/SRE/plural-mgmtcluster", + private_key: "invalid-key" + }, user) + end + test "it will suss out invalid git urls" do user = admin_user() diff --git a/test/console/deployments/workbenches_test.exs b/test/console/deployments/workbenches_test.exs index 26c63b63ba..bc5c0c56e0 100644 --- a/test/console/deployments/workbenches_test.exs +++ b/test/console/deployments/workbenches_test.exs @@ -299,6 +299,126 @@ defmodule Console.Deployments.WorkbenchesTest do end end + describe "update_workbench_job/3" do + test "job owner can update result topology and keeps other result fields" do + user = insert(:user) + + job = + insert(:workbench_job, + user: user, + result: + build(:workbench_job_result, + working_theory: "keep me", + conclusion: "also keep", + topology: nil + ) + ) + + {:ok, updated} = + Workbenches.update_workbench_job( + %{result: %{topology: "graph TD; A-->B"}}, + job.id, + user + ) + + assert updated.id == job.id + result = Console.Repo.preload(refetch(updated), :result).result + assert result.topology == "graph TD; A-->B" + assert result.working_theory == "keep me" + assert result.conclusion == "also keep" + end + + test "job owner can update when passing the job struct" do + user = insert(:user) + job = insert(:workbench_job, user: user, result: build(:workbench_job_result, topology: "old")) + + {:ok, updated} = + Workbenches.update_workbench_job(%{result: %{topology: "new diagram"}}, job, user) + + assert Console.Repo.preload(updated, :result).result.topology == "new diagram" + end + + test "another user cannot update someone else's job" do + owner = insert(:user) + other = insert(:user) + job = insert(:workbench_job, user: owner) + + {:error, _} = Workbenches.update_workbench_job( + %{result: %{topology: "hacked"}}, + job.id, + other + ) + + assert refetch(job.result).topology != "hacked" + end + end + + describe "cancel_workbench_job/2" do + test "job owner can cancel with only read access to the workbench" do + user = insert(:user) + workbench = insert(:workbench, read_bindings: [%{user_id: user.id}]) + job = insert(:workbench_job, user: user, workbench: workbench, status: :running) + + {:ok, cancelled} = Workbenches.cancel_workbench_job(job.id, user) + + assert cancelled.id == job.id + assert cancelled.status == :cancelled + assert refetch(job).status == :cancelled + end + + test "user with workbench write access can cancel another user's job" do + owner = insert(:user) + writer = insert(:user) + workbench = + insert(:workbench, + read_bindings: [%{user_id: owner.id}], + write_bindings: [%{user_id: writer.id}] + ) + + job = insert(:workbench_job, user: owner, workbench: workbench, status: :running) + + {:ok, cancelled} = Workbenches.cancel_workbench_job(job.id, writer) + + assert cancelled.status == :cancelled + assert refetch(job).status == :cancelled + end + + test "user with only read access cannot cancel someone else's job" do + owner = insert(:user) + reader = insert(:user) + + workbench = + insert(:workbench, + read_bindings: [%{user_id: owner.id}, %{user_id: reader.id}] + ) + + job = insert(:workbench_job, user: owner, workbench: workbench, status: :running) + + assert {:error, "forbidden"} = Workbenches.cancel_workbench_job(job.id, reader) + assert refetch(job).status == :running + end + end + + describe "heartbeat/1" do + test "sets status to running and refreshes updated_at" do + job = insert(:workbench_job, status: :pending) + past = Timex.now() |> Timex.shift(seconds: -30) + + {:ok, job} = + job + |> Ecto.Changeset.change(%{updated_at: past}) + |> Console.Repo.update() + + {:ok, updated} = Workbenches.heartbeat(job) + + assert updated.status == :running + assert Timex.after?(updated.updated_at, past) + + job = refetch(job) + assert job.status == :running + end + end + describe "create_message/3" do test "job owner can create a user message for their job" do user = insert(:user) @@ -330,8 +450,7 @@ defmodule Console.Deployments.WorkbenchesTest do other = insert(:user) job = insert(:workbench_job, user: owner) - assert {:error, "you can only create messages for your own jobs"} = - Workbenches.create_message(%{prompt: "unauthorized"}, job.id, other) + {:error, _} = Workbenches.create_message(%{prompt: "unauthorized"}, job.id, other) refute_receive {:event, %PubSub.WorkbenchJobActivityCreated{}} end @@ -399,8 +518,8 @@ defmodule Console.Deployments.WorkbenchesTest do end end - describe "update_job_activity/3" do - test "updates an activity and refreshes job updated_at and status to running" do + describe "update_job_activity/2" do + test "updates an activity" do job = insert(:workbench_job, status: :pending) activity = insert(:workbench_job_activity, workbench_job: job, type: :coding, status: :running) @@ -416,12 +535,12 @@ defmodule Console.Deployments.WorkbenchesTest do assert_receive {:event, %PubSub.WorkbenchJobActivityUpdated{item: ^updated}} job = refetch(job) - assert job.updated_at - assert job.status == :running + assert job.status == :pending end test "updates only the given attributes" do job = insert(:workbench_job) + job_updated_at = job.updated_at activity = insert(:workbench_job_activity, workbench_job: job, type: :observability, status: :pending) @@ -433,7 +552,7 @@ defmodule Console.Deployments.WorkbenchesTest do assert updated.type == :observability assert_receive {:event, %PubSub.WorkbenchJobActivityUpdated{item: ^updated}} - assert refetch(job).updated_at + assert DateTime.compare(refetch(job).updated_at, job_updated_at) == :eq end end @@ -497,6 +616,13 @@ defmodule Console.Deployments.WorkbenchesTest do job = refetch(job) assert job.status == :successful assert job.completed_at + + [activity] = Console.Repo.all(Console.Schema.WorkbenchJobActivity) + + assert activity.type == :conclusion + assert activity.status == :successful + assert activity.prompt == "completing job..." + assert activity.result.output == "Final conclusion." end test "updates the job result conclusion" do @@ -570,6 +696,7 @@ defmodule Console.Deployments.WorkbenchesTest do }, workbench.id, user) assert cron.workbench_id == workbench.id + assert cron.user_id == user.id assert cron.crontab == "*/5 * * * *" assert cron.prompt == "run analysis" assert cron.next_run_at @@ -647,6 +774,184 @@ defmodule Console.Deployments.WorkbenchesTest do end end + describe "create_workbench_prompt/3" do + test "users with read access to the workbench can create a prompt" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + + {:ok, prompt} = Workbenches.create_workbench_prompt(%{prompt: "hello"}, workbench.id, user) + + assert prompt.workbench_id == workbench.id + assert prompt.prompt == "hello" + assert_receive {:event, %PubSub.WorkbenchPromptCreated{item: ^prompt}} + end + + test "users with write access to the workbench can create a prompt" do + user = insert(:user) + project = insert(:project, write_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + + {:ok, prompt} = Workbenches.create_workbench_prompt(%{prompt: "from writer"}, workbench.id, user) + + assert prompt.workbench_id == workbench.id + assert prompt.prompt == "from writer" + end + + test "users without workbench access cannot create a prompt" do + user = insert(:user) + workbench = insert(:workbench) + + {:error, _} = Workbenches.create_workbench_prompt(%{prompt: "nope"}, workbench.id, user) + end + end + + describe "update_workbench_prompt/3" do + test "users with read access can update a prompt" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + prompt = insert(:workbench_prompt, workbench: workbench, prompt: "old") + + {:ok, updated} = Workbenches.update_workbench_prompt(%{prompt: "new text"}, prompt.id, user) + + assert updated.id == prompt.id + assert updated.prompt == "new text" + assert_receive {:event, %PubSub.WorkbenchPromptUpdated{item: ^updated}} + end + + test "users without access cannot update a prompt" do + user = insert(:user) + prompt = insert(:workbench_prompt, prompt: "secret") + + {:error, _} = Workbenches.update_workbench_prompt(%{prompt: "hacked"}, prompt.id, user) + + assert refetch(prompt).prompt == "secret" + end + end + + describe "delete_workbench_prompt/2" do + test "users with read access can delete a prompt" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + prompt = insert(:workbench_prompt, workbench: workbench) + + {:ok, deleted} = Workbenches.delete_workbench_prompt(prompt.id, user) + + assert deleted.id == prompt.id + refute refetch(prompt) + assert_receive {:event, %PubSub.WorkbenchPromptDeleted{item: ^deleted}} + end + + test "users without access cannot delete a prompt" do + user = insert(:user) + prompt = insert(:workbench_prompt) + + {:error, _} = Workbenches.delete_workbench_prompt(prompt.id, user) + + assert refetch(prompt) + end + end + + describe "create_workbench_skill/3" do + test "users with write access to the workbench can create a skill" do + user = insert(:user) + project = insert(:project, write_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + + {:ok, skill} = + Workbenches.create_workbench_skill( + %{name: "debug-skill", description: "debug helper", contents: "run diagnostics"}, + workbench.id, + user + ) + + assert skill.workbench_id == workbench.id + assert skill.name == "debug-skill" + assert skill.description == "debug helper" + assert skill.contents == "run diagnostics" + assert_receive {:event, %PubSub.WorkbenchSkillCreated{item: ^skill}} + end + + test "users with read access cannot create a skill" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + + {:error, _} = + Workbenches.create_workbench_skill( + %{name: "nope", contents: "forbidden"}, + workbench.id, + user + ) + end + end + + describe "update_workbench_skill/3" do + test "users with write access can update a skill" do + user = insert(:user) + project = insert(:project, write_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + skill = insert(:workbench_skill, workbench: workbench, name: "old", contents: "before") + + {:ok, updated} = + Workbenches.update_workbench_skill( + %{name: "new", description: "new desc", contents: "after"}, + skill.id, + user + ) + + assert updated.id == skill.id + assert updated.name == "new" + assert updated.description == "new desc" + assert updated.contents == "after" + assert_receive {:event, %PubSub.WorkbenchSkillUpdated{item: ^updated}} + end + + test "users without write access cannot update a skill" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + skill = insert(:workbench_skill, workbench: workbench, name: "secret", contents: "secret body") + + {:error, _} = + Workbenches.update_workbench_skill( + %{name: "hacked", contents: "hacked"}, + skill.id, + user + ) + + assert refetch(skill).name == "secret" + end + end + + describe "delete_workbench_skill/2" do + test "users with write access can delete a skill" do + user = insert(:user) + project = insert(:project, write_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + skill = insert(:workbench_skill, workbench: workbench) + + {:ok, deleted} = Workbenches.delete_workbench_skill(skill.id, user) + + assert deleted.id == skill.id + refute refetch(skill) + assert_receive {:event, %PubSub.WorkbenchSkillDeleted{item: ^deleted}} + end + + test "users without write access cannot delete a skill" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + skill = insert(:workbench_skill, workbench: workbench) + + {:error, _} = Workbenches.delete_workbench_skill(skill.id, user) + + assert refetch(skill) + end + end + describe "create_workbench_webhook/3" do test "project writers can create a webhook with observability webhook" do user = insert(:user) diff --git a/test/console/graphql/mutations/deployments/workbench_mutations_test.exs b/test/console/graphql/mutations/deployments/workbench_mutations_test.exs index 97a774c4e9..83efbee20f 100644 --- a/test/console/graphql/mutations/deployments/workbench_mutations_test.exs +++ b/test/console/graphql/mutations/deployments/workbench_mutations_test.exs @@ -406,6 +406,135 @@ defmodule Console.GraphQl.Deployments.WorkbenchMutationsTest do end end + describe "updateWorkbenchJob" do + test "job owner with workbench read access can set result topology" do + user = admin_user() + workbench = insert(:workbench) + + job = + insert(:workbench_job, + user: user, + workbench: workbench, + result: + build(:workbench_job_result, + working_theory: "theory", + conclusion: "conclusion", + topology: nil + ) + ) + + {:ok, %{data: %{"updateWorkbenchJob" => updated}}} = run_query(""" + mutation UpdateJobTopology($jobId: ID!, $attributes: WorkbenchJobUpdateAttributes!) { + updateWorkbenchJob(jobId: $jobId, attributes: $attributes) { + id + result { + topology + workingTheory + conclusion + } + } + } + """, %{ + "jobId" => job.id, + "attributes" => %{"result" => %{"topology" => "graph LR; X-->Y"}} + }, %{current_user: user}) + + assert updated["id"] == job.id + assert updated["result"]["topology"] == "graph LR; X-->Y" + assert updated["result"]["workingTheory"] == "theory" + assert updated["result"]["conclusion"] == "conclusion" + end + + test "non-owner cannot update topology even with workbench access" do + owner = insert(:user) + other = insert(:user) + project = insert(:project, read_bindings: [%{user_id: other.id}]) + workbench = insert(:workbench, project: project) + job = insert(:workbench_job, user: owner, workbench: workbench) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation UpdateJobTopology($jobId: ID!, $attributes: WorkbenchJobUpdateAttributes!) { + updateWorkbenchJob(jobId: $jobId, attributes: $attributes) { + id + } + } + """, %{"jobId" => job.id, "attributes" => %{"result" => %{"topology" => "graph TD; A-->B"}}}, + %{current_user: other} + ) + + assert refetch(job.result).topology != "graph TD; A-->B" + end + end + + describe "cancelWorkbenchJob" do + test "job owner can cancel the job" do + user = insert(:user) + workbench = insert(:workbench, read_bindings: [%{user_id: user.id}]) + job = insert(:workbench_job, user: user, workbench: workbench, status: :running) + + {:ok, %{data: %{"cancelWorkbenchJob" => updated}}} = run_query(""" + mutation CancelWorkbenchJob($jobId: ID!) { + cancelWorkbenchJob(jobId: $jobId) { + id + status + } + } + """, %{"jobId" => job.id}, %{current_user: user}) + + assert updated["id"] == job.id + assert updated["status"] == "CANCELLED" + assert refetch(job).status == :cancelled + end + + test "workbench writer can cancel another user's job" do + owner = insert(:user) + writer = insert(:user) + + workbench = + insert(:workbench, + read_bindings: [%{user_id: owner.id}], + write_bindings: [%{user_id: writer.id}] + ) + + job = insert(:workbench_job, user: owner, workbench: workbench, status: :running) + + {:ok, %{data: %{"cancelWorkbenchJob" => updated}}} = run_query(""" + mutation CancelWorkbenchJob($jobId: ID!) { + cancelWorkbenchJob(jobId: $jobId) { + id + status + } + } + """, %{"jobId" => job.id}, %{current_user: writer}) + + assert updated["status"] == "CANCELLED" + assert refetch(job).status == :cancelled + end + + test "reader who is not the owner cannot cancel" do + owner = insert(:user) + reader = insert(:user) + + workbench = + insert(:workbench, + read_bindings: [%{user_id: owner.id}, %{user_id: reader.id}] + ) + + job = insert(:workbench_job, user: owner, workbench: workbench, status: :running) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation CancelWorkbenchJob($jobId: ID!) { + cancelWorkbenchJob(jobId: $jobId) { + id + status + } + } + """, %{"jobId" => job.id}, %{current_user: reader}) + + assert refetch(job).status == :running + end + end + describe "createWorkbenchCron" do test "it can create a workbench cron" do workbench = insert(:workbench) @@ -535,6 +664,276 @@ defmodule Console.GraphQl.Deployments.WorkbenchMutationsTest do end end + describe "workbenchCron" do + test "it can fetch a workbench cron by id with read access" do + workbench = insert(:workbench) + cron = insert(:workbench_cron, workbench: workbench, crontab: "*/5 * * * *", prompt: "trigger-name") + + {:ok, %{data: %{"workbenchCron" => found}}} = run_query(""" + mutation GetWorkbenchCron($id: ID!) { + workbenchCron(id: $id) { + id + crontab + prompt + workbench { id } + } + } + """, %{"id" => cron.id}, %{current_user: admin_user()}) + + assert found["id"] == cron.id + assert found["crontab"] == "*/5 * * * *" + assert found["prompt"] == "trigger-name" + assert found["workbench"]["id"] == workbench.id + end + + test "users without read access cannot fetch a workbench cron" do + user = insert(:user) + cron = insert(:workbench_cron) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation GetWorkbenchCron($id: ID!) { + workbenchCron(id: $id) { + id + } + } + """, %{"id" => cron.id}, %{current_user: user}) + end + end + + describe "createWorkbenchPrompt" do + test "it can create a saved prompt with read access to the workbench" do + workbench = insert(:workbench) + + {:ok, %{data: %{"createWorkbenchPrompt" => prompt}}} = run_query(""" + mutation CreateWorkbenchPrompt($workbenchId: ID!, $attributes: WorkbenchPromptAttributes!) { + createWorkbenchPrompt(workbenchId: $workbenchId, attributes: $attributes) { + id + prompt + workbench { id } + } + } + """, %{"workbenchId" => workbench.id, "attributes" => %{"prompt" => "saved hello"}}, %{current_user: admin_user()}) + + assert prompt["workbench"]["id"] == workbench.id + assert prompt["prompt"] == "saved hello" + end + + test "project readers can create a prompt" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + + {:ok, %{data: %{"createWorkbenchPrompt" => prompt}}} = run_query(""" + mutation CreateWorkbenchPrompt($workbenchId: ID!, $attributes: WorkbenchPromptAttributes!) { + createWorkbenchPrompt(workbenchId: $workbenchId, attributes: $attributes) { + id + prompt + workbench { id } + } + } + """, %{"workbenchId" => workbench.id, "attributes" => %{"prompt" => "from reader"}}, %{current_user: user}) + + assert prompt["workbench"]["id"] == workbench.id + assert prompt["prompt"] == "from reader" + end + + test "users without read access cannot create a prompt" do + user = insert(:user) + workbench = insert(:workbench) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation CreateWorkbenchPrompt($workbenchId: ID!, $attributes: WorkbenchPromptAttributes!) { + createWorkbenchPrompt(workbenchId: $workbenchId, attributes: $attributes) { + id + prompt + } + } + """, %{"workbenchId" => workbench.id, "attributes" => %{"prompt" => "nope"}}, %{current_user: user}) + end + end + + describe "updateWorkbenchPrompt" do + test "it can update a saved prompt" do + workbench = insert(:workbench) + p = insert(:workbench_prompt, workbench: workbench, prompt: "old") + + {:ok, %{data: %{"updateWorkbenchPrompt" => updated}}} = run_query(""" + mutation UpdateWorkbenchPrompt($id: ID!, $attributes: WorkbenchPromptAttributes!) { + updateWorkbenchPrompt(id: $id, attributes: $attributes) { + id + prompt + } + } + """, %{"id" => p.id, "attributes" => %{"prompt" => "new content"}}, %{current_user: admin_user()}) + + assert updated["id"] == p.id + assert updated["prompt"] == "new content" + end + + test "users without access cannot update a prompt" do + user = insert(:user) + p = insert(:workbench_prompt, prompt: "unchanged") + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation UpdateWorkbenchPrompt($id: ID!, $attributes: WorkbenchPromptAttributes!) { + updateWorkbenchPrompt(id: $id, attributes: $attributes) { + id + prompt + } + } + """, %{"id" => p.id, "attributes" => %{"prompt" => "hacked"}}, %{current_user: user}) + + assert refetch(p).prompt == "unchanged" + end + end + + describe "deleteWorkbenchPrompt" do + test "it can delete a saved prompt" do + workbench = insert(:workbench) + p = insert(:workbench_prompt, workbench: workbench) + + {:ok, %{data: %{"deleteWorkbenchPrompt" => deleted}}} = run_query(""" + mutation DeleteWorkbenchPrompt($id: ID!) { + deleteWorkbenchPrompt(id: $id) { + id + } + } + """, %{"id" => p.id}, %{current_user: admin_user()}) + + assert deleted["id"] == p.id + refute refetch(p) + end + + test "users without access cannot delete a prompt" do + user = insert(:user) + p = insert(:workbench_prompt) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation DeleteWorkbenchPrompt($id: ID!) { + deleteWorkbenchPrompt(id: $id) { + id + } + } + """, %{"id" => p.id}, %{current_user: user}) + + assert refetch(p) + end + end + + describe "createWorkbenchSkill" do + test "it can create a saved skill with write access to the workbench" do + workbench = insert(:workbench) + + {:ok, %{data: %{"createWorkbenchSkill" => skill}}} = run_query(""" + mutation CreateWorkbenchSkill($workbenchId: ID!, $attributes: WorkbenchSkillAttributes!) { + createWorkbenchSkill(workbenchId: $workbenchId, attributes: $attributes) { + id + name + description + contents + workbench { id } + } + } + """, %{"workbenchId" => workbench.id, "attributes" => %{"name" => "my-skill", "description" => "desc", "contents" => "echo hello"}}, %{current_user: admin_user()}) + + assert skill["workbench"]["id"] == workbench.id + assert skill["name"] == "my-skill" + assert skill["description"] == "desc" + assert skill["contents"] == "echo hello" + end + + test "project readers cannot create a skill" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation CreateWorkbenchSkill($workbenchId: ID!, $attributes: WorkbenchSkillAttributes!) { + createWorkbenchSkill(workbenchId: $workbenchId, attributes: $attributes) { + id + name + } + } + """, %{"workbenchId" => workbench.id, "attributes" => %{"name" => "forbidden", "contents" => "nope"}}, %{current_user: user}) + end + end + + describe "updateWorkbenchSkill" do + test "it can update a saved skill" do + workbench = insert(:workbench) + skill = insert(:workbench_skill, workbench: workbench, name: "old", description: "old", contents: "before") + + {:ok, %{data: %{"updateWorkbenchSkill" => updated}}} = run_query(""" + mutation UpdateWorkbenchSkill($id: ID!, $attributes: WorkbenchSkillAttributes!) { + updateWorkbenchSkill(id: $id, attributes: $attributes) { + id + name + description + contents + } + } + """, %{"id" => skill.id, "attributes" => %{"name" => "new", "description" => "new-desc", "contents" => "after"}}, %{current_user: admin_user()}) + + assert updated["id"] == skill.id + assert updated["name"] == "new" + assert updated["description"] == "new-desc" + assert updated["contents"] == "after" + end + + test "project readers cannot update a skill" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + skill = insert(:workbench_skill, workbench: workbench, name: "secret", contents: "secret") + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation UpdateWorkbenchSkill($id: ID!, $attributes: WorkbenchSkillAttributes!) { + updateWorkbenchSkill(id: $id, attributes: $attributes) { + id + name + } + } + """, %{"id" => skill.id, "attributes" => %{"name" => "hacked", "contents" => "hacked"}}, %{current_user: user}) + + assert refetch(skill).name == "secret" + end + end + + describe "deleteWorkbenchSkill" do + test "it can delete a saved skill" do + workbench = insert(:workbench) + skill = insert(:workbench_skill, workbench: workbench) + + {:ok, %{data: %{"deleteWorkbenchSkill" => deleted}}} = run_query(""" + mutation DeleteWorkbenchSkill($id: ID!) { + deleteWorkbenchSkill(id: $id) { + id + } + } + """, %{"id" => skill.id}, %{current_user: admin_user()}) + + assert deleted["id"] == skill.id + refute refetch(skill) + end + + test "project readers cannot delete a skill" do + user = insert(:user) + project = insert(:project, read_bindings: [%{user_id: user.id}]) + workbench = insert(:workbench, project: project) + skill = insert(:workbench_skill, workbench: workbench) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation DeleteWorkbenchSkill($id: ID!) { + deleteWorkbenchSkill(id: $id) { + id + } + } + """, %{"id" => skill.id}, %{current_user: user}) + + assert refetch(skill) + end + end + describe "createWorkbenchWebhook" do test "it can create a workbench webhook with observability webhook" do workbench = insert(:workbench) @@ -654,6 +1053,40 @@ defmodule Console.GraphQl.Deployments.WorkbenchMutationsTest do end end + describe "getWorkbenchWebhook" do + test "it can fetch a workbench webhook by id with read access" do + workbench = insert(:workbench) + webhook = insert(:workbench_webhook, workbench: workbench, name: "trigger-name") + + {:ok, %{data: %{"getWorkbenchWebhook" => found}}} = run_query(""" + mutation GetWorkbenchWebhook($id: ID!) { + getWorkbenchWebhook(id: $id) { + id + name + workbench { id } + } + } + """, %{"id" => webhook.id}, %{current_user: admin_user()}) + + assert found["id"] == webhook.id + assert found["name"] == "trigger-name" + assert found["workbench"]["id"] == workbench.id + end + + test "users without read access cannot fetch a workbench webhook" do + user = insert(:user) + webhook = insert(:workbench_webhook) + + {:ok, %{errors: [_ | _]}} = run_query(""" + mutation GetWorkbenchWebhook($id: ID!) { + getWorkbenchWebhook(id: $id) { + id + } + } + """, %{"id" => webhook.id}, %{current_user: user}) + end + end + describe "deleteWorkbenchWebhook" do test "it can delete a workbench webhook" do workbench = insert(:workbench) diff --git a/test/console/graphql/queries/deployments/workbench_queries_test.exs b/test/console/graphql/queries/deployments/workbench_queries_test.exs index b0c5c6eb09..05998cd348 100644 --- a/test/console/graphql/queries/deployments/workbench_queries_test.exs +++ b/test/console/graphql/queries/deployments/workbench_queries_test.exs @@ -179,6 +179,64 @@ defmodule Console.GraphQl.Deployments.WorkbenchQueriesTest do assert Enum.any?(nodes, & &1["crontab"] == "*/5 * * * *" and &1["prompt"] == "run 1") end + test "it can fetch workbench prompts" do + workbench = insert(:workbench) + p1 = insert(:workbench_prompt, workbench: workbench, prompt: "first") + p2 = insert(:workbench_prompt, workbench: workbench, prompt: "second") + + {:ok, %{data: %{"workbench" => found}}} = run_query(""" + query Workbench($id: ID!) { + workbench(id: $id) { + id + prompts(first: 5) { + edges { + node { + id + prompt + } + } + } + } + } + """, %{"id" => workbench.id}, %{current_user: admin_user()}) + + assert found["id"] == workbench.id + nodes = from_connection(found["prompts"]) + assert ids_equal(nodes, [p1, p2]) + assert Enum.any?(nodes, & &1["prompt"] == "first") + assert Enum.any?(nodes, & &1["prompt"] == "second") + end + + test "it can fetch workbench skills" do + workbench = insert(:workbench) + s1 = insert(:workbench_skill, workbench: workbench, name: "skill-one", description: "first", contents: "echo one") + s2 = insert(:workbench_skill, workbench: workbench, name: "skill-two", description: "second", contents: "echo two") + + {:ok, %{data: %{"workbench" => found}}} = run_query(""" + query Workbench($id: ID!) { + workbench(id: $id) { + id + workbenchSkills(first: 5) { + edges { + node { + id + name + description + contents + } + } + } + } + } + """, %{"id" => workbench.id}, %{current_user: admin_user()}) + + assert found["id"] == workbench.id + nodes = from_connection(found["workbenchSkills"]) + assert ids_equal(nodes, [s1, s2]) + assert Enum.any?(nodes, & &1["name"] == "skill-one" and &1["description"] == "first" and &1["contents"] == "echo one") + assert Enum.any?(nodes, & &1["name"] == "skill-two" and &1["description"] == "second" and &1["contents"] == "echo two") + end + test "it can fetch workbench webhooks" do workbench = insert(:workbench) webhook1 = insert(:workbench_webhook, workbench: workbench, name: "wh-one") diff --git a/test/console/pipelines/ai/workbench_cron/pipeline_test.exs b/test/console/pipelines/ai/workbench_cron/pipeline_test.exs index 91415deed2..08a6a0714a 100644 --- a/test/console/pipelines/ai/workbench_cron/pipeline_test.exs +++ b/test/console/pipelines/ai/workbench_cron/pipeline_test.exs @@ -6,12 +6,15 @@ defmodule Console.Pipelines.AI.WorkbenchCron.PipelineTest do describe "handle_event/1" do test "creates a workbench job and updates cron last_run_at and next_run_at" do insert(:user, bot_name: "console", roles: %{admin: true}) - workbench = insert(:workbench) + group = insert(:group) + %{user: user} =insert(:group_member, group: group) + workbench = insert(:workbench, read_bindings: [%{group_id: group.id}]) prompt = "scheduled analysis" past = DateTime.add(DateTime.utc_now(), -60, :second) cron = insert(:workbench_cron, workbench: workbench, prompt: prompt, + user: user, crontab: "*/5 * * * *", next_run_at: past, last_run_at: nil @@ -21,6 +24,7 @@ defmodule Console.Pipelines.AI.WorkbenchCron.PipelineTest do assert job.prompt == prompt assert job.workbench_id == workbench.id + assert job.user_id == user.id assert job.status == :pending assert_receive {:event, %PubSub.WorkbenchJobCreated{item: ^job}} diff --git a/test/console_web/controllers/webhook_controller_test.exs b/test/console_web/controllers/webhook_controller_test.exs index 2433cd1a49..5a3eadfd01 100644 --- a/test/console_web/controllers/webhook_controller_test.exs +++ b/test/console_web/controllers/webhook_controller_test.exs @@ -19,6 +19,27 @@ defmodule ConsoleWeb.WebhookControllerTest do end describe "#scm/2" do + test "it returns 403 for azure devops webhook without valid basic auth", %{conn: conn} do + hook = insert(:scm_webhook, type: :azure_devops) + payload = Jason.encode!(%{}) + + conn + |> put_req_header("content-type", "application/json") + |> post("/ext/v1/webhooks/azure_devops/#{hook.external_id}", payload) + |> response(403) + end + + test "it returns 200 for azure devops webhook with valid basic auth", %{conn: conn} do + hook = insert(:scm_webhook, type: :azure_devops) + payload = Jason.encode!(%{}) + + conn + |> put_req_header("authorization", Plug.BasicAuth.encode_basic_auth("plrl", hook.hmac)) + |> put_req_header("content-type", "application/json") + |> post("/ext/v1/webhooks/azure_devops/#{hook.external_id}", payload) + |> response(200) + end + test "it can handle group memberships", %{conn: conn} do hook = insert(:scm_webhook, type: :github) group = insert(:group, name: "some-org:some-group") diff --git a/test/support/factory.ex b/test/support/factory.ex index 13100a02ed..667873912c 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -1206,6 +1206,23 @@ defmodule Console.Factory do crontab: "*/5 * * * *", prompt: "test prompt", next_run_at: Timex.now(), + workbench: build(:workbench), + user: build(:user) + } + end + + def workbench_prompt_factory do + %Schema.WorkbenchPrompt{ + prompt: "saved prompt text", + workbench: build(:workbench) + } + end + + def workbench_skill_factory do + %Schema.WorkbenchSkill{ + name: sequence(:workbench_skill_name, &"workbench-skill-#{&1}"), + description: "saved skill description", + contents: "saved skill contents", workbench: build(:workbench) } end