Repository: stateful/vscode-runme
Pull request #719 (2023-07-25 16:03:33)

Add alternatives to *.md
To make it easier for users who want to use Runme but not for all markdown files everywhere.


Patch:
diff --git a/package-lock.json b/package-lock.json
index ceb6b026..a7644f68 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "runme",
-  "version": "1.8.0",
+  "version": "1.9.0-edge.2",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "runme",
-      "version": "1.8.0",
+      "version": "1.9.0-edge.2",
       "hasInstallScript": true,
       "license": "Apache-2.0",
       "dependencies": {
diff --git a/package.json b/package.json
index f215b1ac..2e0fed3e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "runme",
-  "version": "1.8.0",
+  "version": "1.9.0-edge.2",
   "publisher": "stateful",
   "displayName": "Runme runs Markdown",
   "description": "Execute commands inside your runbooks, docs, and READMEs.",
@@ -305,6 +305,15 @@
           },
           {
             "filenamePattern": "*.mdi"
+          },
+          {
+            "filenamePattern": "*.mdr"
+          },
+          {
+            "filenamePattern": "*.run"
+          },
+          {
+            "filenamePattern": "*.runme"
           }
         ]
       }
diff --git a/src/client/components/terminal/index.ts b/src/client/components/terminal/index.ts
index 7c543892..098a803c 100644
--- a/src/client/components/terminal/index.ts
+++ b/src/client/components/terminal/index.ts
@@ -327,6 +327,9 @@ export class TerminalView extends LitElement {
   @property()
   isCloudApiLoading: boolean = false
 
+  @property()
+  cloudId?: string
+
   @property()
   shareUrl?: string
 
@@ -336,6 +339,9 @@ export class TerminalView extends LitElement {
   @property()
   isShareReady: boolean = false
 
+  @property()
+  isUpdatedReady: boolean = false
+
   constructor() {
     super()
     this.windowSize = {
@@ -417,14 +423,20 @@ export class TerminalView extends LitElement {
               if (e.output.hasErrors) {
                 return postClientMessage(ctx, ClientMessages.errorMessage, e.output.data)
               }
-              const {
-                data: {
-                  createCellExecution: { htmlUrl },
-                },
-              } = e.output.data
-              this.shareUrl = htmlUrl
-              this.shareText = this.shareEnabledText
-              this.isShareReady = true
+              const { data } = e.output.data
+              if (data.createCellExecution) {
+                const {
+                  createCellExecution: { id, htmlUrl },
+                } = data
+                this.cloudId = id
+                this.shareUrl = htmlUrl
+                this.shareText = this.shareEnabledText
+                this.isShareReady = true
+              }
+              if (data.updateCellExecution) {
+                this.isUpdatedReady = true
+                this.#displayShareDialog()
+              }
             }
             break
 
@@ -649,7 +661,7 @@ export class TerminalView extends LitElement {
     this.shareText = this.shareEnabledText
     this.isShareReady = true
     return postClientMessage(ctx, ClientMessages.optionsMessage, {
-      title: 'Share link created',
+      title: 'Publicly accessible link created and ready to share.',
       options: Object.values(MessageOptions),
       uuid: this.uuid!,
       telemetryEvent: 'app.share',
@@ -662,9 +674,20 @@ export class TerminalView extends LitElement {
       return
     }
     try {
-      if (this.isShareReady) {
+      if (this.isUpdatedReady) {
         return this.#displayShareDialog()
       }
+      if (this.isShareReady) {
+        this.isCloudApiLoading = true
+        await postClientMessage(ctx, ClientMessages.cloudApiRequest, {
+          data: {
+            id: this.cloudId,
+          },
+          uuid: this.uuid!,
+          method: APIMethod.UpdateCellExecution,
+        })
+        return
+      }
 
       this.isCloudApiLoading = true
       const contentWithAnsi =
diff --git a/src/extension/__generated__/gql.ts b/src/extension/__generated__/gql.ts
index 752d0fae..af11f787 100644
--- a/src/extension/__generated__/gql.ts
+++ b/src/extension/__generated__/gql.ts
@@ -14,6 +14,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
  */
 const documents = {
     "mutation CreateCellExecution($data: CellExecutionInput!) {\n  createCellExecution(data: $data) {\n    id\n    htmlUrl\n  }\n}": types.CreateCellExecutionDocument,
+    "mutation UpdateCellExecution($id: String!, $data: CellExecutionUpdateInput!) {\n  updateCellExecution(id: $id, data: $data) {\n    id\n    htmlUrl\n  }\n}": types.UpdateCellExecutionDocument,
 };
 
 /**
@@ -34,6 +35,10 @@ export function graphql(source: string): unknown;
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
 export function graphql(source: "mutation CreateCellExecution($data: CellExecutionInput!) {\n  createCellExecution(data: $data) {\n    id\n    htmlUrl\n  }\n}"): (typeof documents)["mutation CreateCellExecution($data: CellExecutionInput!) {\n  createCellExecution(data: $data) {\n    id\n    htmlUrl\n  }\n}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "mutation UpdateCellExecution($id: String!, $data: CellExecutionUpdateInput!) {\n  updateCellExecution(id: $id, data: $data) {\n    id\n    htmlUrl\n  }\n}"): (typeof documents)["mutation UpdateCellExecution($id: String!, $data: CellExecutionUpdateInput!) {\n  updateCellExecution(id: $id, data: $data) {\n    id\n    htmlUrl\n  }\n}"];
 
 export function graphql(source: string) {
   return (documents as any)[source] ?? {};
diff --git a/src/extension/__generated__/graphql.ts b/src/extension/__generated__/graphql.ts
index aafaec3a..a6b70925 100644
--- a/src/extension/__generated__/graphql.ts
+++ b/src/extension/__generated__/graphql.ts
@@ -19,9 +19,25 @@ export type Scalars = {
   DateTime: { input: any; output: any; }
 };
 
+export type Admin = {
+  __typename?: 'Admin';
+  users?: Maybe<Array<Maybe<User>>>;
+};
+
+export type Anonymous = {
+  __typename?: 'Anonymous';
+  cellExecution?: Maybe<CellExecution>;
+  id?: Maybe<Scalars['String']['output']>;
+};
+
+
+export type AnonymousCellExecutionArgs = {
+  id: Scalars['String']['input'];
+};
+
 export type CellExecution = {
   __typename?: 'CellExecution';
-  createTime?: Maybe<Scalars['DateTime']['output']>;
+  createTime: Scalars['DateTime']['output'];
   exitCode: Scalars['Int']['output'];
   htmlUrl?: Maybe<Scalars['String']['output']>;
   id: Scalars['ID']['output'];
@@ -29,10 +45,11 @@ export type CellExecution = {
   isOwner?: Maybe<Scalars['Boolean']['output']>;
   isPrivate: Scalars['Boolean']['output'];
   metadata?: Maybe<Metadata>;
-  pid?: Maybe<Scalars['Int']['output']>;
+  owner?: Maybe<Owner>;
+  pid: Scalars['Int']['output'];
   stderr: Scalars['Bytes']['output'];
   stdout: Scalars['Bytes']['output'];
-  updateTime?: Maybe<Scalars['DateTime']['output']>;
+  updateTime: Scalars['DateTime']['output'];
   user?: Maybe<User>;
   userId: Scalars['String']['output'];
 };
@@ -58,12 +75,14 @@ export type IdConnect = {
 export type Metadata = {
   __typename?: 'Metadata';
   category?: Maybe<Scalars['String']['output']>;
+  exitType?: Maybe<Scalars['String']['output']>;
   mimeType?: Maybe<Scalars['String']['output']>;
   name?: Maybe<Scalars['String']['output']>;
 };
 
 export type MetadataInput = {
   category?: InputMaybe<Scalars['String']['input']>;
+  exitType?: InputMaybe<Scalars['String']['input']>;
   mimeType?: InputMaybe<Scalars['String']['input']>;
   name?: InputMaybe<Scalars['String']['input']>;
 };
@@ -91,22 +110,28 @@ export type MutationUpdateCellExecutionArgs = {
   id: Scalars['String']['input'];
 };
 
+export type Owner = {
+  __typename?: 'Owner';
+  bio?: Maybe<Scalars['String']['output']>;
+  company?: Maybe<Scalars['String']['output']>;
+  displayName?: Maybe<Scalars['String']['output']>;
+  githubUsername?: Maybe<Scalars['String']['output']>;
+  linkedin?: Maybe<Scalars['String']['output']>;
+  photoUrl?: Maybe<Scalars['String']['output']>;
+  siteUrl?: Maybe<Scalars['String']['output']>;
+  twitter?: Maybe<Scalars['String']['output']>;
+};
+
 export type Query = {
   __typename?: 'Query';
-  cellExecutionById?: Maybe<CellExecution>;
-  cellExecutions?: Maybe<Array<Maybe<CellExecution>>>;
+  admin?: Maybe<Admin>;
+  anonymous?: Maybe<Anonymous>;
   user?: Maybe<User>;
-  users?: Maybe<Array<Maybe<User>>>;
-};
-
-
-export type QueryCellExecutionByIdArgs = {
-  id: Scalars['String']['input'];
 };
 
 
 export type QueryUserArgs = {
-  id: Scalars['String']['input'];
+  id?: InputMaybe<Scalars['String']['input']>;
 };
 
 export type User = {
@@ -116,18 +141,17 @@ export type User = {
   cellExecutions?: Maybe<Array<Maybe<CellExecution>>>;
   company?: Maybe<Scalars['String']['output']>;
   create_time: Scalars['DateTime']['output'];
-  display_name?: Maybe<Scalars['String']['output']>;
+  displayName?: Maybe<Scalars['String']['output']>;
   email: Scalars['String']['output'];
-  email_verified?: Maybe<Scalars['Boolean']['output']>;
+  emailVerified?: Maybe<Scalars['Boolean']['output']>;
   firebase_refresh_time?: Maybe<Scalars['DateTime']['output']>;
-  github_id?: Maybe<Scalars['String']['output']>;
-  github_refresh_time?: Maybe<Scalars['DateTime']['output']>;
-  github_username?: Maybe<Scalars['String']['output']>;
+  githubId?: Maybe<Scalars['String']['output']>;
+  githubUsername?: Maybe<Scalars['DateTime']['output']>;
   id: Scalars['ID']['output'];
   linkedin?: Maybe<Scalars['String']['output']>;
-  photo_url?: Maybe<Scalars['String']['output']>;
-  site_admin: Scalars['Boolean']['output'];
-  site_url?: Maybe<Scalars['String']['output']>;
+  photoUrl?: Maybe<Scalars['String']['output']>;
+  siteAdmin: Scalars['Boolean']['output'];
+  siteUrl?: Maybe<Scalars['String']['output']>;
   twitter?: Maybe<Scalars['String']['output']>;
   update_time: Scalars['DateTime']['output'];
 };
@@ -139,5 +163,14 @@ export type CreateCellExecutionMutationVariables = Exact<{
 
 export type CreateCellExecutionMutation = { __typename?: 'Mutation', createCellExecution?: { __typename?: 'CellExecution', id: string, htmlUrl?: string | null } | null };
 
+export type UpdateCellExecutionMutationVariables = Exact<{
+  id: Scalars['String']['input'];
+  data: CellExecutionUpdateInput;
+}>;
+
+
+export type UpdateCellExecutionMutation = { __typename?: 'Mutation', updateCellExecution?: { __typename?: 'CellExecution', id: string, htmlUrl?: string | null } | null };
+
 
-export const CreateCellExecutionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateCellExecution"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CellExecutionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createCellExecution"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"htmlUrl"}}]}}]}}]} as unknown as DocumentNode<CreateCellExecutionMutation, CreateCellExecutionMutationVariables>;
\ No newline at end of file
+export const CreateCellExecutionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateCellExecution"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CellExecutionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createCellExecution"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"htmlUrl"}}]}}]}}]} as unknown as DocumentNode<CreateCellExecutionMutation, CreateCellExecutionMutationVariables>;
+export const UpdateCellExecutionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCellExecution"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CellExecutionUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCellExecution"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"htmlUrl"}}]}}]}}]} as unknown as DocumentNode<UpdateCellExecutionMutation, UpdateCellExecutionMutationVariables>;
\ No newline at end of file
diff --git a/src/extension/executors/runner.ts b/src/extension/executors/runner.ts
index 23d754aa..1bcef267 100644
--- a/src/extension/executors/runner.ts
+++ b/src/extension/executors/runner.ts
@@ -127,17 +127,17 @@ export async function executeRunner(
   const { programName, commandMode } = getCellProgram(exec.cell, exec.cell.notebook, execKey)
 
   const program = await runner.createProgramSession({
-    programName,
-    environment,
-    exec: execution,
-    envs: Object.entries(envs).map(([k, v]) => `${k}=${v}`),
-    cwd,
     background,
-    tty: interactive,
+    commandMode,
     convertEol: !mimeType || mimeType === 'text/plain',
-    storeLastOutput: true,
+    cwd,
+    environment,
+    envs: Object.entries(envs).map(([k, v]) => `${k}=${v}`),
+    exec: execution,
     languageId: exec.cell.document.languageId,
-    commandMode,
+    programName,
+    storeLastOutput: true,
+    tty: interactive,
   })
 
   context.subscriptions.push(program)
diff --git a/src/extension/kernel.ts b/src/extension/kernel.ts
index bfe75ec6..00e5b85a 100644
--- a/src/extension/kernel.ts
+++ b/src/extension/kernel.ts
@@ -540,9 +540,11 @@ export class Kernel implements Disposable {
 
       successfulCellExecution = false
     }
+    const annotations = getAnnotations(cell)
 
     TelemetryReporter.sendTelemetryEvent('cell.endExecute', {
       'cell.success': successfulCellExecution?.toString(),
+      'cell.mimeType': annotations.mimeType,
     })
     runmeExec.end(successfulCellExecution)
   }
diff --git a/src/extension/messages/cloudApiRequest/index.ts b/src/extension/messages/cloudApiRequest/index.ts
index b73498b2..75b371ba 100644
--- a/src/extension/messages/cloudApiRequest/index.ts
+++ b/src/extension/messages/cloudApiRequest/index.ts
@@ -5,6 +5,7 @@ import { APIMethod, ClientMessage } from '../../../types'
 import { Kernel } from '../../kernel'
 
 import saveCellExecution from './saveCellExecution'
+import updateCellExecution from './updateCellExecution'
 
 export interface IApiMessage {
   messaging: NotebookRendererMessaging
@@ -25,6 +26,8 @@ export async function handleCloudApiMessage({
   switch (message.output.method) {
     case APIMethod.CreateCellExecution:
       return saveCellExecution({ messaging, message, editor }, kernel)
+    case APIMethod.UpdateCellExecution:
+      return updateCellExecution({ messaging, message, editor }, kernel)
     default:
       throw new Error('Method not implemented')
   }
diff --git a/src/extension/messages/cloudApiRequest/saveCellExecution.ts b/src/extension/messages/cloudApiRequest/saveCellExecution.ts
index a92b270f..e1b3be8e 100644
--- a/src/extension/messages/cloudApiRequest/saveCellExecution.ts
+++ b/src/extension/messages/cloudApiRequest/saveCellExecution.ts
@@ -36,11 +36,12 @@ export default async function saveCellExecution(
     }
     const pid = (await terminal.processId) || 0
     const runnerExitStatus = terminal.runnerSession?.hasExited()
-    const exitCode = runnerExitStatus
-      ? runnerExitStatus.type === 'exit'
+    const exitCode =
+      runnerExitStatus?.type === 'exit'
         ? runnerExitStatus.code
-        : -1
-      : 0
+        : runnerExitStatus?.type === 'error'
+        ? 1
+        : 0
     const annotations = getAnnotations(cell)
     delete annotations['runme.dev/uuid']
     const runmeService = new RunmeService({ githubAccessToken: session.accessToken })
@@ -63,6 +64,7 @@ export default async function saveCellExecution(
             mimeType: annotations.mimeType,
             name: annotations.name,
             category: annotations.category || '',
+            exitType: runnerExitStatus?.type,
           },
         },
       },
diff --git a/src/extension/messages/cloudApiRequest/updateCellExecution.ts b/src/extension/messages/cloudApiRequest/updateCellExecution.ts
new file mode 100644
index 00000000..f896df6b
--- /dev/null
+++ b/src/extension/messages/cloudApiRequest/updateCellExecution.ts
@@ -0,0 +1,65 @@
+import { TelemetryReporter } from 'vscode-telemetry'
+
+import { ClientMessages } from '../../../constants'
+import { ClientMessage, IApiMessage } from '../../../types'
+import { InitializeClient } from '../../api/client'
+import { getCellByUuId } from '../../cell'
+import { getAuthSession, getCellRunmeId } from '../../utils'
+import { postClientMessage } from '../../../utils/messaging'
+import { RunmeService } from '../../services/runme'
+import { UpdateCellExecutionDocument } from '../../__generated__/graphql'
+import { Kernel } from '../../kernel'
+
+type APIRequestMessage = IApiMessage<ClientMessage<ClientMessages.cloudApiRequest>>
+
+export default async function updateCellExecution(
+  requestMessage: APIRequestMessage,
+  kernel: Kernel
+): Promise<void | boolean> {
+  const { messaging, message, editor } = requestMessage
+
+  try {
+    const session = await getAuthSession()
+
+    if (!session) {
+      throw new Error('You must authenticate with your GitHub account')
+    }
+    const cell = await getCellByUuId({ editor, uuid: message.output.uuid })
+    if (!cell) {
+      throw new Error('Cell not found')
+    }
+
+    const runmeId = getCellRunmeId(cell)
+    const terminal = kernel.getTerminal(runmeId)
+    if (!terminal) {
+      throw new Error('Could not find an associated terminal')
+    }
+    const runmeService = new RunmeService({ githubAccessToken: session.accessToken })
+    const runmeTokenResponse = await runmeService.getUserToken()
+    if (!runmeTokenResponse) {
+      throw new Error('Unable to retrieve an access token')
+    }
+    const graphClient = InitializeClient({ runmeToken: runmeTokenResponse.token })
+    const result = await graphClient.mutate({
+      mutation: UpdateCellExecutionDocument,
+      variables: {
+        id: message.output.data.id,
+        data: {
+          isPrivate: false,
+        },
+      },
+    })
+    TelemetryReporter.sendTelemetryEvent('app.update')
+    return postClientMessage(messaging, ClientMessages.cloudApiResponse, {
+      data: result,
+      uuid: message.output.uuid,
+    })
+  } catch (error) {
+    TelemetryReporter.sendTelemetryEvent('app.update.error')
+    return postClientMessage(messaging, ClientMessages.cloudApiResponse, {
+      data: (error as any).message,
+      uuid: message.output.uuid,
+      hasErrors: true,
+    })
+  }
+}
diff --git a/src/extension/provider/runmeTask.ts b/src/extension/provider/runmeTask.ts
index 7f6daee3..18127eda 100644
--- a/src/extension/provider/runmeTask.ts
+++ b/src/extension/provider/runmeTask.ts
@@ -118,11 +118,8 @@ export class RunmeTaskProvider implements TaskProvider {
 
     const isBackground = options.isBackground || background
 
-    const { programName, commandMode } = getCellProgram(
-      cell,
-      notebook,
-      ('languageId' in cell && cell.languageId) || 'sh'
-    )
+    const languageId = ('languageId' in cell && cell.languageId) || 'sh'
+    const { programName, commandMode } = getCellProgram(cell, notebook, languageId)
 
     const name = `${command}`
 
@@ -151,18 +148,16 @@ export class RunmeTaskProvider implements TaskProvider {
         }
 
         const runOpts: RunProgramOptions = {
-          programName,
-          exec: {
-            type: 'commands',
-            commands: commands ?? [''],
-          },
+          commandMode,
+          convertEol: true,
           cwd,
           environment,
-          tty: interactive,
-          convertEol: true,
           envs: Object.entries(envs).map(([k, v]) => `${k}=${v}`),
+          exec: { type: 'commands', commands: commands ?? [''] },
+          languageId,
+          programName,
           storeLastOutput: true,
-          commandMode,
+          tty: interactive,
         }
 
         const program = await runner.createProgramSession(runOpts)
diff --git a/src/gql/updateCellExecution.graphql b/src/gql/updateCellExecution.graphql
new file mode 100644
index 00000000..98fb5d96
--- /dev/null
+++ b/src/gql/updateCellExecution.graphql
@@ -0,0 +1,6 @@
+mutation UpdateCellExecution($id: String!, $data: CellExecutionUpdateInput!) {
+  updateCellExecution(id: $id, data: $data) {
+   id,
+   htmlUrl
+  }
+}
diff --git a/src/types.ts b/src/types.ts
index 3f5255a6..dfa365ba 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -304,6 +304,7 @@ export interface NotebookToolbarCommand {
 
 export enum APIMethod {
   CreateCellExecution = 'createCellExecution',
+  UpdateCellExecution = 'updateCellExecution',
 }
 
 export interface IApiMessage<T extends ClientMessage<ClientMessages>> {
diff --git a/src/utils/configuration.ts b/src/utils/configuration.ts
index 848b090e..dc8961ca 100644
--- a/src/utils/configuration.ts
+++ b/src/utils/configuration.ts
@@ -20,6 +20,7 @@ export const OpenViewInEditorAction = z.enum(['split', 'toggle'])
 const DEFAULT_WORKSPACE_FILE_ORDER = ['.env.local', '.env']
 const DEFAULT_RUNME_APP_API_URL = 'https://api.runme.dev'
 const DEFAULT_RUNME_BASE_DOMAIN = 'runme.dev'
+const DEFAULT_RUNME_REMOTE_DEV = 'staging.runme.dev'
 const APP_LOOPBACKS = ['127.0.0.1', 'localhost']
 const APP_LOOPBACK_MAPPING = new Map<string, string>([
   ['api.', ':4000'],
@@ -273,8 +274,24 @@ const getCLIUseIntegratedRunme = (): boolean => {
   return getCLIConfigurationValue('useIntegratedRunme', false)
 }
 
+const getRemoteDev = (baseDomain: string): boolean => {
+  const localDev = APP_LOOPBACKS.map((host) =>
+    Uri.from({ scheme: 'http', authority: host }).toString().slice(0, -1)
+  )
+  return localDev.map((uri) => baseDomain.startsWith(uri)).reduce((p, c) => p || c)
+}
+
 const getRunmeAppUrl = (subdomains: string[]): string => {
-  const base = getRunmeBaseDomain()
+  let base = getRunmeBaseDomain()
+  const isRemoteDev = getRemoteDev(base)
+  if (isRemoteDev) {
+    if (subdomains.length === 1 && subdomains?.[0] === 'app') {
+      return base
+    } else {
+      base = DEFAULT_RUNME_REMOTE_DEV
+    }
+  }
+
   const isLoopback = APP_LOOPBACKS.map((host) => base.includes(host)).reduce((p, c) => p || c)
   const scheme = isLoopback ? 'http' : 'https'
 
@@ -289,8 +306,8 @@ const getRunmeAppUrl = (subdomains: string[]): string => {
     sub = ''
   }
 
-  const url = Uri.parse(`${scheme}://${sub}${base}${port}`, true)
-  return url.toString()
+  const endpoint = Uri.parse(`${scheme}://${sub}${base}${port}`, true)
+  return endpoint.toString()
 }
 
 const getRunmeBaseDomain = (): string => {


Test patch:
diff --git a/tests/extension/configuration.test.ts b/tests/extension/configuration.test.ts
index 5dbd2940..89482b2a 100644
--- a/tests/extension/configuration.test.ts
+++ b/tests/extension/configuration.test.ts
@@ -254,5 +254,13 @@ suite('Configuration', () => {
         const url = getRunmeAppUrl([])
         expect(url).toStrictEqual('http://localhost/')
       })
+
+      test('should allow specific app URL for remote dev returning staging-based domains', async () => {
+        SETTINGS_MOCK.baseDomain = 'http://localhost:4001'
+        const app = getRunmeAppUrl(['app'])
+        expect(app).toStrictEqual('http://localhost:4001')
+        const api = getRunmeAppUrl(['api'])
+        expect(api).toStrictEqual('https://api.staging.runme.dev/')
+      })
     })
 })
diff --git a/tests/extension/executors/utils.test.ts b/tests/extension/executors/utils.test.ts
index 89b0aa82..c2670ba2 100644
--- a/tests/extension/executors/utils.test.ts
+++ b/tests/extension/executors/utils.test.ts
@@ -373,8 +373,8 @@ suite('getCellProgram', () => {
 
       let shellPath = getSystemShellPath()
       if (!shellPath) {
-        console.warn('SHELL env not set likely due to non-interactive execution, using bash as default')
-        shellPath = getSystemShellPath('bash')
+        console.warn(`SHELL env not set likely due to non-interactive execution, using ${shell} as default`)
+        shellPath = getSystemShellPath(shell)
       }
 
       expect(getCellProgram({ metadata: { } } as any, {} as any, shell)).toStrictEqual({
diff --git a/tests/extension/kernel.test.ts b/tests/extension/kernel.test.ts
index 7437f196..72edcc33 100644
--- a/tests/extension/kernel.test.ts
+++ b/tests/extension/kernel.test.ts
@@ -204,7 +204,7 @@ suite('_doExecuteCell', () => {
     )
     expect(TelemetryReporter.sendTelemetryEvent).toHaveBeenCalledWith(
       'cell.endExecute',
-      { 'cell.success': undefined }
+      { 'cell.success': undefined, 'cell.mimeType': undefined }
     )
   })
 
@@ -226,7 +226,7 @@ suite('_doExecuteCell', () => {
     try {
       await k['_doExecuteCell']({
         document: { uri: { fsPath: '/foo/bar' } },
-        metadata: { 'runme.dev/uuid': '849448b2-3c41-4323-920e-3098e71302ce' }
+        metadata: { 'runme.dev/uuid': '849448b2-3c41-4323-920e-3098e71302ce', mimeType: 'text/plain' }
       } as any)
     } catch(e) {
 
@@ -237,7 +237,7 @@ suite('_doExecuteCell', () => {
     )
     expect(TelemetryReporter.sendTelemetryEvent).toHaveBeenCalledWith(
       'cell.endExecute',
-      { 'cell.success': 'false' }
+      { 'cell.success': 'false', 'cell.mimeType': 'text/plain' }
     )
   })
 })