From a114450c32578c90bbe84baf6437f8d939f207d6 Mon Sep 17 00:00:00 2001
From: Michael Aquilina <michaelaquilina@gmail.com>
Date: Fri, 5 Apr 2024 14:58:43 +0100
Subject: [PATCH] feat: Display preview for AI Fix

---
 CHANGELOG.md                                  |   2 +
 media/images/icon-external.svg                |   6 +-
 media/views/common/vscode.scss                |  19 +-
 .../views/snykCode/suggestion/suggestion.scss | 372 +++++++++++++--
 package-lock.json                             |  43 +-
 package.json                                  |   6 +-
 src/snyk/common/constants/commands.ts         |   1 +
 src/snyk/common/languageServer/types.ts       |   8 +
 src/snyk/common/services/learnService.ts      |   2 +-
 .../common/views/analysisTreeNodeProvider.ts  |  12 -
 src/snyk/common/views/issueTreeProvider.ts    |  46 +-
 src/snyk/snykCode/utils/patchUtils.ts         |  42 ++
 src/snyk/snykCode/views/issueTreeProvider.ts  |  20 +-
 .../views/securityIssueTreeProvider.ts        |   8 +-
 .../codeSuggestionWebviewProvider.ts          | 368 +++++++++++----
 .../suggestion/codeSuggestionWebviewScript.ts | 442 +++++++++++++++---
 src/snyk/snykCode/views/suggestion/types.ts   | 113 +++++
 .../snykCode/views/webviewPanelSerializer.ts  |  14 +
 .../providers/ossVulnerabilityTreeProvider.ts |   6 +-
 .../unit/common/services/learnService.test.ts |   1 +
 .../unit/snykCode/utils/patchUtils.test.ts    | 118 +++++
 21 files changed, 1413 insertions(+), 236 deletions(-)
 create mode 100644 src/snyk/snykCode/utils/patchUtils.ts
 create mode 100644 src/snyk/snykCode/views/suggestion/types.ts
 create mode 100644 src/snyk/snykCode/views/webviewPanelSerializer.ts
 create mode 100644 src/test/unit/snykCode/utils/patchUtils.test.ts

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 393f6714a..27aabce4b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,6 @@
 # Snyk Security Changelog
+## [2.6.0]
+- Added a preview for generated Code Fixes
 
 ## [2.4.1]
 - updated the language server protocol version to 11 to support global ignores
diff --git a/media/images/icon-external.svg b/media/images/icon-external.svg
index 59307674f..84ad7ed08 100644
--- a/media/images/icon-external.svg
+++ b/media/images/icon-external.svg
@@ -1,4 +1,4 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
-    <path d="M13 3L16.293 6.293 9.293 13.293 10.707 14.707 17.707 7.707 21 11 21 3z" fill="#7C74F2"/>
-    <path d="M19,19H5V5h7l-2-2H5C3.897,3,3,3.897,3,5v14c0,1.103,0.897,2,2,2h14c1.103,0,2-0.897,2-2v-5l-2-2V19z" fill="#7C74F2"/>
+<svg width="9" height="9" viewBox="0 0 9 9" xmlns="http://www.w3.org/2000/svg" fill="none">
+<path d="M4.99998 0L6.64648 1.6465L3.14648 5.1465L3.85348 5.8535L7.35348 2.3535L8.99998 4V0H4.99998Z" fill="#888"/>
+<path d="M8 8H1V1H4.5L3.5 0H1C0.4485 0 0 0.4485 0 1V8C0 8.5515 0.4485 9 1 9H8C8.5515 9 9 8.5515 9 8V5.5L8 4.5V8Z" fill="#888"/>
 </svg>
diff --git a/media/views/common/vscode.scss b/media/views/common/vscode.scss
index f8a4ba86c..cef44dac2 100644
--- a/media/views/common/vscode.scss
+++ b/media/views/common/vscode.scss
@@ -9,6 +9,8 @@
   --input-padding-horizontal: 4px;
   --input-margin-vertical: 4px;
   --input-margin-horizontal: 0;
+  --button-padding-horizontal: 16px;
+  --button-padding-vertical: 6px;
 }
 
 body {
@@ -25,8 +27,8 @@ ul {
   padding-left: var(--container-paddding);
 }
 
-body > *,
-form > * {
+body>*,
+form>* {
   margin-block-start: var(--input-margin-vertical);
   margin-block-end: var(--input-margin-vertical);
 }
@@ -35,8 +37,16 @@ form > * {
   outline-color: var(--vscode-focusBorder) !important;
 }
 
-a {
+p {
+  margin-top: 0;
+  margin-bottom: 2rem;
+}
+
+a,
+.link {
+  cursor: pointer;
   color: var(--vscode-textLink-foreground);
+  fill: currentColor;
 }
 
 a:hover,
@@ -51,8 +61,7 @@ code {
 
 button {
   border: none;
-  padding: var(--input-padding-vertical) var(--input-padding-horizontal);
-  width: 100%;
+  padding: var(--button-padding-vertical) var(--button-padding-horizontal);
   text-align: center;
   outline: 1px solid transparent;
   outline-offset: 2px !important;
diff --git a/media/views/snykCode/suggestion/suggestion.scss b/media/views/snykCode/suggestion/suggestion.scss
index 04555b5c8..16ce00b3f 100644
--- a/media/views/snykCode/suggestion/suggestion.scss
+++ b/media/views/snykCode/suggestion/suggestion.scss
@@ -1,6 +1,15 @@
 @import '../../common/variables';
 @import '../../common/webview';
 
+button,
+.button {
+  border-radius: 3px;
+}
+
+body {
+  height: 100%
+}
+
 .row {
   display: flex;
   flex-direction: row;
@@ -23,7 +32,7 @@
 
 .suggestion {
   position: relative;
-  display: flex;
+  display: inline-flex;
   flex-direction: column;
   width: 100%;
   height: 100%;
@@ -43,6 +52,39 @@
   line-height: 1.6;
 }
 
+
+.ai-fix {
+  padding-bottom: 0;
+}
+
+.ai-fix p {
+  margin-bottom: 0;
+}
+
+.sn-fix-wrapper {
+  padding: 2rem;
+  margin-top:1rem;
+  background-color: var(--vscode-editor-background);
+  border-radius: .8rem;
+  overflow: auto;
+}
+
+.generate-ai-fix {
+  width: auto;
+  padding: 8px 16px;
+}
+
+.low-opacity {
+  opacity: .5;
+  font-weight: 400;
+}
+
+.suggestion-details-content {
+  padding-bottom: 64px;
+}
+
+.sn-apply-fix {}
+
 .suggestion-details {
   height: auto;
   transition: height 400ms ease-out;
@@ -73,19 +115,6 @@
   padding-top: 0;
 }
 
-.read-more-btn {
-  background: none;
-  border: 2px solid var(--vscode-textLink-foreground);
-  color: var(--vscode-textLink-foreground);
-  cursor: pointer;
-  width: 130px;
-  border-radius: 3px;
-
-}.read-more-btn:hover, .read-more-btn:active  {
-  background: var(--vscode-textLink-foreground);
-  color: var(--vscode-button-foreground);
-}
-
 .cwe-list {
   display: inline-block;
   margin: 0;
@@ -153,12 +182,14 @@
   color: var(--vscode-icon-foreground);
 }
 
-.vscode-dark .removed, .vscode-high-contrast:not(.vscode-high-contrast-light) .removed {
+.vscode-dark .removed,
+.vscode-high-contrast:not(.vscode-high-contrast-light) .removed {
   background-color: #542426;
   color: #fff;
 }
 
-.vscode-dark .added, .vscode-high-contrast:not(.vscode-high-contrast-light) .added {
+.vscode-dark .added,
+.vscode-high-contrast:not(.vscode-high-contrast-light) .added {
   background-color: #1C4428;
   color: #fff;
 }
@@ -189,11 +220,11 @@
 }
 
 .arrow {
+  cursor: pointer;
   display: inline-block;
   width: 20px;
   height: 20px;
   padding: 4px;
-  cursor: pointer;
   border-radius: 4px;
   text-align: center;
   line-height: 1;
@@ -213,37 +244,38 @@
   text-align: center;
 }
 
-#example {
+.example {
   width: 100%;
   border: 1px solid var(--vscode-input-border);
   border-radius: 3px;
   line-height: 1.5;
   background-color: var(--vscode-editor-background);
+  margin-bottom: 1rem;
 }
 
-.example-line.removed {
+.code-line.removed {
   background-color: rgb(255, 215, 213);
 }
 
-.example-line.removed::before {
+.code-line.removed::before {
   content: "-";
   position: absolute;
   padding: 0 4px;
   line-height: 1;
 }
 
-.example-line.added {
+.code-line.added {
   background-color: rgb(204, 255, 216);
 }
 
-.example-line.added::before {
+.code-line.added::before {
   content: "+";
   position: absolute;
   padding: 0 4px;
   line-height: 1;
 }
 
-.example-line>code {
+.code-line>code {
   display: block;
   padding-left: 30px;
   white-space: pre-wrap;
@@ -288,12 +320,296 @@
   display: flex;
 }
 
-.actions .button {
-  margin: 0 0 2rem;
-  flex: 0 0 30%;
+.report-fp-actions {
+  margin-left: auto;
+}
+
+.tabs-nav {
+  margin: 21px 0 -21px;
+}
+
+.tab-item {
+  cursor: pointer;
+  display: inline-block;
+  padding: 5px 10px;
+  border-bottom: 1px solid transparent;
+  font-size: 1.1rem;
+  color: var(--vscode-foreground);
+  text-transform: uppercase;
+}
+
+.tab-item:hover {
+
+}
+
+.tab-item.is-selected {
+  border-bottom: 3px solid var(--vscode-focusBorder);
+}
+
+.tab-content {
+  display: none;
+}
+
+.tab-content.is-selected {
+  display: block;
+}
+
+.is-external {
+  padding-right: 12px;
+  background: url("../../../images/icon-external.svg") no-repeat top right;
+}
+
+.button.secondary {
+  cursor: pointer;
+  width: auto;
+  padding: 6px 16px;
+  border: 1px solid var(--vscode-textLink-foreground);
   border-radius: 3px;
+  background: none;
+  font-size: 1.4rem;
+  line-height: 1;
+  color: var(--vscode-textLink-foreground);
 }
 
-.report-fp-actions {
-  margin-left: auto;
+.button.secondary:hover,
+.button.secondary:active {
+  background: var(--vscode-button-hoverBackground);
+  border-color: var(--vscode-button-hoverBackground);
+  color: var(--vscode-button-foreground);
+}
+
+.sn-readmore {
+  margin-top: 1.6rem;
+}
+
+
+
+
+.sn-loading {
+  display: flex;
+}
+
+.sn-loading svg {
+  inline-size: 6rem;
+  block-size: auto
+}
+
+.sn-loading-wrapper {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+}
+
+.sn-loading-message {
+  opacity: 0;
+  position: absolute;
+  width: 100%;
+  padding-left: 16px;
+  margin-bottom: 8px;
+  font-size: 14px;
+}
+
+.sn-loading-title {
+  font-weight: 600;
+  line-height: 1.5;
+}
+
+.sn-loading-description {
+  margin-bottom: 0;
+  opacity: .75
+}
+
+
+.sn-msg-1 {
+  animation: reduce 4s ease-in;
+}
+
+.sn-msg-2 {
+  animation: reduce 4s ease-in;
+  animation-delay: 4s;
+}
+
+.sn-msg-3 {
+  animation: reduce 4s ease-in;
+  animation-delay: 8s;
+}
+
+.sn-msg-4 {
+  animation: inference 4s ease-in infinite;
+  animation-delay: 12s;
+}
+
+.suggestion-actions {
+  position: fixed;
+  bottom: 0;
+  width: 100%;
+  background-color: var(--vscode-editor-background);
+  background-image: linear-gradient(45deg, rgba(255,255,255,0.075), rgba(255,255,255,0.075));
+  box-shadow: 0 -1px 3px rgba(0,0,0,.05);
+}
+
+#s0 {
+  animation: s0ani 3000ms linear infinite;
+}
+
+#l1 {
+  animation: l1ani 3000ms linear infinite;
+}
+
+#l2 {
+  animation: l2ani 3000ms linear infinite;
+}
+
+#l3 {
+  animation: l3ani 3000ms linear infinite;
+}
+
+#b1 {
+  animation: b1ani 3000ms linear infinite;
+}
+
+#b2 {
+  animation: b2ani 3000ms linear infinite;
+}
+
+#b3 {
+  animation: b3ani 3000ms linear infinite;
+}
+
+@keyframes s0ani {
+  0% {
+    transform: translate(50%, -15%);
+  }
+
+  100% {
+    transform: translate(50%, 115%);
+  }
+}
+
+@keyframes l1ani {
+
+  0%,
+  23% {
+    fill: rgba(255, 255, 255, 0.2);
+  }
+
+  40%,
+  100% {
+    fill: rgba(249, 122, 153, 0.6);
+  }
+}
+
+@keyframes l2ani {
+
+  0%,
+  40% {
+    fill: rgba(255, 255, 255, 0.2);
+  }
+
+  56%,
+  100% {
+    fill: rgba(249, 122, 153, 0.6);
+  }
+}
+
+@keyframes l3ani {
+
+  0%,
+  56% {
+    fill: rgba(255, 255, 255, 0.2);
+  }
+
+  72%,
+  100% {
+    fill: rgba(67, 181, 154, 0.6);
+  }
+}
+
+@keyframes b1ani {
+
+  0%,
+  8% {
+    opacity: 0;
+    transform: scale(1, 1);
+  }
+
+  33% {
+    transform: translate(-10%, -18%) scale(1.6, 1.6);
+  }
+
+  53%,
+  100% {
+    opacity: 1;
+    transform: scale(1, 1);
+  }
+}
+
+@keyframes b2ani {
+
+  0%,
+  36% {
+    opacity: 0;
+    transform: scale(1, 1);
+  }
+
+  50% {
+    transform: translate(-20%, -18%) scale(1.4, 1.4);
+  }
+
+  60%,
+  100% {
+    opacity: 1;
+    transform: scale(1, 1);
+  }
+}
+
+@keyframes b3ani {
+
+  0%,
+  54% {
+    opacity: 0;
+    transform: scale(1, 1);
+  }
+
+  66% {
+    transform: translate(-10%, -27%) scale(1.4, 1.4);
+  }
+
+  76%,
+  100% {
+    opacity: 1;
+    transform: scale(1, 1);
+  }
+}
+
+
+@keyframes reduce {
+
+  15%,
+  85% {
+    opacity: 1
+  }
+
+  86%,
+  100%,
+  0% {
+    opacity: 0;
+  }
+}
+
+@keyframes inference {
+
+  0%,
+  25%,
+  100% {
+    opacity: 1
+  }
+}
+
+
+@media (max-width: 480px) {
+  .wide {
+    display: none;
+  }
 }
diff --git a/package-lock.json b/package-lock.json
index e526745d5..61a3bf0ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
         "@sentry/tracing": "^6.19.7",
         "@snyk/code-client": "^4.23.5",
         "axios": "^1.6.7",
+        "diff": "^5.2.0",
         "glob": "^9.3.5",
         "he": "^1.2.0",
         "htmlparser2": "^7.2.0",
@@ -34,6 +35,7 @@
       "devDependencies": {
         "@amplitude/ampli": "^1.29.0",
         "@types/babel__traverse": "^7.12.2",
+        "@types/diff": "^5.0.9",
         "@types/find-package-json": "^1.2.2",
         "@types/glob": "^8.1.0",
         "@types/he": "^1.2.3",
@@ -1689,6 +1691,12 @@
       "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
       "dev": true
     },
+    "node_modules/@types/diff": {
+      "version": "5.0.9",
+      "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.0.9.tgz",
+      "integrity": "sha512-RWVEhh/zGXpAVF/ZChwNnv7r4rvqzJ7lYNSmZSVTxjV0PBLf6Qu7RNg+SUtkpzxmiNkjCx0Xn2tPp7FIkshJwQ==",
+      "dev": true
+    },
     "node_modules/@types/find-package-json": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/@types/find-package-json/-/find-package-json-1.2.2.tgz",
@@ -3461,10 +3469,9 @@
       }
     },
     "node_modules/diff": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
-      "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
-      "dev": true,
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+      "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
       "engines": {
         "node": ">=0.3.1"
       }
@@ -6114,6 +6121,15 @@
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "dev": true
     },
+    "node_modules/mocha/node_modules/diff": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+      "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
     "node_modules/mocha/node_modules/escape-string-regexp": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -10287,6 +10303,12 @@
       "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==",
       "dev": true
     },
+    "@types/diff": {
+      "version": "5.0.9",
+      "resolved": "https://registry.npmjs.org/@types/diff/-/diff-5.0.9.tgz",
+      "integrity": "sha512-RWVEhh/zGXpAVF/ZChwNnv7r4rvqzJ7lYNSmZSVTxjV0PBLf6Qu7RNg+SUtkpzxmiNkjCx0Xn2tPp7FIkshJwQ==",
+      "dev": true
+    },
     "@types/find-package-json": {
       "version": "1.2.2",
       "resolved": "https://registry.npmjs.org/@types/find-package-json/-/find-package-json-1.2.2.tgz",
@@ -11614,10 +11636,9 @@
       "dev": true
     },
     "diff": {
-      "version": "5.0.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
-      "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
-      "dev": true
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
+      "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A=="
     },
     "dir-glob": {
       "version": "3.0.1",
@@ -13579,6 +13600,12 @@
           "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
           "dev": true
         },
+        "diff": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+          "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+          "dev": true
+        },
         "escape-string-regexp": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
diff --git a/package.json b/package.json
index 00222620d..56bc7339b 100644
--- a/package.json
+++ b/package.json
@@ -260,12 +260,12 @@
       },
       {
         "view": "snyk.views.welcome",
-        "contents": "Welcome to Snyk for Visual Studio Code. šŸ‘‹\nšŸ‘‰ Please wait, the extension is loading...",
+        "contents": "šŸ‘‹ Welcome to Snyk for Visual Studio Code. \nā±ļø Please wait, the extension is loading...",
         "when": "!snyk:error && !snyk:initialized"
       },
       {
         "view": "snyk.views.welcome",
-        "contents": "Welcome to Snyk for Visual Studio Code. šŸ‘‹\nšŸ‘‰ Connect with Snyk to start your first analysis!\nWhen scanning folder files, Snyk may automatically execute code such as invoking the package manager to get dependency information. You should only scan projects you trust. [More info](https://docs.snyk.io/ide-tools/visual-studio-code-extension/workspace-trust)\n[Trust workspace and connect](command:snyk.initiateLogin 'Connect with Snyk')\nBy connecting your account with Snyk, you agree to the Snyk [Privacy Policy](https://snyk.io/policies/privacy), and the Snyk [Terms of Service](https://snyk.io/policies/terms-of-service).",
+        "contents": "šŸ‘‹ Welcome to Snyk for Visual Studio Code. \nšŸ‘‰ Connect with Snyk to start your first analysis!\nWhen scanning folder files, Snyk may automatically execute code such as invoking the package manager to get dependency information. You should only scan projects you trust. [More info](https://docs.snyk.io/ide-tools/visual-studio-code-extension/workspace-trust)\n[Trust workspace and connect](command:snyk.initiateLogin 'Connect with Snyk')\nBy connecting your account with Snyk, you agree to the Snyk [Privacy Policy](https://snyk.io/policies/privacy), and the Snyk [Terms of Service](https://snyk.io/policies/terms-of-service).",
         "when": "!snyk:error && snyk:initialized && !snyk:loggedIn"
       },
       {
@@ -381,6 +381,7 @@
   "devDependencies": {
     "@amplitude/ampli": "^1.29.0",
     "@types/babel__traverse": "^7.12.2",
+    "@types/diff": "^5.0.9",
     "@types/find-package-json": "^1.2.2",
     "@types/glob": "^8.1.0",
     "@types/he": "^1.2.3",
@@ -420,6 +421,7 @@
     "@sentry/tracing": "^6.19.7",
     "@snyk/code-client": "^4.23.5",
     "axios": "^1.6.7",
+    "diff": "^5.2.0",
     "glob": "^9.3.5",
     "he": "^1.2.0",
     "htmlparser2": "^7.2.0",
diff --git a/src/snyk/common/constants/commands.ts b/src/snyk/common/constants/commands.ts
index 7ca96feff..ee3420077 100644
--- a/src/snyk/common/constants/commands.ts
+++ b/src/snyk/common/constants/commands.ts
@@ -23,6 +23,7 @@ export const SNYK_LOGIN_COMMAND = 'snyk.login';
 export const SNYK_WORKSPACE_SCAN_COMMAND = 'snyk.workspace.scan';
 export const SNYK_TRUST_WORKSPACE_FOLDERS_COMMAND = 'snyk.trustWorkspaceFolders';
 export const SNYK_GET_ACTIVE_USER = 'snyk.getActiveUser';
+export const SNYK_CODE_FIX_DIFFS_COMMAND = 'snyk.code.fixDiffs';
 
 // custom Snyk constants used in commands
 export const SNYK_CONTEXT_PREFIX = 'snyk:';
diff --git a/src/snyk/common/languageServer/types.ts b/src/snyk/common/languageServer/types.ts
index e598be7c9..ea5219a0e 100644
--- a/src/snyk/common/languageServer/types.ts
+++ b/src/snyk/common/languageServer/types.ts
@@ -4,6 +4,8 @@ export enum ScanProduct {
   InfrastructureAsCode = 'iac',
 }
 
+export type InProgress = 'inProgress';
+
 export enum ScanStatus {
   InProgress = 'inProgress',
   Success = 'success',
@@ -48,6 +50,7 @@ export type CodeIssueData = {
   rows: Point;
   isSecurityType: boolean;
   priorityScore: number;
+  hasAIFix: boolean;
 };
 
 export type ExampleCommitFix = {
@@ -115,3 +118,8 @@ export type IacIssueData = {
   resolve?: string;
   references?: string[];
 };
+
+export type AutofixUnifiedDiffSuggestion = {
+  fixId: string;
+  unifiedDiffsPerFile: { [key: string]: string };
+};
diff --git a/src/snyk/common/services/learnService.ts b/src/snyk/common/services/learnService.ts
index 259bd9bec..fde6fd070 100644
--- a/src/snyk/common/services/learnService.ts
+++ b/src/snyk/common/services/learnService.ts
@@ -2,7 +2,7 @@ import { SNYK_GET_LESSON_COMMAND } from '../constants/commands';
 import { CodeIssueData, Issue } from '../languageServer/types';
 import { IVSCodeCommands } from '../vscode/commands';
 
-type Lesson = {
+export type Lesson = {
   url: string;
   title: string;
 };
diff --git a/src/snyk/common/views/analysisTreeNodeProvider.ts b/src/snyk/common/views/analysisTreeNodeProvider.ts
index 990602efa..8cea9f592 100644
--- a/src/snyk/common/views/analysisTreeNodeProvider.ts
+++ b/src/snyk/common/views/analysisTreeNodeProvider.ts
@@ -36,16 +36,6 @@ export abstract class AnalysisTreeNodeProvider extends TreeNodeProvider {
     return 0;
   };
 
-  protected getDurationTreeNode(): TreeNode {
-    const ts = new Date(this.statusProvider.lastAnalysisTimestamp);
-    const time = ts.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
-    const day = ts.toLocaleDateString([], { year: '2-digit', month: '2-digit', day: '2-digit' });
-
-    return new TreeNode({
-      text: messages.duration(time, day),
-    });
-  }
-
   protected getNoSeverityFiltersSelectedTreeNode(): TreeNode | null {
     const anyFilterEnabled = Object.values<boolean>(this.configuration.severityFilter).find(enabled => !!enabled);
     if (anyFilterEnabled) {
@@ -81,6 +71,4 @@ export abstract class AnalysisTreeNodeProvider extends TreeNodeProvider {
       },
     });
   }
-
-  protected abstract getFilteredIssues(issues: readonly unknown[]): readonly unknown[];
 }
diff --git a/src/snyk/common/views/issueTreeProvider.ts b/src/snyk/common/views/issueTreeProvider.ts
index b55fe4b01..d81bc3b10 100644
--- a/src/snyk/common/views/issueTreeProvider.ts
+++ b/src/snyk/common/views/issueTreeProvider.ts
@@ -1,4 +1,4 @@
-import _ from 'lodash';
+import _, { flatten } from 'lodash';
 import * as vscode from 'vscode'; // todo: invert dependency
 import { IConfiguration } from '../../common/configuration/configuration';
 import { Issue, IssueSeverity } from '../../common/languageServer/types';
@@ -75,8 +75,7 @@ export abstract class ProductIssueTreeProvider<T> extends AnalysisTreeNodeProvid
       ];
     }
 
-    const [resultNodes, nIssues] = this.getResultNodes();
-    nodes.push(...resultNodes);
+    nodes.push(...this.getResultNodes());
 
     const folderResults = Array.from(this.productService.result.values());
     const allFailed = folderResults.every(folderResult => folderResult instanceof Error);
@@ -86,20 +85,42 @@ export abstract class ProductIssueTreeProvider<T> extends AnalysisTreeNodeProvid
 
     nodes.sort(this.compareNodes);
 
-    const topNodes = [
+    const topNodes: (TreeNode | null)[] = [
       new TreeNode({
-        text: this.getIssueFoundText(nIssues),
+        text: this.getIssueFoundText(this.getTotalIssueCount()),
       }),
-      this.getDurationTreeNode(),
+      this.getFixableIssuesNode(this.getFixableCount()),
       this.getNoSeverityFiltersSelectedTreeNode(),
     ];
+
     nodes.unshift(...topNodes.filter((n): n is TreeNode => n !== null));
     return nodes;
   }
 
-  getResultNodes(): [TreeNode[], number] {
+  getFixableIssuesNode(_fixableIssueCount: number): TreeNode | null {
+    return null; // optionally overridden by products
+  }
+
+  getFilteredIssues(): Issue<T>[] {
+    const folderResults = Array.from(this.productService.result.values());
+    const successfulResults = flatten(folderResults.filter((result): result is Issue<T>[] => Array.isArray(result)));
+    return this.filterIssues(successfulResults);
+  }
+
+  getTotalIssueCount(): number {
+    return this.getFilteredIssues().length;
+  }
+
+  getFixableCount(): number {
+    return this.getFilteredIssues().filter(issue => this.isFixableIssue(issue)).length;
+  }
+
+  isFixableIssue(_issue: Issue<T>) {
+    return false; // optionally overridden by products
+  }
+
+  getResultNodes(): TreeNode[] {
     const nodes: TreeNode[] = [];
-    let totalVulnCount = 0;
 
     for (const result of this.productService.result.entries()) {
       const folderPath = result[0];
@@ -133,7 +154,6 @@ export abstract class ProductIssueTreeProvider<T> extends AnalysisTreeNodeProvid
 
         const issueNodes = filteredIssues.map(issue => {
           fileSeverityCounts[issue.severity] += 1;
-          totalVulnCount++;
           folderVulnCount++;
 
           const issueRange = this.getIssueRange(issue);
@@ -209,7 +229,7 @@ export abstract class ProductIssueTreeProvider<T> extends AnalysisTreeNodeProvid
       }
     }
 
-    return [nodes, totalVulnCount];
+    return nodes;
   }
 
   protected getIssueFoundText(nIssues: number): string {
@@ -220,12 +240,6 @@ export abstract class ProductIssueTreeProvider<T> extends AnalysisTreeNodeProvid
     return `${dir} - ${issueCount} issue${issueCount === 1 ? '' : 's'}`;
   }
 
-  // todo: Obsolete. Remove after OSS scans migration to LS
-  protected getFilteredIssues(diagnostics: readonly unknown[]): readonly unknown[] {
-    // Diagnostics are already filtered by the analyzer
-    return diagnostics;
-  }
-
   static getHighestSeverity(counts: ISeverityCounts): IssueSeverity {
     for (const s of [IssueSeverity.Critical, IssueSeverity.High, IssueSeverity.Medium, IssueSeverity.Low]) {
       if (counts[s]) return s;
diff --git a/src/snyk/snykCode/utils/patchUtils.ts b/src/snyk/snykCode/utils/patchUtils.ts
new file mode 100644
index 000000000..e798f54e4
--- /dev/null
+++ b/src/snyk/snykCode/utils/patchUtils.ts
@@ -0,0 +1,42 @@
+import { IVSCodeLanguages } from '../../common/vscode/languages';
+import { DecorationOptions } from '../../common/vscode/types';
+
+// Supports Unified Diff Format
+export function generateDecorationOptions(patch: string, languages: IVSCodeLanguages): DecorationOptions[] {
+  const codeLines = patch.split('\n');
+
+  // the first two lines are the file names
+  codeLines.shift();
+  codeLines.shift();
+
+  const decorationOptions: DecorationOptions[] = [];
+  let currentLine = -1;
+
+  for (const line of codeLines) {
+    if (line.startsWith('@@ ')) {
+      // format is -original, +new
+      // @@ -start,count +start,count @@
+      // counts are considered optional
+      // we only care about the start line for the new file
+      const [, , added] = line.split(' ');
+      const [startLineValue] = added.split(',');
+
+      // unified diff line numbers start from 1 not 0
+      // vscode.Range starts from 0 not 1
+      currentLine = parseInt(startLineValue) - 1;
+    } else {
+      if (line.startsWith('+')) {
+        const range = languages.createRange(currentLine, 0, currentLine, line.length - 1);
+
+        decorationOptions.push({ range });
+        currentLine++;
+      } else if (line.startsWith('-')) {
+        continue;
+      } else {
+        currentLine++;
+      }
+    }
+  }
+
+  return decorationOptions;
+}
diff --git a/src/snyk/snykCode/views/issueTreeProvider.ts b/src/snyk/snykCode/views/issueTreeProvider.ts
index 8ccd9662d..d8b91ef84 100644
--- a/src/snyk/snykCode/views/issueTreeProvider.ts
+++ b/src/snyk/snykCode/views/issueTreeProvider.ts
@@ -10,6 +10,7 @@ import { IVSCodeLanguages } from '../../common/vscode/languages';
 import { messages } from '../messages/analysis';
 import { IssueUtils } from '../utils/issueUtils';
 import { CodeIssueCommandArg } from './interfaces';
+import { TreeNode } from '../../common/views/treeNode';
 
 export class IssueTreeProvider extends ProductIssueTreeProvider<CodeIssueData> {
   constructor(
@@ -34,6 +35,7 @@ export class IssueTreeProvider extends ProductIssueTreeProvider<CodeIssueData> {
 
   // The title in the tree is taken from the title for vulnerabilities and from the message for quality rules
   getIssueTitle(issue: Issue<CodeIssueData>): string {
+    const fixIcon = issue.additionalData.hasAIFix ? 'āš”ļø' : '';
     const issueTitle = issue.additionalData.isSecurityType
       ? issue.title.split(':')[0]
       : issue.additionalData.message.split('.')[0];
@@ -43,7 +45,7 @@ export class IssueTreeProvider extends ProductIssueTreeProvider<CodeIssueData> {
       prefixIgnored = '[ Ignored ] ';
     }
 
-    return prefixIgnored + issueTitle;
+    return fixIcon + prefixIgnored + issueTitle;
   }
 
   getIssueRange(issue: Issue<CodeIssueData>): Range {
@@ -67,4 +69,20 @@ export class IssueTreeProvider extends ProductIssueTreeProvider<CodeIssueData> {
       ],
     };
   }
+
+  isFixableIssue(issue: Issue<CodeIssueData>): boolean {
+    return issue.additionalData.hasAIFix;
+  }
+
+  getFixableIssuesNode(fixableIssueCount: number): TreeNode {
+    return new TreeNode({
+      text: this.getAIFixableIssuesText(fixableIssueCount),
+    });
+  }
+
+  private getAIFixableIssuesText(issuesCount: number): string {
+    return issuesCount > 0
+      ? `āš”ļø ${issuesCount} ${issuesCount === 1 ? 'vulnerability' : 'vulnerabilities'} can be fixed by Snyk DeepCode AI`
+      : 'There are no vulnerabilities fixable by Snyk DeepCode AI';
+  }
 }
diff --git a/src/snyk/snykCode/views/securityIssueTreeProvider.ts b/src/snyk/snykCode/views/securityIssueTreeProvider.ts
index cfec68524..89ed80ca8 100644
--- a/src/snyk/snykCode/views/securityIssueTreeProvider.ts
+++ b/src/snyk/snykCode/views/securityIssueTreeProvider.ts
@@ -39,8 +39,10 @@ export default class CodeSecurityIssueTreeProvider extends IssueTreeProvider {
   }
 
   protected getIssueFoundText(nIssues: number): string {
-    return `Snyk found ${
-      !nIssues ? 'no vulnerabilities! āœ…' : `${nIssues} ${nIssues === 1 ? 'vulnerability' : 'vulnerabilities'}`
-    }`;
+    if (nIssues > 0) {
+      return nIssues === 1 ? `${nIssues} vulnerability found by Snyk` : `āœ‹ ${nIssues} vulnerabilities found by Snyk`;
+    } else {
+      return 'āœ… Congrats! No vulnerabilities found!';
+    }
   }
 }
diff --git a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts
index bf500c4ef..7c15ca602 100644
--- a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts
+++ b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewProvider.ts
@@ -1,19 +1,21 @@
 import _ from 'lodash';
+import { relative } from 'path';
+import { applyPatch } from 'diff';
 import { marked } from 'marked';
 import * as vscode from 'vscode';
 import {
+  SNYK_CODE_FIX_DIFFS_COMMAND,
   SNYK_IGNORE_ISSUE_COMMAND,
   SNYK_OPEN_BROWSER_COMMAND,
   SNYK_OPEN_LOCAL_COMMAND,
 } from '../../../common/constants/commands';
 import { SNYK_VIEW_SUGGESTION_CODE } from '../../../common/constants/views';
 import { ErrorHandler } from '../../../common/error/errorHandler';
-import { CodeIssueData, ExampleCommitFix, Issue, Marker, Point } from '../../../common/languageServer/types';
+import { AutofixUnifiedDiffSuggestion, CodeIssueData, Issue } from '../../../common/languageServer/types';
 import { ILog } from '../../../common/logger/interfaces';
 import { messages as learnMessages } from '../../../common/messages/learn';
 import { LearnService } from '../../../common/services/learnService';
 import { getNonce } from '../../../common/views/nonce';
-import { WebviewPanelSerializer } from '../../../common/views/webviewPanelSerializer';
 import { WebviewProvider } from '../../../common/views/webviewProvider';
 import { ExtensionContext } from '../../../common/vscode/extensionContext';
 import { IVSCodeLanguages } from '../../../common/vscode/languages';
@@ -23,26 +25,13 @@ import { WEBVIEW_PANEL_QUALITY_TITLE, WEBVIEW_PANEL_SECURITY_TITLE } from '../..
 import { messages as errorMessages } from '../../messages/error';
 import { getAbsoluteMarkerFilePath } from '../../utils/analysisUtils';
 import { encodeExampleCommitFixes } from '../../utils/htmlEncoder';
+import { generateDecorationOptions } from '../../utils/patchUtils';
 import { IssueUtils } from '../../utils/issueUtils';
 import { ICodeSuggestionWebviewProvider } from '../interfaces';
-
-type Suggestion = {
-  id: string;
-  message: string;
-  severity: string;
-  leadURL?: string;
-  rule: string;
-  repoDatasetSize: number;
-  exampleCommitFixes: ExampleCommitFix[];
-  cwe: string[];
-  title: string;
-  text: string;
-  isSecurityType: boolean;
-  uri: string;
-  markers?: Marker[];
-  cols: Point;
-  rows: Point;
-};
+import { readFileSync } from 'fs';
+import { TextDocument } from '../../../common/vscode/types';
+import { Suggestion, SuggestionMessage } from './types';
+import { WebviewPanelSerializer } from '../../../snykCode/views/webviewPanelSerializer';
 
 export class CodeSuggestionWebviewProvider
   extends WebviewProvider<Issue<CodeIssueData>>
@@ -73,17 +62,21 @@ export class CodeSuggestionWebviewProvider
     return this.issue?.id;
   }
 
+  private async postSuggestMessage(message: SuggestionMessage): Promise<void> {
+    await this.panel?.webview.postMessage(message);
+  }
+
   async postLearnLessonMessage(issue: Issue<CodeIssueData>): Promise<void> {
     try {
       if (this.panel) {
         const lesson = await this.learnService.getCodeLesson(issue);
         if (lesson) {
-          void this.panel.webview.postMessage({
+          void this.postSuggestMessage({
             type: 'setLesson',
             args: { url: lesson.url, title: learnMessages.lessonButtonTitle },
           });
         } else {
-          void this.panel.webview.postMessage({
+          void this.postSuggestMessage({
             type: 'setLesson',
             args: null,
           });
@@ -124,7 +117,7 @@ export class CodeSuggestionWebviewProvider
         'snyk-code.svg',
       );
 
-      void this.panel.webview.postMessage({ type: 'set', args: this.mapToModel(issue) });
+      void this.postSuggestMessage({ type: 'set', args: this.mapToModel(issue) });
       void this.postLearnLessonMessage(issue);
 
       this.issue = issue;
@@ -139,7 +132,11 @@ export class CodeSuggestionWebviewProvider
     this.panel.onDidDispose(() => this.onPanelDispose(), null, this.disposables);
     this.panel.onDidChangeViewState(() => this.checkVisibility(), undefined, this.disposables);
     // Handle messages from the webview
-    this.panel.webview.onDidReceiveMessage(msg => this.handleMessage(msg), undefined, this.disposables);
+    this.panel.webview.onDidReceiveMessage(
+      (msg: SuggestionMessage) => this.handleMessage(msg),
+      undefined,
+      this.disposables,
+    );
   }
 
   disposePanel(): void {
@@ -150,60 +147,126 @@ export class CodeSuggestionWebviewProvider
     super.onPanelDispose();
   }
 
+  private getWorkspaceFolderPath(filePath: string) {
+    // get the workspace folders
+    // look at the filepath and identify the folder that contains the filepath
+    for (const folderPath of this.workspace.getWorkspaceFolders()) {
+      if (filePath.startsWith(folderPath)) {
+        return folderPath;
+      }
+    }
+    throw new Error(`Unable to find workspace for: ${filePath}`);
+  }
+
   private mapToModel(issue: Issue<CodeIssueData>): Suggestion {
     const parsedDetails = marked.parse(issue.additionalData.text) as string;
+
     return {
       id: issue.id,
       title: issue.title,
-      uri: issue.filePath,
       severity: _.capitalize(issue.severity),
       ...issue.additionalData,
       text: parsedDetails,
+      hasAIFix: issue.additionalData.hasAIFix,
+      filePath: issue.filePath,
     };
   }
 
-  private async handleMessage(message: any) {
+  private async handleMessage(message: SuggestionMessage) {
     try {
-      const { type, args } = message;
-      switch (type) {
+      switch (message.type) {
         case 'openLocal': {
-          const { uri, cols, rows, suggestionUri } = args as {
-            uri: string;
-            cols: [number, number];
-            rows: [number, number];
-            suggestionUri: string;
-          };
+          const { uri, cols, rows, suggestionUri } = message.args;
           const localUriPath = getAbsoluteMarkerFilePath(this.workspace, uri, suggestionUri);
           const localUri = vscode.Uri.file(localUriPath);
           const range = IssueUtils.createVsCodeRangeFromRange(rows, cols, this.languages);
           await vscode.commands.executeCommand(SNYK_OPEN_LOCAL_COMMAND, localUri, range);
           break;
         }
+
         case 'openBrowser': {
-          const { url } = args as { url: string };
+          const { url } = message.args;
           await vscode.commands.executeCommand(SNYK_OPEN_BROWSER_COMMAND, url);
           break;
         }
+
         case 'ignoreIssue': {
-          const { lineOnly, message, rule, uri, cols, rows } = args as {
-            lineOnly: boolean;
-            message: string;
-            rule: string;
-            uri: string;
-            cols: [number, number];
-            rows: [number, number];
-          };
+          const { lineOnly, rule, uri, cols, rows } = message.args;
           const vscodeUri = vscode.Uri.file(uri);
           const range = IssueUtils.createVsCodeRangeFromRange(rows, cols, this.languages);
           await vscode.commands.executeCommand(SNYK_IGNORE_ISSUE_COMMAND, {
             uri: vscodeUri,
-            matchedIssue: { message, range },
+            matchedIssue: {
+              message: message.args.message,
+              range,
+            },
             ruleId: rule,
             isFileIgnore: !lineOnly,
           });
           this.panel?.dispose();
           break;
         }
+
+        case 'getAutofixDiffs': {
+          this.logger.info('Generating fixes');
+
+          const { suggestion } = message.args;
+          try {
+            const filePath = suggestion.filePath;
+            const folderPath = this.getWorkspaceFolderPath(filePath);
+            const relativePath = relative(folderPath, filePath);
+
+            const issueId = suggestion.id;
+
+            const diffs: AutofixUnifiedDiffSuggestion[] = await vscode.commands.executeCommand(
+              SNYK_CODE_FIX_DIFFS_COMMAND,
+              folderPath,
+              relativePath,
+              issueId,
+            );
+            if (diffs.length === 0) {
+              throw Error('Snyk has not been able to generate a relevant fix');
+            }
+
+            void this.postSuggestMessage({ type: 'setAutofixDiffs', args: { suggestion, diffs } });
+          } catch (error) {
+            void this.postSuggestMessage({ type: 'setAutofixError', args: { suggestion } });
+          }
+
+          break;
+        }
+
+        case 'applyGitDiff': {
+          const { patch, filePath } = message.args;
+
+          const fileContent = readFileSync(filePath, 'utf8');
+          const patchedContent = applyPatch(fileContent, patch);
+
+          if (!patchedContent) {
+            throw Error('Failed to apply patch');
+          }
+          const edit = new vscode.WorkspaceEdit();
+
+          const editor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.path === filePath);
+
+          if (!editor) {
+            throw Error(`Editor with file not found: ${filePath}`);
+          }
+
+          const editorEndLine = editor.document.lineCount;
+          edit.replace(vscode.Uri.parse(filePath), new vscode.Range(0, 0, editorEndLine, 0), patchedContent);
+
+          const success = await vscode.workspace.applyEdit(edit);
+          if (!success) {
+            throw Error('Failed to apply edit to workspace');
+          }
+
+          this.highlightAddedCode(filePath, patch);
+          this.setupCloseOnSave(filePath);
+
+          break;
+        }
+
         default: {
           throw new Error('Unknown message type');
         }
@@ -213,6 +276,65 @@ export class CodeSuggestionWebviewProvider
     }
   }
 
+  private setupCloseOnSave(filePath: string) {
+    vscode.workspace.onDidSaveTextDocument((e: TextDocument) => {
+      if (e.uri.path == filePath) {
+        this.panel?.dispose();
+      }
+    });
+  }
+
+  private highlightAddedCode(filePath: string, diffData: string) {
+    const highlightDecoration = vscode.window.createTextEditorDecorationType({
+      // seems to work well with both dark and light backgrounds
+      backgroundColor: 'rgba(0,255,0,0.3)',
+    });
+
+    const editor = vscode.window.visibleTextEditors.find(editor => editor.document.uri.path === filePath);
+    if (!editor) {
+      return; // No open editor found with the target file
+    }
+
+    const decorationOptions = generateDecorationOptions(diffData, this.languages);
+    if (decorationOptions.length === 0) {
+      return;
+    }
+
+    editor.setDecorations(highlightDecoration, decorationOptions);
+
+    const firstLine = decorationOptions[0].range.start.line;
+
+    // scroll to first added line
+    const line = editor.document.lineAt(firstLine);
+    const range = line.range;
+    editor.revealRange(range, vscode.TextEditorRevealType.AtTop);
+
+    // remove highlight on any of:
+    // - user types
+    // - saves the doc
+    // - after an amount of time
+
+    const removeHighlights = () => {
+      editor.setDecorations(highlightDecoration, []);
+      listeners.forEach(listener => {
+        if (listener instanceof vscode.Disposable) listener.dispose();
+        else clearTimeout(listener);
+      });
+    };
+
+    const documentEventHandler = (document: TextDocument) => {
+      if (document.uri.path == filePath) {
+        removeHighlights();
+      }
+    };
+
+    const listeners = [
+      setTimeout(removeHighlights, 30000),
+      vscode.workspace.onDidSaveTextDocument(documentEventHandler),
+      vscode.workspace.onDidChangeTextDocument(e => documentEventHandler(e.document)),
+    ];
+  }
+
   private getTitle(issue: Issue<CodeIssueData>): string {
     return issue.additionalData.isSecurityType ? WEBVIEW_PANEL_SECURITY_TITLE : WEBVIEW_PANEL_QUALITY_TITLE;
   }
@@ -279,58 +401,132 @@ export class CodeSuggestionWebviewProvider
 
           <div class="learn learn__code">
             <img class="icon" src="${images['learn-icon']}" />
-            <a class="learn--link"></a>
+            <a class="learn--link is-external"></a>
           </div>
-        </section>
-        <section id="suggestion-info" class="delimiter-top">
-          <div id="description" class="suggestion-text"></div>
-          <div class="suggestion-links">
-            <div id="lead-url" class="clickable hidden">
-              <img class="icon" src="${images['icon-external']}" /> More info
-            </div>
+
+          <div class="tabs-nav">
+            <span id='fix-analysis-tab' class="tab-item is-selected sn-fix-analysis">Fix Analysis</span>
+            <span id='vuln-overview-tab' class="tab-item sn-vuln-overview">Vulnerability Overview</span>
           </div>
         </section>
 
-        <section class="delimiter-top suggestion-details-content">
-          <div id="suggestion-details" class="suggestion-details"></div>
-        </section>
-        <section class="actions row no-padding-top">
-          <button class="read-more-btn">Read more</button>
-        </section>
+        <div id='fix-analysis-content' class="tab-content is-selected sn-fix-content">
+          <section id="suggestion-info" class="delimiter-top">
+            <div id="description" class="suggestion-text"></div>
+            <div class="suggestion-links">
+              <div id="lead-url" class="clickable hidden">
+                <img class="icon" src="${images['icon-external']}" /> More info
+              </div>
+            </div>
+          </section>
 
-        <section class="delimiter-top">
-          <div id="info-top" class="font-light">
-            This <span class="issue-type">issue</span> was fixed by <span id="dataset-number"></span> projects. Here are <span id="example-number"></span> examples:
-          </div>
-          <div id="info-no-examples" class="font-light">
-            There are no fix examples for this issue.
+          <section id="fix-wrapper" class="ai-fix">
+            <p>āš” Fix this issue by generating a solution using Snyk DeepCode AI</p>
+
+            <div class="sn-fix-wrapper">
+              <button id="generate-ai-fix" class="generate-ai-fix">āœØ Generate fix <span class="wide">using Snyk DeepCode AI</span></button>
+
+              <div id="fix-loading-indicator" class="sn-loading hidden">
+                <div class="sn-loading-icon">
+                  <svg id="scan-animation" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 248 204" shape-rendering="geometricPrecision"><defs><linearGradient id="mg" x1="16.0903" y1="180" x2="92.743" y2="107.462" spreadMethod="pad" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 0)"><stop id="eQeHIUZsTfX2-fill-0" offset="0%" stop-color="#145deb"/><stop id="eQeHIUZsTfX2-fill-1" offset="100%" stop-color="#441c99"/></linearGradient><linearGradient id="sg" x1="116" y1="0" x2="116" y2="64" spreadMethod="pad" gradientUnits="userSpaceOnUse" gradientTransform="translate(0 0)"><stop id="eQeHIUZsTfX26-fill-0" offset="0%" stop-color="#ff78e1"/><stop id="eQeHIUZsTfX26-fill-1" offset="100%" stop-color="rgba(255,120,225,0)"/></linearGradient></defs><rect width="224" height="180" rx="16" ry="16" transform="translate(12 12)" fill="url(#mg)"/><circle r="4" transform="translate(28 28)" opacity="0.3" fill="#fff"/><circle r="4" transform="translate(40 28)" opacity="0.25" fill="#fff"/><circle r="4" transform="translate(52 28)" opacity="0.2" fill="#fff"/><rect width="48" height="12" rx="6" ry="6" transform="translate(162 56)" opacity="0.2" fill="#fff"/><rect width="80" height="12" rx="6" ry="6" transform="translate(32 92)" opacity="0.2" fill="#fff"/><rect width="72" height="12" rx="6" ry="6" transform="translate(96 164)" opacity="0.2" fill="#fff"/><rect width="56" height="12" rx="6" ry="6" transform="translate(156 128)" opacity="0.2" fill="#fff"/><rect id="l3" width="80" height="12" rx="6" ry="6" transform="translate(64 128)"/><rect id="l2" width="64" height="12" rx="6" ry="6" transform="translate(150 92)"/><rect id="l1" width="117" height="12" rx="6" ry="6" transform="translate(32 56)"/><g id="b3"><rect width="32" height="32" rx="6" ry="6" transform="translate(48 118)" fill="#43b59a"/><path d="M54.5991,134c.7987-.816,2.0938-.816,2.8926,0l2.8926,2.955l10.124-10.343c.7988-.816,2.0939-.816,2.8926,0c.7988.816.7988,2.139,0,2.955L61.8306,141.388c-.7988.816-2.0939.816-2.8926,0l-4.3389-4.433c-.7988-.816-.7988-2.139,0-2.955Z" fill="#fff"/></g><g id="b2"><rect width="32" height="32" rx="6" ry="6" transform="translate(124 81)" fill="#f97a99"/><path d="M142,91c0,.7685-.433,5.3087-1.069,8h-1.862c-.636-2.6913-1.069-7.2315-1.069-8c0-1.1046.895-2,2-2s2,.8954,2,2Z" fill="#fff"/><path d="M140,104c1.105,0,2-.895,2-2s-.895-2-2-2-2,.895-2,2s.895,2,2,2Z" fill="#fff"/></g><g id="b1"><rect width="24" height="24" rx="6" ry="6" transform="translate(28 50)" fill="#f97a99"/><path d="M42,56c0,.7685-.4335,5.3087-1.0693,8h-1.8614C38.4335,61.3087,38,56.7685,38,56c0-1.1046.8954-2,2-2s2,.8954,2,2Z" fill="#fff"/><path d="M40,69c1.1046,0,2-.8954,2-2s-.8954-2-2-2-2,.8954-2,2s.8954,2,2,2Z" fill="#fff"/></g><g id="s0" transform="translate(124,-40)"><g transform="translate(-124,-40)"><rect width="232" height="64" rx="0" ry="0" transform="matrix(1 0 0-1 8 64)" opacity="0.5" fill="url(#sg)"/><rect width="248" height="16" rx="8" ry="8" transform="translate(0 64)" fill="#e555ac"/></g></g></svg>
+                </div>
+                <div class="sn-loading-wrapper">
+                  <div class="sn-loading-message sn-msg-1">
+                    <span class="sn-loading-title">1<span class="font-light">/4</span> Code Reduction...</span>
+                    <p class="sn-loading-description">Reduces the given files to a smaller and relevant code snippet.</p>
+                  </div>
+                  <div class="sn-loading-message sn-msg-2">
+                    <span class="sn-loading-title">2<span class="font-light">/4</span> Parsing...</span>
+                    <p class="sn-loading-description">Analyzing symbols and generating the graph representation.</p>
+                  </div>
+                  <div class="sn-loading-message sn-msg-3">
+                    <span class="sn-loading-title">3<span class="font-light">/4</span> Static Analysis...</span>
+                    <p class="sn-loading-description">Examining the vulnerability code without having to execute the program.</p>
+                  </div>
+                  <div class="sn-loading-message sn-msg-4">
+                    <span class="sn-loading-title">4<span class="font-light">/4</span> Inferencing...</span>
+                    <p class="sn-loading-description">Feeding the reduced code to our neural network to obtain the prediction.</p>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </section>
+
+          <section id="fixes-section" class="sn-ai-fixes hidden">
+          <div id="info-no-diffs" class="font-light">
+          There are no fix diffs for this issue.
           </div>
-          <div id="example-top" class="row between">
-            <div id="current-example" class="repo clickable">
-              <img class="repo-icon icon" src="${images['icon-github']}"></img>
-              <span id="example-link" class="repo-link"></span>
+          <div id="diff-top" class="row between">
+              <div id="info-top" class="font-light">Here are <span id="diff-number"></span> AI-generated solutions:</div>
+              <div class="diffs-nav">
+                <span id="previous-diff" class="arrow" title="Previous diff">
+                  <img src=${images['arrow-left-dark']} class="arrow-icon dark-only"></img>
+                  <img src=${images['arrow-left-light']} class="arrow-icon light-only"></img>
+                </span>
+                <span id="diff-text">
+                  AI solution <strong id="diff-counter">1</strong>/<span id="diff-number2"></span>
+                </span>
+                <span id="next-diff" class="arrow" title="Next diff">
+                  <img src=${images['arrow-right-dark']} class="arrow-icon dark-only"></img>
+                  <img src=${images['arrow-right-light']} class="arrow-icon light-only"></img>
+                </span>
+              </div>
             </div>
-            <div class="examples-nav">
-              <span id="previous-example" class="arrow" title="Previous example">
-                <img src=${images['arrow-left-dark']} class="arrow-icon dark-only"></img>
-                <img src=${images['arrow-left-light']} class="arrow-icon light-only"></img>
-              </span>
-              <span id="example-text">
-                Example <strong id="example-counter">1</strong>/<span id="example-number2"></span>
-              </span>
-              <span id="next-example" class="arrow" title="Next example">
-                <img src=${images['arrow-right-dark']} class="arrow-icon dark-only"></img>
-                <img src=${images['arrow-right-light']} class="arrow-icon light-only"></img>
-              </span>
+            <div id="diff"></div>
+            <button id="apply-fix" class="button sn-apply-fix">Apply fix</button>
+          </section>
+
+          <section id="fixes-error-section" class="sn-ai-fix-error hidden">
+            <div id="info-no-diffs" class="font-light">
+            āš ļø There was an issue when generating the fix.
             </div>
-          </div>
-          <div id="example"></div>
-        </section>
-        <section class="delimiter-top">
+            <div class="sn-fix-wrapper">
+              <button id="retry-generate-fix" class="button generate-ai-fix">āœØ Retry generate AI fixes</button>
+            </div>
+          </section>
+
+          <section class="sn-community-fixes delimiter-top">
+            <h2>Community fixes</h2>
+            <p id="info-top" class="font-light">
+              This type of <span class="issue-type">vulnerability</span> was fixed in <span id="dataset-number"></span> open source projects. Here are <span id="example-number"></span> examples:
+            </p>
+            <div id="info-no-examples" class="font-light">
+              There are no fix examples for this issue.
+            </div>
+            <div id="example-top" class="row between">
+              <div id="current-example" class="repo clickable">
+                <img class="repo-icon icon" src="${images['icon-github']}"></img>
+                <span id="example-link" class="repo-link"></span>
+              </div>
+              <div class="examples-nav">
+                <span id="previous-example" class="arrow" title="Previous example">
+                  <img src=${images['arrow-left-dark']} class="arrow-icon dark-only"></img>
+                  <img src=${images['arrow-left-light']} class="arrow-icon light-only"></img>
+                </span>
+                <span id="example-text">
+                  Example <strong id="example-counter">1</strong>/<span id="example-number2"></span>
+                </span>
+                <span id="next-example" class="arrow" title="Next example">
+                  <img src=${images['arrow-right-dark']} class="arrow-icon dark-only"></img>
+                  <img src=${images['arrow-right-light']} class="arrow-icon light-only"></img>
+                </span>
+              </div>
+            </div>
+            <div id="example" class="example"></div>
+          </section>
+        </div>
+
+        <div id="vuln-overview-content" class="tab-content sn-vuln-content">
+          <section class="delimiter-top suggestion-details-content">
+            <div id="suggestion-details" class="suggestion-details"></div>
+          </section>
+        </div>
+
+        <section class="suggestion-actions delimiter-top">
           <div id="actions-section">
             <div class="actions row">
-              <button id="ignore-line-issue" class="button">Ignore on line <span id="line-position2"></span></button>
-              <button id="ignore-file-issue" class="button">Ignore in this file</button>
+              <button id="ignore-line-issue" class="button secondary">Ignore on line <span id="line-position2"></span></button>
+              <button id="ignore-file-issue" class="button secondary">Ignore in this file</button>
             </div>
           </div>
         </section>
diff --git a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScript.ts b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScript.ts
index 4683022f7..197fced52 100644
--- a/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScript.ts
+++ b/src/snyk/snykCode/views/suggestion/codeSuggestionWebviewScript.ts
@@ -6,7 +6,6 @@
 /* eslint-disable @typescript-eslint/no-unsafe-call */
 /// <reference lib="dom" />
 
-// eslint-disable-next-line @typescript-eslint/no-unused-vars,@typescript-eslint/no-explicit-any
 declare const acquireVsCodeApi: any;
 
 // This script will be run within the webview itself
@@ -51,21 +50,120 @@ declare const acquireVsCodeApi: any;
     title: string;
     text: string;
     isSecurityType: boolean;
-    uri: string;
     markers?: Marker[];
     cols: Point;
     rows: Point;
     priorityScore: number;
+    hasAIFix: boolean;
+    diffs: AutofixUnifiedDiffSuggestion[];
+    filePath: string;
   };
+
   type CurrentSeverity = {
     value: number;
     text: string;
   };
 
+  type AutofixUnifiedDiffSuggestion = {
+    fixId: string;
+    unifiedDiffsPerFile: { [key: string]: string };
+  };
+
+  type OpenLocalMessage = {
+    type: 'openLocal';
+    args: {
+      uri: string;
+      cols: [number, number];
+      rows: [number, number];
+      suggestionUri: string;
+    };
+  };
+
+  type IgnoreIssueMessage = {
+    type: 'ignoreIssue';
+    args: {
+      id: string;
+      severity: 'Low' | 'Medium' | 'High';
+      lineOnly: boolean;
+      message: string;
+      rule: string;
+      uri: string;
+      cols: [number, number];
+      rows: [number, number];
+    };
+  };
+
+  type OpenBrowserMessage = {
+    type: 'openBrowser';
+    args: {
+      url: string;
+    };
+  };
+
+  type GetAutofixDiffsMesssage = {
+    type: 'getAutofixDiffs';
+    args: {
+      suggestion: Suggestion;
+    };
+  };
+
+  type ApplyGitDiffMessage = {
+    type: 'applyGitDiff';
+    args: {
+      patch: string;
+      filePath: string;
+    };
+  };
+
+  type SetSuggestionMessage = {
+    type: 'set';
+    args: Suggestion;
+  };
+
+  type GetSuggestionMessage = {
+    type: 'get';
+  };
+
+  type SetLessonMessage = {
+    type: 'setLesson';
+    args: Lesson;
+  };
+
+  type GetLessonMessage = {
+    type: 'getLesson';
+  };
+
+  type SetAutofixDiffsMessage = {
+    type: 'setAutofixDiffs';
+    args: {
+      suggestion: Suggestion;
+      diffs: AutofixUnifiedDiffSuggestion[];
+    };
+  };
+
+  type SetAutofixErrorMessage = {
+    type: 'setAutofixError';
+    args: {
+      suggestion: Suggestion;
+    };
+  };
+
+  type SuggestionMessage =
+    | OpenLocalMessage
+    | OpenBrowserMessage
+    | IgnoreIssueMessage
+    | GetAutofixDiffsMesssage
+    | ApplyGitDiffMessage
+    | SetSuggestionMessage
+    | GetSuggestionMessage
+    | SetLessonMessage
+    | GetLessonMessage
+    | SetAutofixDiffsMessage
+    | SetAutofixErrorMessage;
+
   const vscode = acquireVsCodeApi();
 
   const elements = {
-    readMoreBtnElem: document.querySelector('.read-more-btn') as HTMLElement,
     suggestionDetailsElem: document.querySelector('#suggestion-details') as HTMLElement,
     suggestionDetailsContentElem: document.querySelector('.suggestion-details-content') as HTMLElement,
     metaElem: document.getElementById('meta') as HTMLElement,
@@ -79,6 +177,29 @@ declare const acquireVsCodeApi: any;
     datasetElem: document.getElementById('dataset-number') as HTMLElement,
     infoTopElem: document.getElementById('info-top') as HTMLElement,
 
+    diffTopElem: document.getElementById('diff-top') as HTMLElement,
+    diffElem: document.getElementById('diff') as HTMLElement,
+    noDiffsElem: document.getElementById('info-no-diffs') as HTMLElement,
+    diffNumElem: document.getElementById('diff-number') as HTMLElement,
+    diffNum2Elem: document.getElementById('diff-number2') as HTMLElement,
+    nextDiffElem: document.getElementById('next-diff') as HTMLElement,
+    previousDiffElem: document.getElementById('previous-diff') as HTMLElement,
+    diffSelectedIndexElem: document.getElementById('diff-counter') as HTMLElement,
+
+    applyFixButton: document.getElementById('apply-fix') as HTMLElement,
+    retryGenerateFixButton: document.getElementById('retry-generate-fix') as HTMLElement,
+    generateAIFixButton: document.getElementById('generate-ai-fix') as HTMLElement,
+
+    fixAnalysisTabElem: document.getElementById('fix-analysis-tab') as HTMLElement,
+    fixAnalysisContentElem: document.getElementById('fix-analysis-content') as HTMLElement,
+    vulnOverviewTabElem: document.getElementById('vuln-overview-tab') as HTMLElement,
+    vulnOverviewContentElem: document.getElementById('vuln-overview-content') as HTMLElement,
+
+    fixLoadingIndicatorElem: document.getElementById('fix-loading-indicator') as HTMLElement,
+    fixWrapperElem: document.getElementById('fix-wrapper') as HTMLElement,
+    fixSectionElem: document.getElementById('fixes-section') as HTMLElement,
+    fixErrorSectionElem: document.getElementById('fixes-error-section') as HTMLElement,
+
     exampleTopElem: document.getElementById('example-top') as HTMLElement,
     exampleElem: document.getElementById('example') as HTMLElement,
     noExamplesElem: document.getElementById('info-no-examples') as HTMLElement,
@@ -86,16 +207,16 @@ declare const acquireVsCodeApi: any;
     exNum2Elem: document.getElementById('example-number2') as HTMLElement,
   };
 
-  let isReadMoreBtnEventBound = false;
-
   function navigateToUrl(url: string) {
-    sendMessage({
+    const message: OpenBrowserMessage = {
       type: 'openBrowser',
       args: { url },
-    });
+    };
+    sendMessage(message);
   }
 
   let exampleCount = 0;
+  let diffSelectedIndex = 0;
 
   // Try to restore the previous state
   let lesson: Lesson | null = vscode.getState()?.lesson || null;
@@ -107,26 +228,32 @@ declare const acquireVsCodeApi: any;
     if (!suggestion?.leadURL) return;
     navigateToUrl(suggestion.leadURL);
   }
+
   function navigateToIssue(_e: any, range: any) {
     if (!suggestion) return;
-    sendMessage({
+    const message: OpenLocalMessage = {
       type: 'openLocal',
       args: getSuggestionPosition(suggestion, range),
-    });
+    };
+
+    sendMessage(message);
   }
+
   function navigateToCurrentExample() {
     if (!suggestion?.exampleCommitFixes) return;
 
     const url = suggestion.exampleCommitFixes[exampleCount].commitURL;
-    sendMessage({
+    const message: OpenBrowserMessage = {
       type: 'openBrowser',
       args: { url },
-    });
+    };
+    sendMessage(message);
   }
+
   function ignoreIssue(lineOnly: boolean) {
     if (!suggestion) return;
 
-    sendMessage({
+    const message: IgnoreIssueMessage = {
       type: 'ignoreIssue',
       args: {
         ...getSuggestionPosition(suggestion),
@@ -136,27 +263,149 @@ declare const acquireVsCodeApi: any;
         severity: suggestion.severity,
         lineOnly: lineOnly,
       },
-    });
+    };
+    sendMessage(message);
   }
+
   function getSuggestionPosition(suggestionParam: Suggestion, position?: { file: string; rows: any; cols: any }) {
     return {
-      uri: position?.file ?? suggestionParam.uri,
+      uri: position?.file ?? suggestionParam.filePath,
       rows: position ? position.rows : suggestionParam.rows,
       cols: position ? position.cols : suggestionParam.cols,
-      suggestionUri: suggestionParam.uri,
+      suggestionUri: suggestionParam.filePath,
+    };
+  }
+
+  function nextDiff() {
+    if (!suggestion || !suggestion.diffs || diffSelectedIndex >= suggestion.diffs.length - 1) return;
+    ++diffSelectedIndex;
+    showCurrentDiff();
+  }
+
+  function previousDiff() {
+    if (!suggestion || !suggestion.diffs || diffSelectedIndex <= 0) return;
+    --diffSelectedIndex;
+    showCurrentDiff();
+  }
+
+  function applyFix() {
+    if (!suggestion) return;
+    const diffSuggestion = suggestion.diffs[diffSelectedIndex];
+    const filePath = suggestion.filePath;
+    const patch = diffSuggestion.unifiedDiffsPerFile[filePath];
+
+    const message: ApplyGitDiffMessage = {
+      type: 'applyGitDiff',
+      args: { filePath, patch },
+    };
+    sendMessage(message);
+  }
+
+  function generateAIFix() {
+    if (!suggestion) {
+      return;
+    }
+
+    toggleElement(generateAIFixButton, 'hide');
+    toggleElement(fixLoadingIndicatorElem, 'show');
+    const message: GetAutofixDiffsMesssage = {
+      type: 'getAutofixDiffs',
+      args: { suggestion },
     };
+    sendMessage(message);
   }
+
+  function retryGenerateAIFix() {
+    console.log('retrying generate AI Fix');
+
+    toggleElement(fixWrapperElem, 'show');
+    toggleElement(fixErrorSectionElem, 'hide');
+
+    generateAIFix();
+  }
+
   function previousExample() {
     if (!suggestion || !suggestion.exampleCommitFixes || exampleCount <= 0) return;
     --exampleCount;
     showCurrentExample();
   }
+
   function nextExample() {
     if (!suggestion || !suggestion.exampleCommitFixes || exampleCount >= suggestion.exampleCommitFixes.length - 1)
       return;
     ++exampleCount;
     showCurrentExample();
   }
+
+  function showCurrentDiff() {
+    if (!suggestion?.diffs?.length || diffSelectedIndex < 0 || diffSelectedIndex >= suggestion.diffs.length) return;
+
+    const { diffTopElem, diffElem, noDiffsElem, diffNumElem, diffNum2Elem, diffSelectedIndexElem } = elements;
+
+    toggleElement(noDiffsElem, 'hide');
+    toggleElement(diffTopElem, 'show');
+    toggleElement(diffElem, 'show');
+
+    diffNumElem.innerText = suggestion.diffs.length.toString();
+    diffNum2Elem.innerText = suggestion.diffs.length.toString();
+
+    diffSelectedIndexElem.innerText = (diffSelectedIndex + 1).toString();
+
+    const diffSuggestion = suggestion.diffs[diffSelectedIndex];
+
+    const filePath = suggestion.filePath;
+    const patch = diffSuggestion.unifiedDiffsPerFile[filePath];
+
+    // clear all elements
+    while (diffElem.firstChild) {
+      diffElem.removeChild(diffElem.firstChild);
+    }
+    diffElem.appendChild(generateDiffHtml(patch));
+  }
+
+  function generateDiffHtml(patch: string): HTMLElement {
+    const codeLines = patch.split('\n');
+
+    // the first two lines are the file names
+    codeLines.shift();
+    codeLines.shift();
+
+    const diffHtml = document.createElement('div');
+    let blockDiv: HTMLElement | null = null;
+
+    for (const line of codeLines) {
+      if (line.startsWith('@@ ')) {
+        blockDiv = document.createElement('div');
+        blockDiv.className = 'example';
+
+        if (blockDiv) {
+          diffHtml.appendChild(blockDiv);
+        }
+      } else {
+        const lineDiv = document.createElement('div');
+        lineDiv.className = 'code-line';
+
+        if (line.startsWith('+')) {
+          lineDiv.classList.add('added');
+        } else if (line.startsWith('-')) {
+          lineDiv.classList.add('removed');
+        } else {
+          lineDiv.classList.add('none');
+        }
+
+        const lineCode = document.createElement('code');
+        // if line is empty, we need to fallback to ' '
+        // to make sure it displays in the diff
+        lineCode.innerText = line.slice(1, line.length) || ' ';
+
+        lineDiv.appendChild(lineCode);
+        blockDiv?.appendChild(lineDiv);
+      }
+    }
+
+    return diffHtml;
+  }
+
   function showCurrentExample() {
     if (
       !suggestion?.exampleCommitFixes?.length ||
@@ -176,7 +425,7 @@ declare const acquireVsCodeApi: any;
     example.querySelectorAll('*').forEach(n => n.remove());
     for (const l of suggestion.exampleCommitFixes[exampleCount].lines) {
       const line = document.createElement('div');
-      line.className = `example-line ${l.lineChange}`;
+      line.className = `code-line ${l.lineChange}`;
       example.appendChild(line);
       const code = document.createElement('code');
       code.innerHTML = l.line;
@@ -184,6 +433,20 @@ declare const acquireVsCodeApi: any;
     }
   }
 
+  function toggleElement(element: Element | null, toggle: 'hide' | 'show') {
+    if (!element) {
+      return;
+    }
+
+    if (toggle === 'show') {
+      element.classList.remove('hidden');
+    } else if (toggle === 'hide') {
+      element.classList.add('hidden');
+    } else {
+      console.error('Unexpected toggle value', toggle);
+    }
+  }
+
   /**
    * Transforms a severity string from a `Suggestion` object into a `CurrentSeverity` object.
    *
@@ -327,30 +590,31 @@ declare const acquireVsCodeApi: any;
       descriptionElem.innerHTML = suggestion.message;
     }
 
-    moreInfoElem.className = suggestion.leadURL ? 'clickable' : 'clickable hidden';
+    toggleElement(moreInfoElem, suggestion.leadURL ? 'show' : 'hide');
 
-    suggestionPosition2Elem.innerHTML = (Number(suggestion.rows[0]) + 1).toString();
+    suggestionPosition2Elem.innerText = (Number(suggestion.rows[0]) + 1).toString();
 
     infoTopElem.classList.add('font-light');
     if (suggestion.repoDatasetSize) {
-      datasetElem.innerHTML = suggestion.repoDatasetSize.toString();
+      datasetElem.innerText = suggestion.repoDatasetSize.toString();
     } else {
-      infoTopElem.classList.add('hidden');
+      toggleElement(infoTopElem, 'hide');
     }
 
     if (suggestion?.exampleCommitFixes?.length) {
-      exampleTopElem.className = 'row between';
-      exampleElem.className = '';
+      toggleElement(exampleTopElem, 'show');
+      exNumElem.innerText = suggestion.exampleCommitFixes.length.toString();
+      exNum2Elem.innerText = suggestion.exampleCommitFixes.length.toString();
 
-      exNumElem.innerHTML = suggestion.exampleCommitFixes.length.toString();
+      toggleElement(exampleElem, 'show');
+      toggleElement(noExamplesElem, 'hide');
 
-      exNum2Elem.innerHTML = suggestion.exampleCommitFixes.length.toString();
-      noExamplesElem.className = 'hidden';
       showCurrentExample();
     } else {
-      exampleTopElem.className = 'row between hidden';
-      exampleElem.className = 'hidden';
+      toggleElement(exampleTopElem, 'hide');
       noExamplesElem.className = 'font-light';
+
+      toggleElement(exampleElem, 'hide');
     }
   }
 
@@ -379,7 +643,7 @@ declare const acquireVsCodeApi: any;
     if (suggestion.cwe) {
       suggestion.cwe.forEach(cwe => {
         const cweElement = document.createElement('a');
-        cweElement.className = 'suggestion-meta suggestion-cwe';
+        cweElement.className = 'suggestion-meta suggestion-cwe is-external';
         cweElement.href = `https://cwe.mitre.org/data/definitions/${cwe.split('-')[1]}.html`;
         cweElement.textContent = cwe;
         metaElem.appendChild(cweElement);
@@ -404,50 +668,67 @@ declare const acquireVsCodeApi: any;
     if (suggestion.priorityScore !== undefined) {
       const priorityScoreElement = document.createElement('span');
       priorityScoreElement.className = 'suggestion-meta';
-      priorityScoreElement.textContent = `Priority Score: ${suggestion.priorityScore}`;
+      priorityScoreElement.textContent = `Priority score: ${suggestion.priorityScore}`;
       metaElem.appendChild(priorityScoreElement);
     }
-  }
 
-  function showSuggestionDetails(suggestion: Suggestion) {
-    const { suggestionDetailsElem, readMoreBtnElem, suggestionDetailsContentElem } = elements;
+    const fixesSection = document.querySelector('.ai-fix');
+    const communityFixesSection = document.querySelector('.sn-community-fixes');
 
-    if (!suggestion || !suggestion.text || !suggestionDetailsElem || !readMoreBtnElem) {
-      readMoreBtnElem.classList.add('hidden');
-      suggestionDetailsContentElem.classList.add('hidden');
-      return;
+    if (!suggestion.hasAIFix) {
+      toggleElement(fixesSection, 'hide');
+      toggleElement(communityFixesSection, 'show');
+    } else {
+      toggleElement(fixesSection, 'show');
+      toggleElement(communityFixesSection, 'hide');
     }
+  }
+
+  function showSuggestionDetails(suggestion: Suggestion) {
+    const {
+      suggestionDetailsElem,
+      fixAnalysisTabElem,
+      fixAnalysisContentElem,
+      vulnOverviewTabElem,
+      vulnOverviewContentElem,
+    } = elements;
 
     suggestionDetailsElem.innerHTML = suggestion.text;
-    suggestionDetailsElem.classList.add('collapsed');
-    readMoreBtnElem.classList.remove('hidden');
-    suggestionDetailsContentElem.classList.remove('hidden');
 
-    if (!isReadMoreBtnEventBound) {
-      readMoreBtnElem.addEventListener('click', () => {
-        const isCollapsed = suggestionDetailsElem.classList.contains('collapsed');
+    fixAnalysisTabElem.addEventListener('click', () => {
+      fixAnalysisTabElem.classList.add('is-selected');
+      fixAnalysisContentElem.classList.add('is-selected');
+      vulnOverviewTabElem.classList.remove('is-selected');
+      vulnOverviewContentElem.classList.remove('is-selected');
+    });
 
-        if (isCollapsed) {
-          suggestionDetailsElem.classList.remove('collapsed');
-          readMoreBtnElem.textContent = 'Read less';
-        } else {
-          suggestionDetailsElem.classList.add('collapsed');
-          readMoreBtnElem.textContent = 'Read more';
-        }
-      });
-      isReadMoreBtnEventBound = true;
-    }
+    vulnOverviewTabElem.addEventListener('click', () => {
+      vulnOverviewContentElem.classList.add('is-selected');
+      vulnOverviewTabElem.classList.add('is-selected');
+      fixAnalysisTabElem.classList.remove('is-selected');
+      fixAnalysisContentElem.classList.remove('is-selected');
+    });
   }
 
-  function sendMessage(message: {
-    type: string;
-    args:
-      | { uri: any; rows: any; cols: any }
-      | { url: any }
-      | { url: string }
-      | { message: any; rule: any; id: any; severity: any; lineOnly: boolean; uri: any; rows: any; cols: any }
-      | { suggestion: any };
-  }) {
+  const {
+    generateAIFixButton,
+    retryGenerateFixButton,
+    applyFixButton,
+    nextDiffElem,
+    previousDiffElem,
+    fixSectionElem,
+    fixLoadingIndicatorElem,
+    fixWrapperElem,
+    fixErrorSectionElem,
+  } = elements;
+
+  generateAIFixButton?.addEventListener('click', generateAIFix);
+  retryGenerateFixButton?.addEventListener('click', retryGenerateAIFix);
+  nextDiffElem.addEventListener('click', nextDiff);
+  previousDiffElem.addEventListener('click', previousDiff);
+  applyFixButton.addEventListener('click', applyFix);
+
+  function sendMessage(message: SuggestionMessage) {
     vscode.postMessage(message);
   }
 
@@ -464,21 +745,24 @@ declare const acquireVsCodeApi: any;
 
   // deepcode ignore InsufficientValidation: Content Security Policy applied in provider
   window.addEventListener('message', event => {
-    const { type, args } = event.data;
-    switch (type) {
+    const message = event.data as SuggestionMessage;
+    switch (message.type) {
       case 'set': {
-        suggestion = args;
+        suggestion = message.args;
         vscode.setState({ ...vscode.getState(), suggestion });
         showCurrentSuggestion();
         break;
       }
       case 'get': {
-        suggestion = vscode.getState()?.suggestion || {};
-        showCurrentSuggestion();
+        const newSuggestion = vscode.getState()?.suggestion || {};
+        if (newSuggestion != suggestion) {
+          suggestion = newSuggestion;
+          showCurrentSuggestion();
+        }
         break;
       }
       case 'setLesson': {
-        lesson = args;
+        lesson = message.args;
         vscode.setState({ ...vscode.getState(), lesson });
         fillLearnLink();
         break;
@@ -488,6 +772,30 @@ declare const acquireVsCodeApi: any;
         fillLearnLink();
         break;
       }
+      case 'setAutofixDiffs': {
+        if (suggestion?.id === message.args.suggestion.id) {
+          toggleElement(fixSectionElem, 'show');
+          toggleElement(fixLoadingIndicatorElem, 'hide');
+          toggleElement(fixWrapperElem, 'hide');
+
+          const { diffs } = message.args;
+          suggestion.diffs = diffs;
+
+          vscode.setState({ ...vscode.getState(), suggestion });
+          showCurrentDiff();
+        }
+        break;
+      }
+      case 'setAutofixError': {
+        const errorSuggestion = message.args.suggestion;
+
+        if (errorSuggestion.id != suggestion?.id) {
+          console.log('Got an error for a previously generated suggestion: ignoring');
+          break;
+        }
+        toggleElement(fixWrapperElem, 'hide');
+        toggleElement(fixErrorSectionElem, 'show');
+      }
     }
   });
 })();
diff --git a/src/snyk/snykCode/views/suggestion/types.ts b/src/snyk/snykCode/views/suggestion/types.ts
new file mode 100644
index 000000000..3a6513b7f
--- /dev/null
+++ b/src/snyk/snykCode/views/suggestion/types.ts
@@ -0,0 +1,113 @@
+import { AutofixUnifiedDiffSuggestion, ExampleCommitFix, Marker, Point } from '../../../common/languageServer/types';
+import { Lesson } from '../../../common/services/learnService';
+
+export type Suggestion = {
+  id: string;
+  message: string;
+  severity: string;
+  leadURL?: string;
+  rule: string;
+  repoDatasetSize: number;
+  exampleCommitFixes: ExampleCommitFix[];
+  cwe: string[];
+  title: string;
+  text: string;
+  isSecurityType: boolean;
+  markers?: Marker[];
+  cols: Point;
+  rows: Point;
+  hasAIFix?: boolean;
+  filePath: string;
+};
+
+export type OpenLocalMessage = {
+  type: 'openLocal';
+  args: {
+    uri: string;
+    cols: [number, number];
+    rows: [number, number];
+    suggestionUri: string;
+  };
+};
+
+export type IgnoreIssueMessage = {
+  type: 'ignoreIssue';
+  args: {
+    id: string;
+    severity: 'Low' | 'Medium' | 'High';
+    lineOnly: boolean;
+    message: string;
+    rule: string;
+    uri: string;
+    cols: [number, number];
+    rows: [number, number];
+  };
+};
+
+export type OpenBrowserMessage = {
+  type: 'openBrowser';
+  args: {
+    url: string;
+  };
+};
+
+export type GetAutofixDiffsMesssage = {
+  type: 'getAutofixDiffs';
+  args: {
+    suggestion: Suggestion;
+  };
+};
+
+export type ApplyGitDiffMessage = {
+  type: 'applyGitDiff';
+  args: {
+    patch: string;
+    filePath: string;
+  };
+};
+
+export type SetSuggestionMessage = {
+  type: 'set';
+  args: Suggestion;
+};
+
+export type GetSuggestionMessage = {
+  type: 'get';
+};
+
+export type SetLessonMessage = {
+  type: 'setLesson';
+  args: Lesson | null;
+};
+
+export type GetLessonMessage = {
+  type: 'getLesson';
+};
+
+export type SetAutofixDiffsMessage = {
+  type: 'setAutofixDiffs';
+  args: {
+    suggestion: Suggestion;
+    diffs: AutofixUnifiedDiffSuggestion[];
+  };
+};
+
+export type SetAutofixErrorMessage = {
+  type: 'setAutofixError';
+  args: {
+    suggestion: Suggestion;
+  };
+};
+
+export type SuggestionMessage =
+  | OpenLocalMessage
+  | OpenBrowserMessage
+  | IgnoreIssueMessage
+  | GetAutofixDiffsMesssage
+  | ApplyGitDiffMessage
+  | SetSuggestionMessage
+  | GetSuggestionMessage
+  | SetLessonMessage
+  | GetLessonMessage
+  | SetAutofixDiffsMessage
+  | SetAutofixErrorMessage;
diff --git a/src/snyk/snykCode/views/webviewPanelSerializer.ts b/src/snyk/snykCode/views/webviewPanelSerializer.ts
new file mode 100644
index 000000000..e48327bef
--- /dev/null
+++ b/src/snyk/snykCode/views/webviewPanelSerializer.ts
@@ -0,0 +1,14 @@
+import * as vscode from 'vscode';
+import { WebviewProvider } from '../../../snyk/common/views/webviewProvider';
+import { Logger } from '../../common/logger/logger';
+
+export class WebviewPanelSerializer<Provider extends WebviewProvider<State>, State>
+  implements vscode.WebviewPanelSerializer
+{
+  constructor(private readonly provider: Provider) {}
+  async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel): Promise<void> {
+    // we want to make sure the panel is closed on startup
+    webviewPanel.dispose();
+    return Promise.resolve();
+  }
+}
diff --git a/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts b/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts
index f3aeba284..1aadf6c2d 100644
--- a/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts
+++ b/src/snyk/snykOss/providers/ossVulnerabilityTreeProvider.ts
@@ -38,9 +38,8 @@ export default class OssIssueTreeProvider extends ProductIssueTreeProvider<OssIs
     return super.getRootChildren();
   }
 
-  override getResultNodes(): [TreeNode[], number] {
+  override getResultNodes(): TreeNode[] {
     const nodes: TreeNode[] = [];
-    let totalVulnCount = 0;
 
     for (const result of this.productService.result.entries()) {
       const folderPath = result[0];
@@ -78,7 +77,6 @@ export default class OssIssueTreeProvider extends ProductIssueTreeProvider<OssIs
 
         const vulnerabilityNodes: TreeNode[] = filteredIssues.map((issue: Issue<OssIssueData>) => {
           fileSeverityCounts[issue.severity] += 1;
-          totalVulnCount++;
           folderVulnCount++;
 
           return new TreeNode({
@@ -140,7 +138,7 @@ export default class OssIssueTreeProvider extends ProductIssueTreeProvider<OssIs
       }
     }
 
-    return [nodes, totalVulnCount];
+    return nodes;
   }
 
   onDidChangeTreeData = this.viewManagerService.refreshOssViewEmitter.event;
diff --git a/src/test/unit/common/services/learnService.test.ts b/src/test/unit/common/services/learnService.test.ts
index b97bb9529..8607f06e0 100644
--- a/src/test/unit/common/services/learnService.test.ts
+++ b/src/test/unit/common/services/learnService.test.ts
@@ -35,6 +35,7 @@ suite('LearnService', () => {
         rows: [1, 2],
         isSecurityType: true,
         priorityScore: 880,
+        hasAIFix: false,
       },
       title: 'not used',
       severity: IssueSeverity.Critical,
diff --git a/src/test/unit/snykCode/utils/patchUtils.test.ts b/src/test/unit/snykCode/utils/patchUtils.test.ts
new file mode 100644
index 000000000..96e5ba63e
--- /dev/null
+++ b/src/test/unit/snykCode/utils/patchUtils.test.ts
@@ -0,0 +1,118 @@
+import assert from 'assert';
+import * as patchUtils from '../../../../snyk/snykCode/utils/patchUtils';
+import { IVSCodeLanguages } from '../../../../snyk/common/vscode/languages';
+
+suite('generateDecorationOptions', () => {
+  let languages: IVSCodeLanguages;
+
+  setup(() => {
+    languages = {
+      createRange: (startLine: number, startCharacter: number, endLine: number, endCharacter: number) => ({
+        start: { line: startLine, character: startCharacter },
+        end: { line: endLine, character: endCharacter },
+      }),
+    } as IVSCodeLanguages;
+  });
+
+  test('generates ranges for adding to empty files', () => {
+    const patch = `--- /home/mike/boom
++++ /home/mike/boom-fixed
+@@ -1 1,4 @@
++ def main():
++     print("hello world")
++
++ main()`;
+    const result = patchUtils.generateDecorationOptions(patch, languages);
+    assert.strictEqual(result.length, 4);
+
+    assert.strictEqual(result[0].range.start.line, 0);
+    assert.strictEqual(result[0].range.start.character, 0);
+    assert.strictEqual(result[0].range.end.line, 0);
+    assert.strictEqual(result[0].range.end.character, 12);
+
+    assert.strictEqual(result[3].range.start.line, 3);
+    assert.strictEqual(result[3].range.start.character, 0);
+    assert.strictEqual(result[3].range.end.line, 3);
+    assert.strictEqual(result[3].range.end.character, 7);
+  });
+
+  test('generates empty result for completely removing a file', () => {
+    const patch = `--- /home/mike/boom
++++ /home/mike/boom-fixed
+@@ -1,4 1 @@
+- def main():
+-     print("hello world")
+-
+- main()`;
+    const result = patchUtils.generateDecorationOptions(patch, languages);
+    assert.strictEqual(result.length, 0);
+  });
+
+  test('works with single hunks', () => {
+    const patch = `-- /home/patch/goof
++++ /home/patch/goof-fixed
+@@ -1 +15,8 @@
+ var fileType = require('file-type');
+ var AdmZip = require('adm-zip');
+ var fs = require('fs');
++var RateLimit = require('express-rate-limit');
++var limiter = new RateLimit({
++  windowMs: parseInt(process.env.WINDOW_MS, 10),
++  max: parseInt(process.env.MAX_IP_REQUESTS, 10),
++  delayMs:parseInt(process.env.DELAY_MS, 10),
++  headers: true
++});
++app.user(limiter);
+
+ // prototype-pollution
+ var _ = require('lodash');`;
+
+    const result = patchUtils.generateDecorationOptions(patch, languages);
+
+    assert.strictEqual(result.length, 8);
+
+    assert.strictEqual(result[0].range.start.line, 17);
+    assert.strictEqual(result[0].range.start.character, 0);
+    assert.strictEqual(result[0].range.end.line, 17);
+    assert.strictEqual(result[0].range.end.character, 46);
+
+    assert.strictEqual(result[7].range.start.line, 24);
+    assert.strictEqual(result[7].range.start.character, 0);
+    assert.strictEqual(result[7].range.end.line, 24);
+    assert.strictEqual(result[7].range.end.character, 18);
+  });
+
+  test('works with multiple hunks', () => {
+    const patch = `-- /home/patch/snek
++++ /home/patch/snek-fixed
+@@ -1,2 +1,2 @@
+ import math
+ from my_module import do_some_work
+
+ def generate_number() -> int:
+-   return math.random() * 100
++   return math.random() * 20
+
+ result = do_some_work()
+ print(result)
+
+@@ -25,1 +25,1 @@
+-result *= generate_number()
++result += generate_number()
+`;
+
+    const result = patchUtils.generateDecorationOptions(patch, languages);
+
+    assert.strictEqual(result.length, 2);
+
+    assert.strictEqual(result[0].range.start.line, 4);
+    assert.strictEqual(result[0].range.start.character, 0);
+    assert.strictEqual(result[0].range.end.line, 4);
+    assert.strictEqual(result[0].range.end.character, 28);
+
+    assert.strictEqual(result[1].range.start.line, 24);
+    assert.strictEqual(result[1].range.start.character, 0);
+    assert.strictEqual(result[1].range.end.line, 24);
+    assert.strictEqual(result[1].range.end.character, 27);
+  });
+});