Repository: stateful/vscode-runme
Pull request #1624 (2024-09-06 15:15:18)

Grouping e2e testing modules for Dependabot Updates

sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=stateful_vscode-runme&pullRequest=1624) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=stateful_vscode-runme&pullRequest=1624&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=stateful_vscode-runme&pullRequest=1624&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=stateful_vscode-runme&pullRequest=1624&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=stateful_vscode-runme&pullRequest=1624&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=stateful_vscode-runme&pullRequest=1624&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=stateful_vscode-runme&pullRequest=1624)



Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index c0338e46..ce016d59 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -31,3 +31,12 @@ updates:
           - "@aws-sdk/client-ec2"
           - "@aws-sdk/client-eks"
           - "@aws-sdk/credential-providers"
+      e2e-tests:
+        patterns:
+          - "@wdio/cli"
+          - "@wdio/local-runner"
+          - "@wdio/mocha-framework"
+          - "@wdio/spec-reporter"
+          - "chromedriver"
+          - "wdio-vscode-service"
+          - "webdriverio"
diff --git a/.github/scripts/overwrites/stateful.sh b/.github/scripts/overwrites/stateful.sh
index dd7ead50..0a7b120b 100755
--- a/.github/scripts/overwrites/stateful.sh
+++ b/.github/scripts/overwrites/stateful.sh
@@ -8,7 +8,7 @@ npm pkg set homepage="https://stateful.com"
 npm pkg set contributes.configuration[0].properties[runme.app.baseDomain].default="platform.stateful.com"
 npm pkg set contributes.configuration[0].properties[runme.app.platformAuth].default=true --json
 npm pkg set contributes.configuration[0].properties[runme.app.enableShare].default=true --json
-npm pkg set contributes.configuration[0].properties[runme.experiments.escalationButton].default=true --json
+npm pkg set contributes.configuration[0].properties[runme.features.escalate].default=true --json
 npm pkg set contributes.configuration[0].properties[runme.server.lifecycleIdentity].default=1 --json
 npm pkg set contributes.configuration[0].properties[runme.app.notebookAutoSave].default="yes"
 npm pkg set contributes.configuration[0].properties[runme.app.panel.runme.cloud].default="\"\"" --json
diff --git a/__mocks__/vscode.ts b/__mocks__/vscode.ts
index 32cd0025..2ea1673b 100644
--- a/__mocks__/vscode.ts
+++ b/__mocks__/vscode.ts
@@ -322,3 +322,5 @@ export enum EndOfLine {
    */
   CRLF = 2
 }
+
+export const version = '9.9.9'
diff --git a/package-lock.json b/package-lock.json
index 38452cf3..db3873f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "runme",
-  "version": "3.7.4-edge.0",
+  "version": "3.7.4-edge.2",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "runme",
-      "version": "3.7.4-edge.0",
+      "version": "3.7.4-edge.2",
       "hasInstallScript": true,
       "license": "Apache-2.0",
       "dependencies": {
@@ -63,6 +63,7 @@
         "@graphql-codegen/typescript": "4.0.9",
         "@octokit/rest": "^19.0.13",
         "@types/node": "^18.19.14",
+        "@types/semver": "^7.5.8",
         "@types/uuid": "^10.0.0",
         "@types/vscode": "1.72.0",
         "@types/vscode-notebook-renderer": "^1.72.3",
@@ -7179,6 +7180,12 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/semver": {
+      "version": "7.5.8",
+      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+      "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
+      "dev": true
+    },
     "node_modules/@types/shimmer": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
diff --git a/package.json b/package.json
index e3491bf8..5fa09b09 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "runme",
-  "version": "3.7.4-edge.0",
+  "version": "3.7.4-edge.2",
   "publisher": "stateful",
   "displayName": "Runme Notebooks for DevOps",
   "description": "Notebooks for DevOps written in Markdown. Runnable, configurable commands/code and embedded terminals. Integrated with cloud consoles, containers and pipelines.",
@@ -56,6 +56,45 @@
     "onStartupFinished"
   ],
   "main": "./out/extension.js",
+  "runme": {
+    "features": [
+      {
+        "name": "Escalate",
+        "enabled": true,
+        "conditions": {
+          "os": "All",
+          "vsCodeVersion": ">=1.58.0",
+          "statefulAuthRequired": true,
+          "allowedExtensions": [
+            "stateful.platform"
+          ]
+        }
+      },
+      {
+        "name": "Share",
+        "enabled": true,
+        "conditions": {
+          "os": "All",
+          "vsCodeVersion": ">=1.58.0",
+          "statefulAuthRequired": true,
+          "allowedExtensions": [
+            "stateful.platform"
+          ]
+        }
+      },
+      {
+        "name": "Gist",
+        "enabled": true,
+        "conditions": {
+          "os": "All",
+          "vsCodeVersion": ">=1.58.0",
+          "allowedExtensions": [
+            "stateful.runme"
+          ]
+        }
+      }
+    ]
+  },
   "contributes": {
     "commands": [
       {
@@ -88,7 +127,7 @@
       },
       {
         "command": "runme.runForkCommand",
-        "title": "Run fork ENV in new terminal"
+        "title": "Run fork session in new terminal"
       },
       {
         "command": "runme.surveyWinDefaultShell",
@@ -557,12 +596,6 @@
             "title": "AI Auto Cell",
             "markdownDescription": "If set to `true`, enable AI auto generation of cells. Reload the window after changing this setting."
           },
-          "runme.experiments.escalationButton": {
-            "type": "boolean",
-            "scope": "window",
-            "default": false,
-            "markdownDescription": "If set to `true`, the secondary action button after saving a call will display Save or Escalate depending on the exit code."
-          },
           "runme.experiments.smartEnvStore": {
             "type": "boolean",
             "scope": "window",
@@ -802,11 +835,6 @@
             "scope": "window",
             "markdownDescription": "Base domain to be use for Runme app"
           },
-          "runme.app.enableShare": {
-            "type": "boolean",
-            "default": false,
-            "markdownDescription": "Enable collaboration in notebook terminal"
-          },
           "runme.app.panel.runme.cloud": {
             "type": "string",
             "default": "runme.placeholder",
@@ -862,6 +890,24 @@
             "scope": "window",
             "default": "https://docs.runme.dev",
             "markdownDescription": "Documentation Base URL"
+          },
+          "runme.features.escalate": {
+            "type": "boolean",
+            "scope": "window",
+            "default": false,
+            "markdownDescription": "If set to `true`, the secondary action button after saving a call will display Save or Escalate depending on the exit code."
+          },
+          "runme.features.gist": {
+            "type": "boolean",
+            "scope": "window",
+            "default": false,
+            "markdownDescription": "If set to `true`, the Gist button will be displayed to share the output through Github Gists."
+          },
+          "runme.features.share": {
+            "type": "boolean",
+            "scope": "window",
+            "default": false,
+            "markdownDescription": "If set to `true` Enables collaboration in notebook terminal."
           }
         }
       }
@@ -1113,6 +1159,7 @@
     "@graphql-codegen/typescript": "4.0.9",
     "@octokit/rest": "^19.0.13",
     "@types/node": "^18.19.14",
+    "@types/semver": "^7.5.8",
     "@types/uuid": "^10.0.0",
     "@types/vscode": "1.72.0",
     "@types/vscode-notebook-renderer": "^1.72.3",
diff --git a/src/client/components/terminal/share.ts b/src/client/components/terminal/actionButton.ts
similarity index 52%
rename from src/client/components/terminal/share.ts
rename to src/client/components/terminal/actionButton.ts
index 96cdfa3a..5a46f586 100644
--- a/src/client/components/terminal/share.ts
+++ b/src/client/components/terminal/actionButton.ts
@@ -5,16 +5,25 @@ import { when } from 'lit/directives/when.js'
 import { ShareIcon } from '../icons/share'
 import { SaveIcon } from '../icons/save'
 
-@customElement('share-cell')
-export class ShareCell extends LitElement {
+@customElement('action-button')
+export class ActionButton extends LitElement {
   @property({ type: String })
-  shareText: string = 'Copy'
+  text: string = 'Copy'
+
+  @property({ type: String })
+  loadingText: string = 'Saving...'
+
+  @property({ type: Boolean, reflect: true })
+  loading: boolean = false
 
   @property({ type: Boolean, reflect: true })
   disabled: boolean = false
 
   @property({ type: Boolean, reflect: true })
-  displayShareIcon: boolean = false
+  shareIcon: boolean = false
+
+  @property({ type: Boolean, reflect: true })
+  saveIcon: boolean = false
 
   /* eslint-disable */
   static styles = css`
@@ -39,33 +48,32 @@ export class ShareCell extends LitElement {
     }
   `
 
-  private onShareClick(e: Event) {
+  private onClick(e: Event) {
     if (e.defaultPrevented) {
       e.preventDefault()
     }
-    const event = new CustomEvent('onShare')
+
+    const event = new CustomEvent(this.disabled ? 'onClickDisabled' : 'onClick')
+
     this.dispatchEvent(event)
   }
 
   render() {
-    const className = this.shareText.toLocaleLowerCase()
-    return when(
-      this.disabled,
-      () => html`
-        <vscode-button
-          class=${className}
-          disabled
-          appearance="secondary"
-          @click=${this.onShareClick}
-        >
-          ${ShareIcon} ${this.shareText}
-        </vscode-button>
-      `,
-      () => html`
-        <vscode-button class=${className} appearance="secondary" @click=${this.onShareClick}>
-          ${this.displayShareIcon ? ShareIcon : SaveIcon} ${this.shareText}
-        </vscode-button>
-      `,
-    )
+    const className = this.text.toLocaleLowerCase()
+    return html`
+      <vscode-button
+        ?disabled=${this.loading}
+        class=${className}
+        appearance="secondary"
+        @click=${this.onClick}
+      >
+        ${when(this.shareIcon, () => ShareIcon)} ${when(this.saveIcon, () => SaveIcon)}
+        ${when(
+          this.loading,
+          () => html`${this.loadingText}`,
+          () => html`${this.text}`,
+        )}
+      </vscode-button>
+    `
   }
 }
diff --git a/src/client/components/terminal/index.ts b/src/client/components/terminal/index.ts
index 787960f4..f98de618 100644
--- a/src/client/components/terminal/index.ts
+++ b/src/client/components/terminal/index.ts
@@ -1,5 +1,5 @@
 import { LitElement, css, html, PropertyValues, unsafeCSS } from 'lit'
-import { customElement, property } from 'lit/decorators.js'
+import { customElement, property, state } from 'lit/decorators.js'
 import { when } from 'lit/directives/when.js'
 import { Disposable, TerminalDimensions } from 'vscode'
 import { ITheme, Terminal as XTermJS } from '@xterm/xterm'
@@ -16,7 +16,7 @@ import { APIMethod } from '../../../types'
 import type { TerminalConfiguration } from '../../../utils/configuration'
 import '../closeCellButton'
 import '../copyButton'
-import './share'
+import './actionButton'
 import './gistCell'
 import './open'
 import {
@@ -24,6 +24,7 @@ import {
   CreateExtensionCellOutputMutation,
   UpdateCellOutputMutation,
 } from '../../../extension/__generated-platform__/graphql'
+import { isFeatureActive, loadFeatureSnapshot, FeatureObserver } from '../../../features'
 
 interface IWindowSize {
   width: number
@@ -48,6 +49,8 @@ const LISTEN_TO_EVENTS = [
   ClientMessages.optionsMessage,
   ClientMessages.onCopyTextToClipboard,
   ClientMessages.onProgramClose,
+  ClientMessages.featuresResponse,
+  ClientMessages.featuresUpdateAction,
 ]
 
 const ANSI_COLORS = [
@@ -73,10 +76,6 @@ const ANSI_COLORS = [
 @customElement(RENDERERS.TerminalView)
 export class TerminalView extends LitElement {
   protected copyText = 'Copy'
-  protected shareText = 'Save'
-  protected saveText = 'Save'
-  protected shareEnabledText = 'Share'
-  protected escalateEnabledText = 'Escalate'
 
   static styles = css`
     .xterm {
@@ -319,9 +318,16 @@ export class TerminalView extends LitElement {
   protected fitAddon?: FitAddon
   protected serializer?: SerializeAddon
   protected windowSize: IWindowSize
-
   protected rows: number = 10
 
+  protected platformId?: string
+  protected exitCode?: number | void
+  protected isSlackReady?: boolean
+  protected isShareReady: boolean = false
+
+  @state()
+  protected featureState$?: FeatureObserver
+
   @property({ type: String })
   id!: string
 
@@ -356,20 +362,11 @@ export class TerminalView extends LitElement {
   lastLine?: number // TODO: Get the last line of the terminal and store it.
 
   @property({ type: Boolean })
-  isCloudApiLoading: boolean = false
-
-  @property()
-  cloudId?: string
+  isLoading: boolean = false
 
   @property()
   shareUrl?: string
 
-  @property({ type: Boolean })
-  enableShareButton: boolean = false
-
-  @property({ type: Boolean })
-  isShareReady: boolean = false
-
   @property({ type: Boolean })
   isUpdatedReady: boolean = false
 
@@ -449,6 +446,10 @@ export class TerminalView extends LitElement {
         }
 
         switch (e.type) {
+          case ClientMessages.featuresResponse:
+          case ClientMessages.featuresUpdateAction:
+            this.featureState$ = loadFeatureSnapshot(e.output.snapshot)
+            break
           case ClientMessages.activeThemeChanged:
             this.#updateTerminalTheme()
             break
@@ -471,7 +472,7 @@ export class TerminalView extends LitElement {
               if (e.output.id !== this.id) {
                 return
               }
-              this.isCloudApiLoading = false
+              this.isLoading = false
               if (e.output.hasErrors) {
                 return postClientMessage(ctx, ClientMessages.errorMessage, e.output.data)
               }
@@ -480,21 +481,20 @@ export class TerminalView extends LitElement {
                 e.output.data.hasOwnProperty('displayShare') &&
                 e.output.data.displayShare === false
               ) {
-                this.shareText = this.saveText
                 return
               }
 
               const data = (e.output.data?.data || {}) as CreateExtensionCellOutputMutation &
                 CreateCellExecutionMutation &
                 UpdateCellOutputMutation
-              const { escalationButton: escalationButtonEnabled } = e.output
               // TODO: Remove createCellExecution once the transition is complete and tested enough.
               if (data.createExtensionCellOutput || data.createCellExecution) {
                 const objData = data.createCellExecution || data.createExtensionCellOutput || {}
-                const { exitCode, id, htmlUrl } = objData
-                this.cloudId = id
+                const { exitCode, id, htmlUrl, isSlackReady } = objData
+                this.platformId = id
                 this.shareUrl = htmlUrl || ''
-                this.shareText = this.getSecondaryButtonLabel(exitCode, escalationButtonEnabled)
+                this.exitCode = exitCode
+                this.isSlackReady = !!isSlackReady
                 this.isShareReady = true
                 // Dispatch tangle update event
                 return postClientMessage(ctx, ClientMessages.tangleEvent, {
@@ -506,11 +506,12 @@ export class TerminalView extends LitElement {
               }
               if (data.updateCellOutput) {
                 const {
-                  updateCellOutput: { exitCode },
+                  updateCellOutput: { exitCode, isSlackReady },
                 } = data
                 this.isUpdatedReady = true
-                this.shareText = this.getSecondaryButtonLabel(exitCode, escalationButtonEnabled)
-                this.#displayShareDialog(this.shareText)
+                this.exitCode = exitCode
+                this.isSlackReady = !!isSlackReady
+                this.#displayShareDialog()
               }
             }
             break
@@ -521,7 +522,7 @@ export class TerminalView extends LitElement {
                 return
               }
               const answer = e.output.option
-              this.isCloudApiLoading = false
+              this.isLoading = false
               switch (answer) {
                 case MessageOptions.OpenLink: {
                   return postClientMessage(ctx, ClientMessages.openExternalLink, {
@@ -545,12 +546,16 @@ export class TerminalView extends LitElement {
             return postClientMessage(ctx, ClientMessages.infoMessage, 'Link copied!')
           }
           case ClientMessages.onProgramClose: {
-            const { 'runme.dev/id': id, code, escalationButton: escalationButtonEnabled } = e.output
-            if (id !== this.id || !this.isAutoSaveEnabled) {
+            const { 'runme.dev/id': id, code } = e.output
+            if (id !== this.id) {
+              return
+            }
+            this.exitCode = code
+
+            if (!this.isAutoSaveEnabled) {
               return
             }
-            const btnSecondaryText = this.getSecondaryButtonLabel(code, escalationButtonEnabled)
-            this.shareText = this.isAutoSaveEnabled ? btnSecondaryText : this.saveText
+
             return this.#shareCellOutput(false)
           }
         }
@@ -562,13 +567,8 @@ export class TerminalView extends LitElement {
         }),
       ),
     )
-  }
 
-  getSecondaryButtonLabel(code: number | null | void, escalationButtonEnabled = false): string {
-    if (!escalationButtonEnabled) {
-      return this.shareEnabledText
-    }
-    return code === 0 ? this.shareEnabledText : this.escalateEnabledText
+    postClientMessage(ctx, ClientMessages.featuresRequest, {})
   }
 
   disconnectedCallback(): void {
@@ -744,13 +744,12 @@ export class TerminalView extends LitElement {
     })
   }
 
-  async #displayShareDialog(btnText?: string): Promise<boolean | void> {
+  async #displayShareDialog(): Promise<boolean | void> {
     const ctx = getContext()
     if (!ctx.postMessage || !this.shareUrl) {
       return
     }
-    this.shareText = btnText ?? this.shareEnabledText
-    this.isShareReady = true
+
     return postClientMessage(ctx, ClientMessages.optionsMessage, {
       title:
         'Please share link with caution. Anyone with the link has access. Click "Open" to toggle visibility.',
@@ -788,10 +787,10 @@ export class TerminalView extends LitElement {
         return this.#displayShareDialog()
       }
       if (this.isShareReady) {
-        this.isCloudApiLoading = true
+        this.isLoading = true
         await postClientMessage(ctx, ClientMessages.platformApiRequest, {
           data: {
-            id: this.cloudId,
+            id: this.platformId,
           },
           id: this.id!,
           method: APIMethod.UpdateCellExecution,
@@ -799,7 +798,7 @@ export class TerminalView extends LitElement {
         return
       }
 
-      this.isCloudApiLoading = true
+      this.isLoading = true
       const contentWithAnsi =
         this.serializer?.serialize({ excludeModes: true, excludeAltBuffer: true }) ?? ''
       await postClientMessage(ctx, ClientMessages.platformApiRequest, {
@@ -811,7 +810,7 @@ export class TerminalView extends LitElement {
         method: APIMethod.CreateCellExecution,
       })
     } catch (error) {
-      this.isCloudApiLoading = false
+      this.isLoading = false
       postClientMessage(
         ctx,
         ClientMessages.infoMessage,
@@ -828,6 +827,11 @@ export class TerminalView extends LitElement {
     postClientMessage(getContext(), ClientMessages.openLink, this.shareUrl!)
   }
 
+  #onEscalateDisabled(): void {
+    const message = 'There is no Slack integration configured yet. \nOpen Dashboard to configure it'
+    postClientMessage(getContext(), ClientMessages.errorMessage, message)
+  }
+
   // Render the UI as a function of component state
   render() {
     return html`<section>
@@ -848,31 +852,48 @@ export class TerminalView extends LitElement {
           }}"
         ></copy-button>
         ${when(
-          this.isSessionOutputsEnabled && !this.isPlatformAuthEnabled,
+          this.isSessionOutputsEnabled && isFeatureActive(this.featureState$, 'Gist'),
           () => {
             return html`<gist-cell @onGist="${this.#openSessionOutput}"></gist-cell>`
           },
           () => {},
         )}
         ${when(
-          this.enableShareButton,
+          (this.exitCode === undefined || this.exitCode === 0 || !this.platformId) &&
+            isFeatureActive(this.featureState$, 'Share'),
+          () => {
+            return html` <action-button
+              ?loading=${this.isLoading}
+              ?shareIcon="${true}"
+              text="Save"
+              @onClick="${this.#triggerShareCellOutput}"
+            >
+            </action-button>`
+          },
+          () => {},
+        )}
+        ${when(
+          this.exitCode !== 0 && this.platformId && isFeatureActive(this.featureState$, 'Escalate'),
           () => {
-            return html` <share-cell
-              ?disabled=${this.isCloudApiLoading}
-              ?displayShareIcon=${this.isShareReady}
-              shareText="${this.isCloudApiLoading ? 'Saving ...' : this.shareText}"
-              @onShare="${this.#triggerShareCellOutput}"
+            return html` <action-button
+              ?disabled=${!this.isSlackReady}
+              ?loading=${this.isLoading}
+              ?saveIcon="${true}"
+              text="Escalate"
+              @onClick="${this.#triggerShareCellOutput}"
+              @onClickDisabled=${this.#onEscalateDisabled}
             >
-            </share-cell>`
+            </action-button>`
           },
           () => {},
         )}
         ${when(
-          this.cloudId && !this.isCloudApiLoading,
+          this.platformId && !this.isLoading,
           () => {
             return html` <open-cell
-            ?disabled=${!this.isPlatformAuthEnabled}
-            @onOpen="${this.#triggerOpenCellOutput}"></share-cell>`
+              ?disabled=${!this.isPlatformAuthEnabled}
+              @onOpen="${this.#triggerOpenCellOutput}"
+            ></open-cell>`
           },
           () => {},
         )}
diff --git a/src/client/index.ts b/src/client/index.ts
index 9bc0b582..5a533622 100644
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@ -121,12 +121,6 @@ export const activate: ActivationFunction<void> = (context) => {
           if (typeof payload.output.scrollback === 'number') {
             terminalElement.setAttribute('scrollback', payload.output.scrollback.toString())
           }
-          if (payload.output.enableShareButton) {
-            terminalElement.setAttribute(
-              'enableShareButton',
-              payload.output.enableShareButton.toString(),
-            )
-          }
           if (payload.output.initialRows !== undefined) {
             terminalElement.setAttribute('initialRows', payload.output.initialRows.toString())
           }
@@ -135,7 +129,7 @@ export const activate: ActivationFunction<void> = (context) => {
             terminalElement.setAttribute('initialContent', payload.output.content)
           }
 
-          if (payload.output.isAutoSaveEnabled && payload.output.enableShareButton) {
+          if (payload.output.isAutoSaveEnabled) {
             terminalElement.setAttribute(
               'isAutoSaveEnabled',
               payload.output.isAutoSaveEnabled.toString(),
diff --git a/src/constants.ts b/src/constants.ts
index e12a41b2..19c94f27 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -68,6 +68,9 @@ export enum ClientMessages {
   gcpServicesLoaded = 'gcp:servicesLoaded',
   daggerSyncState = 'dagger:syncState',
   daggerCliAction = 'dagger:cliAction',
+  featuresUpdateAction = 'features:updateAction',
+  featuresRequest = 'features:request',
+  featuresResponse = 'features:response',
 }
 
 // [pretty print, languageId, destination]
@@ -1110,7 +1113,7 @@ export const CATEGORY_SEPARATOR = ','
 export const EXECUTION_CELL_STORAGE_KEY = 'executionCell'
 export const EXECUTION_CELL_CREATION_DATE_STORAGE_KEY = 'executionCellCreationDate'
 export const SAVE_CELL_LOGIN_CONSENT_STORAGE_KEY = 'loginConsent'
-export const CLOUD_USER_SIGNED_IN = 'userSignedIn'
+export const GITHUB_USER_SIGNED_IN = 'userSignedIn'
 export const PLATFORM_USER_SIGNED_IN = 'platformUserSignedIn'
 
 export const RUNME_FRONTMATTER_PARSED = 'runme.dev/frontmatterParsed'
diff --git a/src/extension/cell.ts b/src/extension/cell.ts
index 44bcd78f..821a9b5a 100644
--- a/src/extension/cell.ts
+++ b/src/extension/cell.ts
@@ -16,7 +16,7 @@ import {
 } from 'vscode'
 
 import {
-  CLOUD_USER_SIGNED_IN,
+  GITHUB_USER_SIGNED_IN,
   NOTEBOOK_AUTOSAVE_ON,
   OutputType,
   PLATFORM_USER_SIGNED_IN,
@@ -36,7 +36,6 @@ import {
   getNotebookTerminalConfigurations,
   getSessionOutputs,
   isPlatformAuthEnabled,
-  isRunmeAppButtonsEnabled,
 } from '../utils/configuration'
 
 import { RUNME_TRANSIENT_REVISION } from './constants'
@@ -245,7 +244,7 @@ export class NotebookCellOutputManager {
 
             const isSignedIn = isPlatformAuthEnabled()
               ? ContextState.getKey(PLATFORM_USER_SIGNED_IN)
-              : ContextState.getKey(CLOUD_USER_SIGNED_IN)
+              : ContextState.getKey(GITHUB_USER_SIGNED_IN)
 
             const isSessionOutputsEnabled = getSessionOutputs()
 
@@ -255,7 +254,6 @@ export class NotebookCellOutputManager {
                 'runme.dev/id': cellId,
                 content: stdoutBase64,
                 initialRows: terminalRows || terminalConfigurations.rows,
-                enableShareButton: isRunmeAppButtonsEnabled(),
                 isAutoSaveEnabled: isSignedIn ? ContextState.getKey(NOTEBOOK_AUTOSAVE_ON) : false,
                 isPlatformAuthEnabled: isPlatformAuthEnabled(),
                 isSessionOutputsEnabled,
diff --git a/src/extension/commands/index.ts b/src/extension/commands/index.ts
index 32bfdb42..16caeaad 100644
--- a/src/extension/commands/index.ts
+++ b/src/extension/commands/index.ts
@@ -27,7 +27,6 @@ import {
   getCLIUseIntegratedRunme,
   getTLSEnabled,
   isNotebookTerminalEnabledForCell,
-  isRunmeAppButtonsEnabled,
 } from '../../utils/configuration'
 import { Kernel } from '../kernel'
 import {
@@ -383,7 +382,7 @@ export async function addToRecommendedExtensions(context: ExtensionContext) {
 }
 
 export async function toggleAutosave(autoSaveIsOn: boolean) {
-  if (autoSaveIsOn && isRunmeAppButtonsEnabled()) {
+  if (autoSaveIsOn) {
     await promptUserSession()
   }
   return ContextState.addKey(NOTEBOOK_AUTOSAVE_ON, autoSaveIsOn)
diff --git a/src/extension/executors/runner/index.ts b/src/extension/executors/runner/index.ts
index bee0cddf..4f603ef2 100644
--- a/src/extension/executors/runner/index.ts
+++ b/src/extension/executors/runner/index.ts
@@ -193,7 +193,6 @@ export const executeRunner: IKernelRunner = async ({
     postClientMessage(messaging, ClientMessages.onProgramClose, {
       'runme.dev/id': cellId,
       code,
-      escalationButton: kernel.hasExperimentEnabled('escalationButton', false)!,
     })
     if (!background) {
       return
diff --git a/src/extension/extension.ts b/src/extension/extension.ts
index 11f1ae1f..61e2bc24 100644
--- a/src/extension/extension.ts
+++ b/src/extension/extension.ts
@@ -9,6 +9,7 @@ import {
   env,
   Uri,
   NotebookCell,
+  authentication,
 } from 'vscode'
 import { TelemetryReporter } from 'vscode-telemetry'
 import Channel from 'tangle/webviews'
@@ -21,7 +22,7 @@ import {
   getSessionOutputs,
   isPlatformAuthEnabled,
 } from '../utils/configuration'
-import { WebViews } from '../constants'
+import { AuthenticationProviders, WebViews } from '../constants'
 
 import { Kernel } from './kernel'
 import KernelServer from './server/kernelServer'
@@ -36,6 +37,7 @@ import {
   bootFile,
   resetNotebookSettings,
   getPlatformAuthSession,
+  getGithubAuthSession,
   openFileAsRunmeNotebook,
 } from './utils'
 import { RunmeTaskProvider } from './provider/runmeTask'
@@ -73,7 +75,7 @@ import * as survey from './survey'
 import { RunmeCodeLensProvider } from './provider/codelens'
 import CloudPanel from './panels/cloud'
 import { createDemoFileRunnerForActiveNotebook, createDemoFileRunnerWatcher } from './handler/utils'
-import { CloudAuthProvider } from './provider/cloudAuth'
+import { GithubAuthProvider } from './provider/githubAuth'
 import { StatefulAuthProvider } from './provider/statefulAuth'
 import { IPanel } from './panels/base'
 import { NotebookPanel as EnvStorePanel } from './panels/notebook'
@@ -341,26 +343,6 @@ export class RunmeExtension {
       ),
     )
 
-    if (isPlatformAuthEnabled()) {
-      context.subscriptions.push(new StatefulAuthProvider(context, uriHandler))
-      const session = await getPlatformAuthSession(true)
-      if (session) {
-        const openDashboardStr = 'Open Dashboard'
-        const answer = await window.showInformationMessage(
-          'Logged into the Stateful Platform',
-          openDashboardStr,
-        )
-
-        if (answer === openDashboardStr) {
-          const dashboardUri = getRunmeAppUrl(['app'])
-          const uri = Uri.parse(dashboardUri)
-          env.openExternal(uri)
-        }
-      }
-    } else {
-      context.subscriptions.push(new CloudAuthProvider(context))
-    }
-
     await bootFile(context)
 
     if (kernel.hasExperimentEnabled('shellWarning', false)) {
@@ -374,7 +356,7 @@ export class RunmeExtension {
         }
       }
 
-      const logger = getLogger('runme.beta.shellWarning')
+      const logger = getLogger('runme.experiments.shellWarning')
 
       kernel
         .runProgram('echo $SHELL')
@@ -396,6 +378,42 @@ export class RunmeExtension {
           showUnsupportedShellMessage()
         })
     }
+
+    if (isPlatformAuthEnabled()) {
+      context.subscriptions.push(new StatefulAuthProvider(context, uriHandler))
+      getPlatformAuthSession(true).then((session) => {
+        if (session) {
+          const openDashboardStr = 'Open Dashboard'
+          window
+            .showInformationMessage('Logged into the Stateful Platform', openDashboardStr)
+            .then((answer) => {
+              if (answer === openDashboardStr) {
+                const dashboardUri = getRunmeAppUrl(['app'])
+                const uri = Uri.parse(dashboardUri)
+                env.openExternal(uri)
+              }
+            })
+        }
+      })
+    } else {
+      context.subscriptions.push(new GithubAuthProvider(context))
+      getGithubAuthSession(false).then((session) => {
+        kernel.updateFeatureState('githubAuth', !!session)
+      })
+    }
+
+    authentication.onDidChangeSessions((e) => {
+      if (e.provider.id === AuthenticationProviders.Stateful) {
+        getPlatformAuthSession(false).then((session) => {
+          kernel.updateFeatureState('statefulAuth', !!session)
+        })
+      }
+      if (e.provider.id === AuthenticationProviders.GitHub) {
+        getGithubAuthSession(false).then((session) => {
+          kernel.updateFeatureState('githubAuth', !!session)
+        })
+      }
+    })
   }
 
   protected handleMasking(kernel: Kernel, maskingIsOn: boolean): (e: NotebookUiEvent) => void {
diff --git a/src/extension/kernel.ts b/src/extension/kernel.ts
index 0990621f..213a04b6 100644
--- a/src/extension/kernel.ts
+++ b/src/extension/kernel.ts
@@ -1,4 +1,5 @@
 import path from 'node:path'
+import os from 'node:os'
 
 import {
   Disposable,
@@ -22,9 +23,11 @@ import {
   NotebookEditorSelectionChangeEvent,
   CancellationToken,
   NotebookData,
+  version,
 } from 'vscode'
 import { TelemetryReporter } from 'vscode-telemetry'
 import { UnaryCall } from '@protobuf-ts/runtime-rpc'
+import { map } from 'rxjs/operators'
 
 import {
   type ActiveTerminal,
@@ -45,6 +48,14 @@ import {
 import { API } from '../utils/deno/api'
 import { postClientMessage } from '../utils/messaging'
 import { getNotebookExecutionOrder, registerExtensionEnvVarsMutation } from '../utils/configuration'
+import {
+  isFeatureActive,
+  FeatureContext,
+  getFeatureSnapshot,
+  loadFeaturesState,
+  updateFeatureContext,
+  FEATURES_CONTEXT_STATE_KEY,
+} from '../features'
 
 import getLogger from './logger'
 import executor, {
@@ -112,6 +123,7 @@ export class Kernel implements Disposable {
   static readonly type = 'runme' as const
 
   readonly #experiments = new Map<string, boolean>()
+  readonly #featuresSettings = new Map<string, boolean>()
 
   #disposables: Disposable[] = []
   #controller = notebooks.createNotebookController(
@@ -132,22 +144,23 @@ export class Kernel implements Disposable {
   protected panelManager: PanelManager
   protected serializer?: SerializerBase
   protected reporter?: GrpcReporter
+  protected featuresState$?
 
   readonly onVarsChangeEvent: EnvVarsChangedEvent
 
   constructor(protected context: ExtensionContext) {
     const config = workspace.getConfiguration('runme.experiments')
+    const features = workspace.getConfiguration('runme.features')
 
     this.onVarsChangeEvent = new EnvVarsChangedEvent()
-
     this.#experiments.set('grpcSerializer', config.get<boolean>('grpcSerializer', true))
     this.#experiments.set('grpcRunner', config.get<boolean>('grpcRunner', true))
     this.#experiments.set('grpcServer', config.get<boolean>('grpcServer', true))
-    this.#experiments.set('escalationButton', config.get<boolean>('escalationButton', false))
     this.#experiments.set('smartEnvStore', config.get<boolean>('smartEnvStore', false))
     this.#experiments.set('aiLogs', config.get<boolean>('aiLogs', false))
     this.#experiments.set('shellWarning', config.get<boolean>('shellWarning', false))
     this.#experiments.set('reporter', config.get<boolean>('reporter', false))
+    this.#featuresSettings.set('escalate', features.get<boolean>('escalate', false))
 
     this.cellManager = new NotebookCellManager(this.#controller)
     this.#controller.supportsExecutionOrder = getNotebookExecutionOrder()
@@ -170,6 +183,7 @@ export class Kernel implements Disposable {
 
     this.messaging.postMessage({ from: 'kernel' })
     this.panelManager = new PanelManager(context)
+
     this.#disposables.push(
       this.messaging.onDidReceiveMessage(this.#handleRendererMessage.bind(this)),
       workspace.onDidOpenNotebookDocument(this.#handleOpenNotebook.bind(this)),
@@ -180,6 +194,45 @@ export class Kernel implements Disposable {
       this.onVarsChangeEvent,
       this.registerTerminalProfile(),
     )
+
+    const packageJSON = context?.extension?.packageJSON || {}
+    const featContext: FeatureContext = {
+      os: os.platform(),
+      vsCodeVersion: version as string,
+      extensionVersion: packageJSON?.version,
+      extensionId: context?.extension?.id,
+      githubAuth: false,
+      statefulAuth: false,
+    }
+
+    this.featuresState$ = loadFeaturesState(packageJSON, featContext, this.#featuresSettings)
+
+    if (this.featuresState$) {
+      const subscription = this.featuresState$
+        .pipe(map((_state) => getFeatureSnapshot(this.featuresState$)))
+        .subscribe((snapshot) => {
+          ContextState.addKey(FEATURES_CONTEXT_STATE_KEY, snapshot)
+          postClientMessage(this.messaging, ClientMessages.featuresUpdateAction, {
+            snapshot: snapshot,
+          })
+        })
+
+      this.#disposables.push({
+        dispose: () => subscription.unsubscribe(),
+      })
+    }
+  }
+
+  isFeatureActive(featureName: string): boolean {
+    if (!this.featuresState$) {
+      return false
+    }
+
+    return isFeatureActive(this.featuresState$, featureName)
+  }
+
+  updateFeatureState<K extends keyof FeatureContext>(key: K, value: FeatureContext[K]) {
+    updateFeatureContext(this.featuresState$, key, value, this.#featuresSettings)
   }
 
   registerNotebookCell(cell: NotebookCell) {
@@ -569,6 +622,12 @@ export class Kernel implements Disposable {
       return insertCodeCell(message.output.cellId, editor, cellText, 'sh', false)
     } else if (message.type.startsWith('terminal:')) {
       return
+    } else if (message.type === ClientMessages.featuresRequest) {
+      const snapshot = getFeatureSnapshot(this.featuresState$)
+      postClientMessage(this.messaging, ClientMessages.featuresResponse, {
+        snapshot: snapshot,
+      })
+      return
     }
 
     log.error(`Unknown kernel event type: ${message.type}`)
diff --git a/src/extension/messages/platformRequest/saveCellExecution.ts b/src/extension/messages/platformRequest/saveCellExecution.ts
index bd312909..00f01700 100644
--- a/src/extension/messages/platformRequest/saveCellExecution.ts
+++ b/src/extension/messages/platformRequest/saveCellExecution.ts
@@ -41,13 +41,10 @@ export default async function saveCellExecution(
 
   log.info('Saving cell execution')
 
-  const escalationButton = kernel.hasExperimentEnabled('escalationButton', false)!
   const cacheId = GrpcSerializer.getDocumentCacheId(editor.notebook.metadata) as string
   const plainSessionOutput = await kernel.getPlainCache(cacheId)
   const maskedSessionOutput = await kernel.getMaskedCache(cacheId)
 
-  log.info(`escalationButton: ${escalationButton ? 'enabled' : 'disabled'}`)
-
   const vsEnv = {
     appHost: env.appHost,
     appName: env.appName,
@@ -75,14 +72,12 @@ export default async function saveCellExecution(
     let data:
       | FetchResult<CreateExtensionCellOutputMutation>
       | FetchResult<CreateCellExecutionMutation>
-    let showEscalationButton: boolean
 
     if (!session) {
       return postClientMessage(messaging, ClientMessages.platformApiResponse, {
         data: {
           displayShare: false,
         },
-        escalationButton,
         id: message.output.id,
       })
     }
@@ -154,8 +149,6 @@ export default async function saveCellExecution(
         },
       })
       data = result
-      showEscalationButton = !!(result.data as CreateExtensionCellOutputMutation)
-        ?.createExtensionCellOutput?.isSlackReady
     }
     // TODO: Remove the legacy createCellExecution mutation once the reporter is fully tested.
     else {
@@ -252,19 +245,14 @@ export default async function saveCellExecution(
       })
 
       data = result
-      showEscalationButton = !!(result.data as CreateCellExecutionMutation)?.createCellExecution
-        ?.isSlackReady
     }
 
     log.info('Cell execution saved')
 
-    log.info(`showEscalationButton: ${showEscalationButton ? 'enabled' : 'disabled'}`)
-
     TelemetryReporter.sendTelemetryEvent('app.save')
     return postClientMessage(messaging, ClientMessages.platformApiResponse, {
       data,
       id: message.output.id,
-      escalationButton: showEscalationButton,
     })
   } catch (error) {
     log.error('Error saving cell execution', (error as Error).message)
@@ -272,7 +260,6 @@ export default async function saveCellExecution(
     return postClientMessage(messaging, ClientMessages.platformApiResponse, {
       data: (error as any).message,
       id: message.output.id,
-      escalationButton,
       hasErrors: true,
     })
   }
diff --git a/src/extension/messages/platformRequest/updateCellExecution.ts b/src/extension/messages/platformRequest/updateCellExecution.ts
index 9bdeb623..a6164058 100644
--- a/src/extension/messages/platformRequest/updateCellExecution.ts
+++ b/src/extension/messages/platformRequest/updateCellExecution.ts
@@ -15,14 +15,11 @@ const log = getLogger('UpdateCell')
 
 export default async function updateCellExecution(
   requestMessage: APIRequestMessage,
-  kernel: Kernel,
+  _kernel: Kernel,
 ): Promise<void | boolean> {
   const { messaging, message } = requestMessage
   log.info('Updating cell execution', message.output.data.id)
 
-  const escalationButton = kernel.hasExperimentEnabled('escalationButton', false)!
-  log.info(`escalationButton: ${escalationButton ? 'enabled' : 'disabled'}`, message.output.data.id)
-
   try {
     const session = await getPlatformAuthSession()
 
@@ -43,17 +40,10 @@ export default async function updateCellExecution(
     })
     log.info('Cell execution updated', message.output.data.id)
 
-    const showEscalationButton = !!result.data?.updateCellOutput?.isSlackReady
-    log.info(
-      `showEscalationButton: ${showEscalationButton ? 'enabled' : 'disabled'}`,
-      message.output.data.id,
-    )
-
     TelemetryReporter.sendTelemetryEvent('app.update')
     return postClientMessage(messaging, ClientMessages.platformApiResponse, {
       data: result,
       id: message.output.id,
-      escalationButton: showEscalationButton,
     })
   } catch (error) {
     log.error('Error updating cell execution', message.output.data.id, (error as Error).message)
@@ -61,7 +51,6 @@ export default async function updateCellExecution(
     return postClientMessage(messaging, ClientMessages.platformApiResponse, {
       data: (error as any).message,
       id: message.output.id,
-      escalationButton,
       hasErrors: true,
     })
   }
diff --git a/src/extension/provider/cellStatusBar/items/fork.ts b/src/extension/provider/cellStatusBar/items/fork.ts
index b9520a80..aaddc018 100644
--- a/src/extension/provider/cellStatusBar/items/fork.ts
+++ b/src/extension/provider/cellStatusBar/items/fork.ts
@@ -5,7 +5,7 @@ import CellStatusBarItem from './cellStatusBarItem'
 export class ForkStatusBarItem extends CellStatusBarItem {
   getStatusBarItem(_cell: NotebookCell): NotebookCellStatusBarItem | undefined {
     const item = new NotebookCellStatusBarItem(
-      '$(github-action) Fork ENV',
+      '$(github-action) Fork',
       NotebookCellStatusBarAlignment.Right,
     )
     item.command = 'runme.runForkCommand'
diff --git a/src/extension/provider/cloudAuth.ts b/src/extension/provider/githubAuth.ts
similarity index 54%
rename from src/extension/provider/cloudAuth.ts
rename to src/extension/provider/githubAuth.ts
index 5a27745b..c6ccfe49 100644
--- a/src/extension/provider/cloudAuth.ts
+++ b/src/extension/provider/githubAuth.ts
@@ -1,13 +1,13 @@
 import { Disposable, ExtensionContext, authentication } from 'vscode'
 
 import { checkSession } from '../utils'
-import { CLOUD_USER_SIGNED_IN } from '../../constants'
+import { GITHUB_USER_SIGNED_IN } from '../../constants'
 import ContextState from '../contextState'
 
-export class CloudAuthProvider implements Disposable {
+export class GithubAuthProvider implements Disposable {
   constructor(context: ExtensionContext) {
-    const userSignedIn = context.globalState.get(CLOUD_USER_SIGNED_IN, false)
-    ContextState.addKey(CLOUD_USER_SIGNED_IN, userSignedIn)
+    const userSignedIn = context.globalState.get(GITHUB_USER_SIGNED_IN, false)
+    ContextState.addKey(GITHUB_USER_SIGNED_IN, userSignedIn)
     authentication.onDidChangeSessions(() => {
       checkSession(context)
     })
diff --git a/src/extension/utils.ts b/src/extension/utils.ts
index 47339072..35c785ad 100644
--- a/src/extension/utils.ts
+++ b/src/extension/utils.ts
@@ -41,7 +41,7 @@ import {
   SERVER_ADDRESS,
   CATEGORY_SEPARATOR,
   NOTEBOOK_AUTOSAVE_ON,
-  CLOUD_USER_SIGNED_IN,
+  GITHUB_USER_SIGNED_IN,
   NOTEBOOK_OUTPUTS_MASKED,
 } from '../constants'
 import {
@@ -55,8 +55,8 @@ import {
   getTLSDir,
   getTLSEnabled,
   isPlatformAuthEnabled,
-  isRunmeAppButtonsEnabled,
 } from '../utils/configuration'
+import { FEATURES_CONTEXT_STATE_KEY, isFeatureActive, loadFeatureSnapshot } from '../features'
 
 import CategoryQuickPickItem from './quickPickItems/category'
 import getLogger from './logger'
@@ -65,7 +65,6 @@ import { BOOTFILE, BOOTFILE_DEMO } from './constants'
 import { IRunnerEnvironment } from './runner/environment'
 import { setCurrentCellExecutionDemo } from './handler/utils'
 import ContextState from './contextState'
-import { RunmeService } from './services/runme'
 import { GCPResolver } from './resolvers/gcpResolver'
 import { AWSResolver } from './resolvers/awsResolver'
 
@@ -552,7 +551,7 @@ export function convertEnvList(envs: string[]): Record<string, string | undefine
   )
 }
 
-export function getAuthSession(createIfNone: boolean = true) {
+export function getGithubAuthSession(createIfNone: boolean = true) {
   return authentication.getSession(AuthenticationProviders.GitHub, ['user:email'], {
     createIfNone,
   })
@@ -567,48 +566,20 @@ export async function getPlatformAuthSession(createIfNone: boolean = true) {
 
 export async function resolveAuthToken(createIfNone: boolean = true) {
   let session: AuthenticationSession | undefined
-
-  if (isPlatformAuthEnabled()) {
-    session = await getPlatformAuthSession(createIfNone)
-    if (!session) {
-      throw new Error('You must authenticate with your Stateful account')
-    }
-
-    return session.accessToken
-  }
-
-  session = await getAuthSession(createIfNone)
+  session = await getPlatformAuthSession(createIfNone)
   if (!session) {
-    throw new Error('You must authenticate with your GitHub account')
-  }
-
-  const service = new RunmeService({ githubAccessToken: session.accessToken })
-  const response = await service.getUserToken()
-  if (!response) {
-    throw new Error('Unable to retrieve an access token')
+    throw new Error('You must authenticate with your Stateful account')
   }
 
-  return response.token
+  return session.accessToken
 }
 
 export async function resolveAppToken(createIfNone: boolean = true) {
-  if (isPlatformAuthEnabled()) {
-    const session = await getPlatformAuthSession(createIfNone)
-    if (!session) {
-      return null
-    }
-    return { token: session.accessToken }
-  }
-
-  const session = await getAuthSession(createIfNone)
-
-  if (session) {
-    const service = new RunmeService({ githubAccessToken: session.accessToken })
-    const userToken = await service.getUserToken()
-    return await service.getAppToken(userToken)
+  const session = await getPlatformAuthSession(createIfNone)
+  if (!session) {
+    return null
   }
-
-  return null
+  return { token: session.accessToken }
 }
 
 export function fetchStaticHtml(appUrl: string) {
@@ -735,7 +706,7 @@ export async function resolveUserSession(
 ): Promise<AuthenticationSession | undefined> {
   return isPlatformAuthEnabled()
     ? await getPlatformAuthSession(createIfNone)
-    : await getAuthSession(createIfNone)
+    : await getGithubAuthSession(createIfNone)
 }
 
 /**
@@ -744,13 +715,20 @@ export async function resolveUserSession(
  * This only happens once. Subsequent saves will not display the prompt.
  * @returns AuthenticationSession
  */
-export async function promptUserSession(): Promise<AuthenticationSession | undefined> {
-  let session: AuthenticationSession | undefined = await resolveUserSession(false)
-  const displayLoginPrompt = getLoginPrompt() && isRunmeAppButtonsEnabled()
+export async function promptUserSession() {
+  let session = await resolveUserSession(false)
+  // if (session) {
+  //   return Promise.resolve()
+  // }
+
+  const provider = isPlatformAuthEnabled() ? 'Stateful' : 'GitHub'
+  const featureState$ = loadFeatureSnapshot(ContextState.getKey(FEATURES_CONTEXT_STATE_KEY))
+  const displayLoginPrompt = getLoginPrompt() && isFeatureActive(featureState$, 'Share')
+
   if (!session && displayLoginPrompt !== false) {
     const option = await window.showInformationMessage(
       `Securely store your cell outputs.
-      Sign in with ${isPlatformAuthEnabled() ? 'Stateful' : 'GitHub'} is required, do you want to proceed?`,
+      Sign in with ${provider} is required, do you want to proceed?`,
       'Yes',
       'No',
       'Open Settings',
@@ -765,17 +743,17 @@ export async function promptUserSession(): Promise<AuthenticationSession | undef
 
     session = await resolveUserSession(true)
     if (!session) {
-      throw new Error('You must authenticate with your GitHub account')
+      throw new Error(`You must authenticate with your ${provider} account`)
     }
   }
 
-  return session
+  // return session
 }
 
 export async function checkSession(context: ExtensionContext) {
-  const session = await getAuthSession(false)
-  context.globalState.update(CLOUD_USER_SIGNED_IN, !!session)
-  ContextState.addKey(CLOUD_USER_SIGNED_IN, !!session)
+  const session = await getGithubAuthSession(false)
+  context.globalState.update(GITHUB_USER_SIGNED_IN, !!session)
+  ContextState.addKey(GITHUB_USER_SIGNED_IN, !!session)
 }
 
 export function editJsonc(
diff --git a/src/features.ts b/src/features.ts
new file mode 100644
index 00000000..fad26bd7
--- /dev/null
+++ b/src/features.ts
@@ -0,0 +1,237 @@
+import { BehaviorSubject } from 'rxjs'
+import { satisfies } from 'semver'
+
+export const FEATURES_CONTEXT_STATE_KEY = 'features'
+
+export type FeatureContext = {
+  os?: string
+  vsCodeVersion?: string
+  runmeVersion?: string
+  extensionVersion?: string
+  githubAuth?: boolean // `true`, `false`, or `undefined`
+  statefulAuth?: boolean // `true`, `false`, or `undefined`
+  extensionId?: string
+}
+
+export type FeatureCondition = {
+  os?: string
+  vsCodeVersion?: string
+  runmeVersion?: string
+  extensionVersion?: string
+  githubAuthRequired?: boolean
+  statefulAuthRequired?: boolean
+  allowedExtensions?: string[]
+}
+
+export type Feature = {
+  name: string
+  enabled: boolean
+  activated: boolean
+  conditions: FeatureCondition
+}
+
+export type FeatureState = {
+  features: Feature[]
+  context?: FeatureContext
+}
+
+export type FeatureObserver = BehaviorSubject<FeatureState>
+
+function loadFeaturesFromPackageJson(packageJSON: any): Feature[] {
+  const features = (packageJSON?.runme?.features || []) as Feature[]
+  return features
+}
+
+export function loadFeaturesState(
+  packageJSON: any,
+  context?: FeatureContext,
+  overrides: Map<string, boolean> = new Map(),
+): FeatureObserver {
+  const initialFeatures: Feature[] = loadFeaturesFromPackageJson(packageJSON)
+  const state = new BehaviorSubject<FeatureState>({
+    features: initialFeatures,
+    context,
+  })
+  updateFeatureState(state, context, overrides)
+  return state
+}
+
+function checkEnabled(enabled?: boolean, contextEnabled?: boolean): boolean {
+  return enabled === true || contextEnabled === true
+}
+
+function checkOS(os?: string, contextOS?: string): boolean {
+  return !os || os === 'All' || contextOS === os
+}
+
+function checkVersion(requiredVersion?: string, actualVersion?: string): boolean {
+  if (!requiredVersion) {
+    return true
+  }
+
+  if (!actualVersion) {
+    return true
+  }
+
+  return satisfies(actualVersion, requiredVersion)
+}
+
+function checkAuth(required?: boolean, isAuthenticated?: boolean): boolean {
+  if (!required) {
+    return true
+  }
+
+  if (!isAuthenticated) {
+    return false
+  }
+
+  return true
+}
+
+function checkExtensionId(allowedExtensions?: string[], extensionId?: string): boolean {
+  if (!allowedExtensions?.length) {
+    return true
+  }
+
+  if (!extensionId) {
+    return false
+  }
+
+  return allowedExtensions.includes(extensionId)
+}
+
+function isActive(
+  feature: Feature,
+  context?: FeatureContext,
+  overrides: Map<string, boolean> = new Map(),
+): boolean {
+  const {
+    os,
+    vsCodeVersion,
+    runmeVersion,
+    extensionVersion,
+    githubAuthRequired,
+    statefulAuthRequired,
+    allowedExtensions,
+  } = feature.conditions
+
+  if (!checkEnabled(feature.enabled, overrides.get(feature.name) ?? false)) {
+    console.log(`Feature "${feature.name}" is inactive due to checkEnabled.`)
+    return false
+  }
+
+  if (!checkOS(os, context?.os)) {
+    console.log(
+      `Feature "${feature.name}" is inactive due to checkOS. Expected OS: ${os}, actual OS: ${context?.os}`,
+    )
+    return false
+  }
+
+  if (!checkVersion(vsCodeVersion, context?.vsCodeVersion)) {
+    console.log(
+      `Feature "${feature.name}" is inactive due to checkVersion (vsCodeVersion). Expected: ${vsCodeVersion}, actual: ${context?.vsCodeVersion}`,
+    )
+    return false
+  }
+
+  if (!checkVersion(runmeVersion, context?.runmeVersion)) {
+    console.log(
+      `Feature "${feature.name}" is inactive due to checkVersion (runmeVersion). Expected: ${runmeVersion}, actual: ${context?.runmeVersion}`,
+    )
+    return false
+  }
+
+  if (!checkVersion(extensionVersion, context?.extensionVersion)) {
+    console.log(
+      `Feature "${feature.name}" is inactive due to checkVersion (extensionVersion). Expected: ${extensionVersion}, actual: ${context?.extensionVersion}`,
+    )
+    return false
+  }
+
+  if (!checkAuth(githubAuthRequired, context?.githubAuth)) {
+    console.log(
+      `Feature "${feature.name}" is inactive due to checkAuth (githubAuth). Required: ${githubAuthRequired}, actual: ${context?.githubAuth}`,
+    )
+    return false
+  }
+
+  if (!checkAuth(statefulAuthRequired, context?.statefulAuth)) {
+    console.log(
+      `Feature "${feature.name}" is inactive due to checkAuth (statefulAuth). Required: ${statefulAuthRequired}, actual: ${context?.statefulAuth}`,
+    )
+    return false
+  }
+
+  if (!checkExtensionId(allowedExtensions, context?.extensionId)) {
+    console.log(
+      `Feature "${feature.name}" is inactive due to checkExtensionId. Expected: ${allowedExtensions}, actual: ${context?.extensionId}`,
+    )
+    return false
+  }
+
+  console.log(`Feature "${feature.name} is active`)
+  return true
+}
+
+export function updateFeatureState(
+  featureState$: FeatureObserver,
+  context?: FeatureContext,
+  overrides: Map<string, boolean> = new Map(),
+) {
+  const currentState = featureState$.getValue()
+  const updatedFeatures = currentState.features.map((feature) => ({
+    ...feature,
+    activated: isActive(feature, context, overrides),
+  }))
+  featureState$.next({ features: updatedFeatures, context })
+  return featureState$
+}
+
+export function updateFeatureContext<K extends keyof FeatureContext>(
+  featureState$: FeatureObserver | undefined,
+  key: K,
+  value: FeatureContext[K],
+  overrides: Map<string, boolean>,
+) {
+  if (!featureState$) {
+    return
+  }
+  const currentState = featureState$.getValue()
+  const newContext = { ...(currentState.context || {}), [key]: value }
+
+  if (newContext[key] !== currentState?.context?.[key]) {
+    updateFeatureState(featureState$, newContext, overrides)
+  }
+}
+
+export function getFeatureSnapshot(featureState$: FeatureObserver | undefined): string {
+  if (!featureState$) {
+    return ''
+  }
+
+  return JSON.stringify(featureState$.getValue())
+}
+
+export function loadFeatureSnapshot(snapshot: string): FeatureObserver {
+  const { features, context } = JSON.parse(snapshot)
+  const featureState$ = new BehaviorSubject<FeatureState>({
+    features,
+    context,
+  })
+
+  featureState$.next({ features, context: featureState$.getValue().context })
+  return featureState$
+}
+
+export function isFeatureActive(featureState$?: FeatureObserver, featureName?: string): boolean {
+  if (!featureState$) {
+    return false
+  }
+
+  if (!featureName) {
+    return false
+  }
+
+  const feature = featureState$.getValue().features.find((f) => f.name === featureName)
+  return feature ? feature.activated : false
+}
diff --git a/src/types.ts b/src/types.ts
index d9081918..f2f07c80 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -377,7 +377,6 @@ interface Payload {
     ['runme.dev/id']: string
     content?: string
     initialRows?: number
-    enableShareButton: boolean
     isAutoSaveEnabled: boolean
     isSessionOutputsEnabled: boolean
     isPlatformAuthEnabled: boolean
@@ -438,7 +437,6 @@ export interface ClientMessagePayload {
   [ClientMessages.onProgramClose]: {
     ['runme.dev/id']: string
     code: number | void
-    escalationButton: boolean
   }
   [ClientMessages.activeThemeChanged]: string
   [ClientMessages.openLink]: string
@@ -499,7 +497,6 @@ export interface ClientMessagePayload {
   [ClientMessages.platformApiResponse]: {
     data: any
     id: string
-    escalationButton: boolean
     hasErrors?: boolean
   }
   [ClientMessages.optionsModal]: {
@@ -641,6 +638,16 @@ export interface ClientMessagePayload {
     command: string
     argument?: string
   }
+
+  [ClientMessages.featuresUpdateAction]: {
+    snapshot: string
+  }
+
+  [ClientMessages.featuresRequest]: {}
+
+  [ClientMessages.featuresResponse]: {
+    snapshot: string
+  }
 }
 
 export interface OutputItemsPayload {
diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts
index 0684617f..f851784b 100644
--- a/src/utils/configuration.ts
+++ b/src/utils/configuration.ts
@@ -400,10 +400,6 @@ const getRunmeBaseDomain = (): string => {
   return baseDomain
 }
 
-const isRunmeAppButtonsEnabled = (): boolean => {
-  return getCloudConfigurationValue('enableShare', true)
-}
-
 const getRunmePanelIdentifier = (identifer: string): string => {
   const configurationSection = workspace.getConfiguration(`${APP_SECTION_NAME}.panel`)
   const configurationValue = configurationSection.get<string>(identifer) || identifer
@@ -467,7 +463,6 @@ export {
   getTLSEnabled,
   isNotebookTerminalEnabledForCell,
   isNotebookTerminalFeatureEnabled,
-  isRunmeAppButtonsEnabled,
   isPlatformAuthEnabled,
   registerExtensionEnvVarsMutation,
   getSessionOutputs,


Test patch:
diff --git a/tests/e2e/pageobjects/cell.page.ts b/tests/e2e/pageobjects/cell.page.ts
index 0f4f36a9..0c49b590 100644
--- a/tests/e2e/pageobjects/cell.page.ts
+++ b/tests/e2e/pageobjects/cell.page.ts
@@ -106,7 +106,9 @@ export class NotebookCell extends BasePage<typeof notebookCellLocators, typeof l
         expectedTerminal !== OutputType.Display ? await row.getText() : await row.getHTML(false)
 
       if (expectedTerminal === OutputType.TerminalView) {
-        const stopTerm = text.lastIndexOf('Copy\n')
+        // TODO: Why?
+        // const stopTerm = text.lastIndexOf('Copy\n')
+        const stopTerm = text.lastIndexOf('Copy')
         text = text.slice(0, stopTerm).trim()
       }
 
diff --git a/tests/extension/extension.test.ts b/tests/extension/extension.test.ts
index 42d1d821..6b313ada 100644
--- a/tests/extension/extension.test.ts
+++ b/tests/extension/extension.test.ts
@@ -68,6 +68,7 @@ vi.mock('../../src/extension/utils', async () => ({
   checkSession: vi.fn(),
   togglePreviewButton: vi.fn(),
   resetNotebookSettings: vi.fn(),
+  getGithubAuthSession: vi.fn().mockResolvedValue(undefined),
 }))
 
 vi.mock('../../src/extension/grpc/runner/v1', () => ({}))
diff --git a/tests/extension/features.test.ts b/tests/extension/features.test.ts
new file mode 100644
index 00000000..6eb532f7
--- /dev/null
+++ b/tests/extension/features.test.ts
@@ -0,0 +1,188 @@
+import { describe, it, expect, beforeEach } from 'vitest'
+
+import {
+  updateFeatureState,
+  getFeatureSnapshot,
+  loadFeaturesState,
+  isFeatureActive,
+  loadFeatureSnapshot,
+  FeatureContext,
+  FeatureState,
+  FeatureObserver,
+} from '../../src/features'
+
+const packageJSON = {
+  runme: {
+    features: [
+      {
+        name: 'Escalate',
+        enabled: true,
+        conditions: {
+          os: 'All',
+          vsCodeVersion: '>=1.0.0',
+          runmeVersion: '>=1.0.0',
+          extensionVersion: '>=1.0.0',
+          githubAuthRequired: undefined,
+          statefulAuthRequired: true,
+          allowedExtensions: ['stateful.platform'],
+        },
+      },
+      {
+        name: 'Feature B',
+        enabled: true,
+        conditions: {
+          os: 'win32',
+          vsCodeVersion: '>=1.0.0',
+          runmeVersion: '>=1.0.0',
+          extensionVersion: '>=1.0.0',
+          githubAuthRequired: true,
+          statefulAuthRequired: false,
+          allowedExtensions: ['stateful.runme', 'stateful.platform'],
+        },
+      },
+    ],
+  },
+}
+
+describe('Feature Store', () => {
+  let featureState$: FeatureObserver
+
+  beforeEach(() => {
+    featureState$ = loadFeaturesState(packageJSON, {
+      os: 'linux',
+    })
+  })
+
+  it('should evaluate and enable features correctly based on context', () => {
+    const initialContext: FeatureContext = {
+      os: 'win32',
+      vsCodeVersion: '1.59.0',
+      extensionVersion: '1.0.0',
+      runmeVersion: '1.3.0',
+      githubAuth: true,
+      statefulAuth: true,
+      extensionId: 'stateful.runme',
+    }
+
+    updateFeatureState(featureState$, initialContext)
+
+    const currentFeatures = (featureState$.getValue() as FeatureState).features
+
+    expect(currentFeatures[0].activated).toBe(false)
+    expect(currentFeatures[1].activated).toBe(true)
+  })
+
+  it('should take a snapshot of the current state', () => {
+    const initialContext: FeatureContext = {
+      os: 'win32',
+      vsCodeVersion: '1.59.0',
+      extensionVersion: '1.0.0',
+      runmeVersion: '1.3.0',
+      githubAuth: true,
+      statefulAuth: true,
+      extensionId: 'stateful.runme',
+    }
+
+    updateFeatureState(featureState$, initialContext)
+    const snapshot = getFeatureSnapshot(featureState$)
+
+    expect(snapshot).toBe(JSON.stringify(featureState$.getValue()))
+  })
+
+  it('should load a snapshot and restore feature state', () => {
+    const initialContext: FeatureContext = {
+      os: 'win32',
+      vsCodeVersion: '1.59.0',
+      extensionVersion: '1.0.0',
+      runmeVersion: '1.3.0',
+      githubAuth: true,
+      statefulAuth: true,
+      extensionId: 'stateful.runme',
+    }
+
+    updateFeatureState(featureState$, initialContext)
+    const snapshot = getFeatureSnapshot(featureState$)
+
+    featureState$.next({
+      context: initialContext,
+      features: [
+        {
+          name: 'Escalate',
+          enabled: true,
+          conditions: {
+            os: 'All',
+            vsCodeVersion: '>=1.58.0',
+            runmeVersion: '>=1.2.0',
+            githubAuth: undefined,
+            statefulAuth: true,
+          },
+        },
+        {
+          name: 'Feature B',
+          enabled: true,
+          conditions: {
+            os: 'win32',
+            vsCodeVersion: '>=1.60.0',
+            githubAuth: true,
+            statefulAuth: false,
+          },
+        },
+      ],
+    })
+
+    const featureStateCopy$ = loadFeatureSnapshot(snapshot)
+
+    const restoredFeatures = featureStateCopy$.getValue()
+    expect(restoredFeatures).toEqual(JSON.parse(snapshot))
+  })
+
+  it('should disable a feature if context does not meet conditions', () => {
+    const newContext: FeatureContext = {
+      os: 'win32',
+      vsCodeVersion: '0.0.1',
+      extensionVersion: '0.0.1',
+      runmeVersion: '0.0.1',
+      githubAuth: false,
+      statefulAuth: false,
+      extensionId: 'stateful.runme',
+    }
+    updateFeatureState(featureState$, newContext)
+
+    const currentFeatures = (featureState$.getValue() as FeatureState).features
+    expect(currentFeatures[0].activated).toBe(false)
+    expect(currentFeatures[1].activated).toBe(false)
+  })
+
+  it('should correctly identify if a feature is enabled by name', () => {
+    const ctx: FeatureContext = {
+      os: 'linux',
+      vsCodeVersion: '1.59.0',
+      extensionVersion: '1.0.0',
+      runmeVersion: '1.3.0',
+      githubAuth: true,
+      statefulAuth: true,
+      extensionId: 'stateful.platform',
+    }
+    updateFeatureState(featureState$, ctx)
+
+    expect(isFeatureActive(featureState$, 'Escalate')).toBe(true)
+    expect(isFeatureActive(featureState$, 'Feature B')).toBe(false)
+    expect(isFeatureActive(featureState$, 'Nonexistent Feature')).toBe(false)
+  })
+
+  it('should correctly identify if a feature is enabled by extensionId', () => {
+    const ctx: FeatureContext = {
+      os: 'win32',
+      vsCodeVersion: '1.59.0',
+      extensionVersion: '1.0.0',
+      runmeVersion: '1.3.0',
+      githubAuth: true,
+      statefulAuth: true,
+      extensionId: 'stateful.runme',
+    }
+    updateFeatureState(featureState$, ctx)
+
+    expect(isFeatureActive(featureState$, 'Escalate')).toBe(false)
+    expect(isFeatureActive(featureState$, 'Feature B')).toBe(true)
+  })
+})
diff --git a/tests/extension/kernel.test.ts b/tests/extension/kernel.test.ts
index a35a6c8c..4f44a1bc 100644
--- a/tests/extension/kernel.test.ts
+++ b/tests/extension/kernel.test.ts
@@ -29,7 +29,7 @@ vi.mock('../../src/extension/utils', async () => {
     getNotebookCategories: vi.fn().mockResolvedValue([]),
     isWindows: () => false,
     isShellLanguage: () => false,
-    getAuthSession: vi.fn().mockResolvedValue({
+    getGithubAuthSession: vi.fn().mockResolvedValue({
       accessToken: '123',
     }),
   }
diff --git a/tests/extension/messages/platformApiRequest/saveCellExecution.test.ts b/tests/extension/messages/platformApiRequest/saveCellExecution.test.ts
index 5dff98f7..8c49a8e4 100644
--- a/tests/extension/messages/platformApiRequest/saveCellExecution.test.ts
+++ b/tests/extension/messages/platformApiRequest/saveCellExecution.test.ts
@@ -14,6 +14,15 @@ import { GrpcSerializer } from '../../../../src/extension/serializer'
 vi.mock('vscode-telemetry')
 vi.mock('../../../src/extension/runner', () => ({}))
 vi.mock('../../../src/extension/grpc/runner/v1', () => ({}))
+vi.mock('node:os', async (importOriginal) => {
+  const original = (await importOriginal()) as any
+  return {
+    default: {
+      ...original.default,
+      platform: vi.fn().mockReturnValue('linux'),
+    },
+  }
+})
 
 vi.mock('vscode', async () => {
   const mocked = (await vi.importActual('../../../../__mocks__/vscode')) as any
@@ -117,6 +126,14 @@ suite('Save cell execution', () => {
               "from": "kernel",
             },
           ],
+          [
+            {
+              "output": {
+                "snapshot": "{"features":[],"context":{"os":"linux","vsCodeVersion":"9.9.9","githubAuth":false,"statefulAuth":false}}",
+              },
+              "type": "features:updateAction",
+            },
+          ],
           [
             {
               "output": {
@@ -126,7 +143,6 @@ suite('Save cell execution', () => {
                     "id": "cell-id",
                   },
                 },
-                "escalationButton": false,
                 "id": "cell-id",
               },
               "type": "common:platformApiResponse",
@@ -142,6 +158,10 @@ suite('Save cell execution', () => {
             "type": "return",
             "value": undefined,
           },
+          {
+            "type": "return",
+            "value": undefined,
+          },
         ],
       }
     `)
@@ -188,6 +208,14 @@ suite('Save cell execution', () => {
               "from": "kernel",
             },
           ],
+          [
+            {
+              "output": {
+                "snapshot": "{"features":[],"context":{"os":"linux","vsCodeVersion":"9.9.9","githubAuth":false,"statefulAuth":false}}",
+              },
+              "type": "features:updateAction",
+            },
+          ],
           [
             {
               "output": {
@@ -197,7 +225,6 @@ suite('Save cell execution', () => {
                     "id": "cell-id",
                   },
                 },
-                "escalationButton": false,
                 "id": "cell-id",
               },
               "type": "common:platformApiResponse",
@@ -209,7 +236,6 @@ suite('Save cell execution', () => {
                 "data": {
                   "displayShare": false,
                 },
-                "escalationButton": false,
                 "id": "cell-id",
               },
               "type": "common:platformApiResponse",
@@ -229,6 +255,10 @@ suite('Save cell execution', () => {
             "type": "return",
             "value": undefined,
           },
+          {
+            "type": "return",
+            "value": undefined,
+          },
         ],
       }
     `)