From bab44380f9659ae0516a61bf9836618465fdc456 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:41:10 +0000 Subject: [PATCH 1/3] feat: add diff line count to plans table results view Show +added/-removed line counts (via LCS diff) alongside the "View diff" button in the release target results table, similar to GitHub's diff display. Co-authored-by: Aditya Choudhari --- .../page.$deploymentId.plans.$planId.tsx | 42 +++++++++++++++---- packages/trpc/src/routes/deployment-plans.ts | 27 ++++++++++++ 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/apps/web/app/routes/ws/deployments/page.$deploymentId.plans.$planId.tsx b/apps/web/app/routes/ws/deployments/page.$deploymentId.plans.$planId.tsx index 621b5e0cb..2087cb496 100644 --- a/apps/web/app/routes/ws/deployments/page.$deploymentId.plans.$planId.tsx +++ b/apps/web/app/routes/ws/deployments/page.$deploymentId.plans.$planId.tsx @@ -41,6 +41,29 @@ function resultTitle(result: Result) { return `${result.environment.name} · ${result.resource.name} · ${result.agent.name}`; } +function DiffStats({ + stats, +}: { + stats: { added: number; removed: number } | null; +}) { + if (stats == null) return null; + return ( + + {stats.added > 0 && ( + + +{stats.added} + + )} + {stats.added > 0 && stats.removed > 0 && ( + + )} + {stats.removed > 0 && ( + -{stats.removed} + )} + + ); +} + function ChangesCell({ result, onViewDiff, @@ -63,14 +86,17 @@ function ChangesCell({ return Unsupported; if (result.hasChanges === true) return ( - +
+ + +
); if (result.hasChanges === false) return No changes; diff --git a/packages/trpc/src/routes/deployment-plans.ts b/packages/trpc/src/routes/deployment-plans.ts index fdb362c07..aa8660316 100644 --- a/packages/trpc/src/routes/deployment-plans.ts +++ b/packages/trpc/src/routes/deployment-plans.ts @@ -7,6 +7,30 @@ import { Permission } from "@ctrlplane/validators/auth"; import { protectedProcedure, router } from "../trpc.js"; +function computeDiffStats( + current: string | null, + proposed: string | null, +): { added: number; removed: number } | null { + if (current == null || proposed == null) return null; + const a = current.split("\n"); + const b = proposed.split("\n"); + const m = a.length; + const n = b.length; + let prev = new Array(n + 1).fill(0); + for (let i = 1; i <= m; i++) { + const curr = new Array(n + 1).fill(0); + for (let j = 1; j <= n; j++) { + curr[j] = + a[i - 1] === b[j - 1] + ? (prev[j - 1] ?? 0) + 1 + : Math.max(prev[j] ?? 0, curr[j - 1] ?? 0); + } + prev = curr; + } + const lcs = prev[n] ?? 0; + return { added: n - lcs, removed: m - lcs }; +} + type PlanSummary = { total: number; computing: number; @@ -161,6 +185,8 @@ export const deploymentPlansRouter = router({ hasChanges: schema.deploymentPlanTargetResult.hasChanges, message: schema.deploymentPlanTargetResult.message, contentHash: schema.deploymentPlanTargetResult.contentHash, + current: schema.deploymentPlanTargetResult.current, + proposed: schema.deploymentPlanTargetResult.proposed, startedAt: schema.deploymentPlanTargetResult.startedAt, completedAt: schema.deploymentPlanTargetResult.completedAt, dispatchContext: schema.deploymentPlanTargetResult.dispatchContext, @@ -198,6 +224,7 @@ export const deploymentPlansRouter = router({ }, status: r.status, hasChanges: r.hasChanges, + diffStats: computeDiffStats(r.current, r.proposed), message: r.message, contentHash: r.contentHash, startedAt: r.startedAt, From 899690f358fa6b5d75c4aee0bbc1e01b7267e04f Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Thu, 23 Apr 2026 11:45:10 -0700 Subject: [PATCH 2/3] cleanup --- packages/trpc/package.json | 2 + packages/trpc/src/routes/deployment-plans.ts | 24 ++----- pnpm-lock.yaml | 74 ++++++-------------- 3 files changed, 32 insertions(+), 68 deletions(-) diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 299b14492..c939d8361 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -32,6 +32,7 @@ "@trpc/server": "11.0.0-rc.364", "better-auth": "^1.4.6", "cel-js": "^0.8.2", + "diff": "^9.0.0", "lodash": "catalog:", "superjson": "catalog:", "ts-is-present": "catalog:", @@ -43,6 +44,7 @@ "@ctrlplane/prettier-config": "workspace:*", "@ctrlplane/tsconfig": "workspace:*", "@octokit/types": "^13.5.0", + "@types/diff": "^8.0.0", "@types/lodash": "catalog:", "@types/node": "catalog:node22", "@types/uuid": "^10.0.0", diff --git a/packages/trpc/src/routes/deployment-plans.ts b/packages/trpc/src/routes/deployment-plans.ts index beb0e9e72..70b89f02c 100644 --- a/packages/trpc/src/routes/deployment-plans.ts +++ b/packages/trpc/src/routes/deployment-plans.ts @@ -1,4 +1,6 @@ import { TRPCError } from "@trpc/server"; +import { diffLines } from "diff"; +import _ from "lodash"; import { z } from "zod"; import { and, count, desc, eq, inArray, takeFirstOrNull } from "@ctrlplane/db"; @@ -12,23 +14,11 @@ function computeDiffStats( proposed: string | null, ): { added: number; removed: number } | null { if (current == null || proposed == null) return null; - const a = current.split("\n"); - const b = proposed.split("\n"); - const m = a.length; - const n = b.length; - let prev = new Array(n + 1).fill(0); - for (let i = 1; i <= m; i++) { - const curr = new Array(n + 1).fill(0); - for (let j = 1; j <= n; j++) { - curr[j] = - a[i - 1] === b[j - 1] - ? (prev[j - 1] ?? 0) + 1 - : Math.max(prev[j] ?? 0, curr[j - 1] ?? 0); - } - prev = curr; - } - const lcs = prev[n] ?? 0; - return { added: n - lcs, removed: m - lcs }; + const parts = diffLines(current, proposed); + return { + added: _.sumBy(parts, (p) => (p.added ? p.count : 0)), + removed: _.sumBy(parts, (p) => (p.removed ? p.count : 0)), + }; } type PlanSummary = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1d8f7daf..37f782fc8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,7 +164,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.4.6 - version: 1.4.6(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.4.6(next@15.2.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) cel-js: specifier: ^0.8.2 version: 0.8.2 @@ -628,7 +628,7 @@ importers: version: 0.11.1(typescript@5.9.3)(zod@3.24.2) better-auth: specifier: ^1.4.6 - version: 1.4.6(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.4.6(next@15.2.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) lodash: specifier: 'catalog:' version: 4.17.21 @@ -912,10 +912,13 @@ importers: version: 11.0.0-rc.364 better-auth: specifier: ^1.4.6 - version: 1.4.6(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + version: 1.4.6(next@15.2.4(@babel/core@7.28.4)(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1) cel-js: specifier: ^0.8.2 version: 0.8.2 + diff: + specifier: ^9.0.0 + version: 9.0.0 lodash: specifier: 'catalog:' version: 4.17.21 @@ -941,6 +944,9 @@ importers: '@ctrlplane/tsconfig': specifier: workspace:* version: link:../../tooling/typescript + '@types/diff': + specifier: ^8.0.0 + version: 8.0.0 '@types/lodash': specifier: 'catalog:' version: 4.17.12 @@ -4403,6 +4409,10 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/diff@8.0.0': + resolution: {integrity: sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw==} + deprecated: This is a stub types definition. diff provides its own type definitions, so you do not need this installed. + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -5556,6 +5566,10 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -12322,6 +12336,10 @@ snapshots: '@types/d3-transition': 3.0.8 '@types/d3-zoom': 3.0.8 + '@types/diff@8.0.0': + dependencies: + diff: 9.0.0 + '@types/estree@1.0.8': {} '@types/express-serve-static-core@5.1.0': @@ -12934,26 +12952,6 @@ snapshots: react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - better-auth@1.4.6(next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1))(react-dom@19.2.1(react@19.2.1))(react@19.2.1): - dependencies: - '@better-auth/core': 1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1) - '@better-auth/telemetry': 1.4.6(@better-auth/core@1.4.6(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.1.5(zod@4.1.12))(jose@6.1.0)(kysely@0.28.8)(nanostores@1.0.1)) - '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - '@noble/ciphers': 2.0.1 - '@noble/hashes': 2.0.1 - better-call: 1.1.5(zod@4.1.12) - defu: 6.1.4 - jose: 6.1.0 - kysely: 0.28.8 - ms: 4.0.0-nightly.202508271359 - nanostores: 1.0.1 - zod: 4.1.12 - optionalDependencies: - next: 15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) - better-call@1.1.5(zod@4.1.12): dependencies: '@better-auth/utils': 0.3.0 @@ -13630,6 +13628,8 @@ snapshots: diff@4.0.2: optional: true + diff@9.0.0: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -15684,34 +15684,6 @@ snapshots: - babel-plugin-macros optional: true - next@15.2.4(@opentelemetry/api@1.9.0)(@playwright/test@1.53.2)(react-dom@19.2.1(react@19.2.1))(react@19.2.1): - dependencies: - '@next/env': 15.2.4 - '@swc/counter': 0.1.3 - '@swc/helpers': 0.5.15 - busboy: 1.6.0 - caniuse-lite: 1.0.30001760 - postcss: 8.4.31 - react: 19.2.1 - react-dom: 19.2.1(react@19.2.1) - styled-jsx: 5.1.6(@babel/core@7.24.5)(react@19.2.1) - optionalDependencies: - '@next/swc-darwin-arm64': 15.2.4 - '@next/swc-darwin-x64': 15.2.4 - '@next/swc-linux-arm64-gnu': 15.2.4 - '@next/swc-linux-arm64-musl': 15.2.4 - '@next/swc-linux-x64-gnu': 15.2.4 - '@next/swc-linux-x64-musl': 15.2.4 - '@next/swc-win32-arm64-msvc': 15.2.4 - '@next/swc-win32-x64-msvc': 15.2.4 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.53.2 - sharp: 0.33.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - optional: true - no-case@2.3.2: dependencies: lower-case: 1.1.4 From 9722abcc07ad833aa1f3eb4ce2387809c60fae1b Mon Sep 17 00:00:00 2001 From: Aditya Choudhari Date: Thu, 23 Apr 2026 11:55:37 -0700 Subject: [PATCH 3/3] ui updates --- .../page.$deploymentId.plans.$planId.tsx | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/apps/web/app/routes/ws/deployments/page.$deploymentId.plans.$planId.tsx b/apps/web/app/routes/ws/deployments/page.$deploymentId.plans.$planId.tsx index 280331cdd..92d431989 100644 --- a/apps/web/app/routes/ws/deployments/page.$deploymentId.plans.$planId.tsx +++ b/apps/web/app/routes/ws/deployments/page.$deploymentId.plans.$planId.tsx @@ -3,6 +3,7 @@ import { FileText } from "lucide-react"; import { Link, useParams } from "react-router"; import { trpc } from "~/api/trpc"; +import { cn } from "~/lib/utils"; import { Breadcrumb, BreadcrumbItem, @@ -10,7 +11,6 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "~/components/ui/breadcrumb"; -import { Button } from "~/components/ui/button"; import { Separator } from "~/components/ui/separator"; import { SidebarTrigger } from "~/components/ui/sidebar"; import { @@ -65,13 +65,7 @@ function DiffStats({ ); } -function ChangesCell({ - result, - onViewDiff, -}: { - result: Result; - onViewDiff: (resultId: string) => void; -}) { +function ChangesCell({ result }: { result: Result }) { if (result.status === "computing") return ; if (result.status === "errored") @@ -86,19 +80,7 @@ function ChangesCell({ if (result.status === "unsupported") return Unsupported; if (result.hasChanges === true) - return ( -
- - -
- ); + return ; if (result.hasChanges === false) return No changes; return ; @@ -125,8 +107,12 @@ function ResultsTableRow({ result: Result; onViewDiff: (resultId: string) => void; }) { + const isClickable = result.hasChanges === true; return ( - + onViewDiff(result.resultId) : undefined} + > {result.environment.name} {result.resource.name} {result.agent.name} @@ -134,7 +120,7 @@ function ResultsTableRow({ - + );