diff --git a/cli/src/commands/review/aiReviewCore.gen.ts b/cli/src/commands/review/aiReviewCore.gen.ts index 34d98da18f..546b82f124 100644 --- a/cli/src/commands/review/aiReviewCore.gen.ts +++ b/cli/src/commands/review/aiReviewCore.gen.ts @@ -74,7 +74,7 @@ export function formatFolderInstructions( } // What the review asks of the model and how its answer is read, for every place it runs: the -// deployment request panel here, and `wmill review` on a git sync pull request, which carries +// deployment request panel and the drafts review here, and `wmill review` on a git sync pull request, which carries // this module through cli/generate-ai-review.ts. Keep it free of `$lib` imports; the // staleness test in cli/test/ai_review_bundle_unit.test.ts fails when the CLI's copy drifts. @@ -109,32 +109,47 @@ export const TOTAL_CHAR_BUDGET = 160_000 // Under the Anthropic SDK's non-streaming pre-flight max_tokens limit. export const MAX_OUTPUT_TOKENS = 8_000 -/** What is being reviewed: a fork's deployment request, or a pull request on a git sync repo. */ -export type ReviewSubject = 'deployment_request' | 'pull_request' +/** + * What is being reviewed: a fork's deployment request, a workspace's undeployed drafts, or a + * pull request on a git sync repo. + */ +export type ReviewSubject = 'deployment_request' | 'draft' | 'pull_request' const INTRO: Record = { deployment_request: 'You review a Windmill deployment request: a set of changes made in a fork workspace that will be merged into its parent workspace. Each changed item is given with its parent version (what is deployed now) and its fork version (what will be deployed).', + draft: + 'You review drafts in a Windmill workspace: changes saved to items of the workspace but not deployed yet. Each changed item is given with its deployed version (what is deployed now) and its draft version (what will be deployed).', pull_request: "You review a pull request on a git repository that Windmill's git sync keeps in step with a workspace: merging it deploys its changes. Each changed item is given with the files that make it up in the repository, each as a unified diff of the base branch (what is deployed now) against the pull request (what will be deployed). Every diff line starts with its line number: R for line n of the pull request's version of the file, L for line n of the base version, then + for an added line, - for a removed one and a space for unchanged context." } +const ITEM_ANSWER_SHAPE = + '{"summary": "", "findings": [{"kind": "", "path": "", "severity": "high" | "medium" | "low", "comment": ""}]}' + const ANSWER_SHAPE: Record = { - deployment_request: - '{"summary": "", "findings": [{"kind": "", "path": "", "severity": "high" | "medium" | "low", "comment": ""}]}', + deployment_request: ITEM_ANSWER_SHAPE, + draft: ITEM_ANSWER_SHAPE, pull_request: '{"summary": "", "findings": [{"kind": "", "path": "", "file": "", "line": "", "severity": "high" | "medium" | "low", "comment": ""}]}' } const SOURCE: Record = { deployment_request: 'The parent workspace', + draft: 'The workspace', pull_request: "The repository's base branch" } +const APPROVED: Record = { + deployment_request: 'the merge', + draft: 'the deployment', + pull_request: 'the merge' +} + export function reviewSystemPrompt(subject: ReviewSubject): string { return `${INTRO[subject]} An item can be a script, flow, app, schedule, trigger, folder, resource type, datatable migration, variable or resource. -Report only issues a reviewer should act on before approving the merge: +Report only issues a reviewer should act on before approving ${APPROVED[subject]}: - bugs and logic errors introduced by the change - breaking changes: removed or renamed inputs, changed input types, changed outputs that callers may rely on - risky operational changes: schedules retimed, triggers pointed at a different script or flow, folder owners or permissions, app policies, destructive migrations diff --git a/frontend/src/lib/components/CompareDrafts.svelte b/frontend/src/lib/components/CompareDrafts.svelte index 2dc0b1fa59..a4e9495ce3 100644 --- a/frontend/src/lib/components/CompareDrafts.svelte +++ b/frontend/src/lib/components/CompareDrafts.svelte @@ -42,6 +42,9 @@ import { userStore, userWorkspaces } from '$lib/stores' import { capabilitiesIn } from '$lib/roleCapabilities' import { childWorkspaceNoun } from '$lib/utils/devWorkspaceLabel' + import { copilotInfo } from '$lib/aiStore' + import AiReview from './deploymentRequest/AiReview.svelte' + import { draftReviewItem } from './deploymentRequest/draftReviewItem' interface Props { currentWorkspaceId: string @@ -456,6 +459,13 @@ selectedItems = [] } + // Your own drafts only: another user's draft is view-only, and reading one returns yours. + const reviewItems = $derived( + visibleItems + .filter((i) => selectedItems.includes(i.key) && i.mine && i.draftKind !== 'data_pipeline') + .map((i) => draftReviewItem(i.draftKind, i.path, currentWorkspaceId, i.draft_only)) + ) + // --- Diff --- let diffDrawer: DiffDrawer | undefined = $state(undefined) let isFlow = $state(false) @@ -480,7 +490,13 @@ mode: 'simple', original: deployed as any, current: draft as any, - title: 'Deployed → Draft' + title: 'Deployed → Draft', + aiReview: { + workspaceId: currentWorkspaceId, + kind: item.draftKind, + path: item.path, + added: item.draft_only + } }) } @@ -912,35 +928,45 @@ {/snippet} {#snippet footer()} -
-
- - +
+
+
+ + +
+ {#if !deployPerm.ok} + {deployPerm.reason} + {:else if undeployableSelectedCount > 0} + + {undeployableSelectedCount} selected draft{undeployableSelectedCount !== 1 + ? 's' + : ''} + can't be deployed (no write permission on the path) but can still be discarded + + {/if}
- {#if !deployPerm.ok} - {deployPerm.reason} - {:else if undeployableSelectedCount > 0} - - {undeployableSelectedCount} selected draft{undeployableSelectedCount !== 1 ? 's' : ''} - can't be deployed (no write permission on the path) but can still be discarded - + {#if $copilotInfo.enabled} + {/if}
{/snippet} diff --git a/frontend/src/lib/components/CompareWorkspaces.svelte b/frontend/src/lib/components/CompareWorkspaces.svelte index 8868813958..06b691679c 100644 --- a/frontend/src/lib/components/CompareWorkspaces.svelte +++ b/frontend/src/lib/components/CompareWorkspaces.svelte @@ -1854,9 +1854,12 @@ {#if mergeIntoParent && $copilotInfo.enabled && !hasOpenDeploymentRequest && !deploymentRequestPanel?.isDialogOpen()} {/if} diff --git a/frontend/src/lib/components/DiffDrawer.svelte b/frontend/src/lib/components/DiffDrawer.svelte index 2e5810e036..b7b5aa8922 100644 --- a/frontend/src/lib/components/DiffDrawer.svelte +++ b/frontend/src/lib/components/DiffDrawer.svelte @@ -14,7 +14,10 @@ import { sendUserToast } from '$lib/toast' import type { Script } from '$lib/gen' import Select from './select/Select.svelte' - import type { DiffVersionOption } from './diff_drawer' + import type { DiffAiReview, DiffVersionOption } from './diff_drawer' + import { copilotInfo } from '$lib/aiStore' + import AiReview from './deploymentRequest/AiReview.svelte' + import type { ReviewScope } from './deploymentRequest/aiReview' type DiffData = { lang?: string @@ -36,6 +39,25 @@ let diffViewer: Drawer | undefined = $state(undefined) + let aiReviewScope: ReviewScope | undefined = $state(undefined) + /** Normal mode: what the review is rebuilt from when the version picker swaps the left side. */ + let aiReviewTarget: { review: DiffAiReview; current: Value } | undefined = undefined + + function reviewScope(review: DiffAiReview, before: Value, after: Value): ReviewScope { + return { + kind: 'drafts', + workspaceId: review.workspaceId, + items: [ + { + kind: review.kind, + path: review.path, + change: review.added ? 'added' : 'modified', + load: async () => ({ before, after }) + } + ] + } + } + interface Props { restoreDeployed?: () => Promise isFlow?: boolean @@ -77,6 +99,8 @@ if (token == null) openingToken++ data = undefined diffType = undefined + aiReviewScope = undefined + aiReviewTarget = undefined diffViewer?.openDrawer() } @@ -111,6 +135,8 @@ loadingVersion = false data = undefined diffType = undefined + aiReviewScope = undefined + aiReviewTarget = undefined diffViewer?.closeDrawer() } @@ -209,6 +235,11 @@ deployed: prepareDiff(value), deployedLabel: opt?.isHead ? headLabel : opt?.label } + // The review covers the version on screen; a new scope also drops a result for the + // previous one. + if (aiReviewTarget) { + aiReviewScope = reviewScope(aiReviewTarget.review, value, aiReviewTarget.current) + } } catch (e: any) { // The snap-back would otherwise be the only sign the version never loaded. if (generation === versionLoadGeneration) { @@ -270,6 +301,7 @@ current: Value defaultDiffType?: 'deployed' | 'draft' button?: { text: string; onClick: () => void } + aiReview?: DiffAiReview } | { mode: 'simple' @@ -277,6 +309,7 @@ current: Value title: string button?: { text: string; onClick: () => void } + aiReview?: DiffAiReview }, token?: number ) { @@ -284,6 +317,8 @@ // caller with none is taking the drawer, so it claims one. if (token != null && token !== openingToken) return if (token == null) openingToken++ + aiReviewScope = undefined + aiReviewTarget = undefined if (diff.mode === 'normal') { const { deployed, @@ -326,6 +361,13 @@ // The draft-vs-current view (and its tab) is obsolete — always show the // deployed-vs-current diff. diffType = 'deployed' + if (diff.aiReview) { + aiReviewTarget = { + review: { ...diff.aiReview, added: diff.aiReview.added || !!deployed.draft_only }, + current + } + aiReviewScope = reviewScope(aiReviewTarget.review, deployed, current) + } } else { const { original, current, title, button } = diff data = { @@ -336,6 +378,7 @@ button } diffType = 'custom' + if (diff.aiReview) aiReviewScope = reviewScope(diff.aiReview, original, current) } } @@ -343,6 +386,14 @@
+ {#if data && aiReviewScope && $copilotInfo.enabled} +
+ + {#key aiReviewScope} + + {/key} +
+ {/if} {#if data}