From ac5d5c31d285edb72c92ad2f2689757e9d2bcd5d Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:28:01 -0400 Subject: [PATCH 1/4] Hyperlink shortcut labels in DevSessionUI footer When the terminal supports hyperlinks, the shortcut labels (p, c, g) are rendered as clickable OSC 8 links and the separate URL list is hidden. When hyperlinks are not supported, labels render as plain text with the URL list shown below as before. Adds terminalSupportsHyperlinks() to @shopify/cli-kit/node/system and makes boolean check ordering consistent (status.isReady first). Co-Authored-By: Claude Opus 4.6 --- .../dev/ui/components/DevSessionUI.test.tsx | 63 ++++++++++++++++++- .../dev/ui/components/DevSessionUI.tsx | 33 +++++++--- packages/cli-kit/src/public/node/system.ts | 5 ++ 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx index 7d6b5f5ab61..62bf6aed3aa 100644 --- a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx +++ b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.test.tsx @@ -14,7 +14,14 @@ import {unstyled} from '@shopify/cli-kit/node/output' import {openURL} from '@shopify/cli-kit/node/system' import {Writable} from 'stream' -vi.mock('@shopify/cli-kit/node/system') +vi.mock('@shopify/cli-kit/node/system', async () => { + const actual: any = await vi.importActual('@shopify/cli-kit/node/system') + return { + ...actual, + openURL: vi.fn(), + terminalSupportsHyperlinks: mocks.terminalSupportsHyperlinks, + } +}) vi.mock('@shopify/cli-kit/node/context/local') vi.mock('@shopify/cli-kit/node/tree-kill') @@ -23,6 +30,7 @@ const mocks = vi.hoisted(() => { useStdin: vi.fn(() => { return {isRawModeSupported: true} }), + terminalSupportsHyperlinks: vi.fn(() => false), } }) @@ -544,6 +552,59 @@ describe('DevSessionUI', () => { renderInstance.unmount() }) + test('hides URL list when terminal supports hyperlinks', async () => { + // Given + mocks.terminalSupportsHyperlinks.mockReturnValue(true) + + const renderInstance = render( + , + ) + + await waitForInputsToBeReady() + + // Then - shortcuts should be present but URL list should be hidden + const output = unstyled(renderInstance.lastFrame()!) + expect(output).toContain('(p)') + expect(output).toContain('(g)') + expect(output).not.toContain('Preview URL:') + expect(output).not.toContain('GraphiQL URL:') + + renderInstance.unmount() + mocks.terminalSupportsHyperlinks.mockReturnValue(false) + }) + + test('shows URL list when terminal does not support hyperlinks', async () => { + // Given + mocks.terminalSupportsHyperlinks.mockReturnValue(false) + + const renderInstance = render( + , + ) + + await waitForInputsToBeReady() + + // Then - both shortcuts and URL list should be present + const output = unstyled(renderInstance.lastFrame()!) + expect(output).toContain('(p)') + expect(output).toContain('(g)') + expect(output).toContain('Preview URL: https://shopify.com') + expect(output).toContain('GraphiQL URL: https://graphiql.shopify.com') + + renderInstance.unmount() + }) + test('shows non-interactive fallback when raw mode is not supported', async () => { // Given - mock useStdin to return false for isRawModeSupported mocks.useStdin.mockReturnValue({isRawModeSupported: false}) diff --git a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx index 0946bbbca53..dd87a71deaa 100644 --- a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx +++ b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx @@ -15,7 +15,7 @@ import React, {FunctionComponent, useEffect, useMemo, useState} from 'react' import {AbortController, AbortSignal} from '@shopify/cli-kit/node/abort' import {Box, Text, useInput, useStdin} from '@shopify/cli-kit/node/ink' import {handleCtrlC} from '@shopify/cli-kit/node/ui' -import {openURL} from '@shopify/cli-kit/node/system' +import {openURL, terminalSupportsHyperlinks} from '@shopify/cli-kit/node/system' import figures from '@shopify/cli-kit/node/figures' import {isUnitTest} from '@shopify/cli-kit/node/context/local' import {treeKill} from '@shopify/cli-kit/node/tree-kill' @@ -126,7 +126,7 @@ const DevSessionUI: FunctionComponent = ({ shortcuts: [ { key: 'p', - condition: () => Boolean(status.previewURL && status.isReady), + condition: () => Boolean(status.isReady && status.previewURL), action: async () => { await metadata.addPublicMetadata(() => ({ cmd_dev_preview_url_opened: true, @@ -138,7 +138,7 @@ const DevSessionUI: FunctionComponent = ({ }, { key: 'g', - condition: () => Boolean(status.graphiqlURL && status.isReady), + condition: () => Boolean(status.isReady && status.graphiqlURL), action: async () => { await metadata.addPublicMetadata(() => ({ cmd_dev_graphiql_opened: true, @@ -168,19 +168,34 @@ const DevSessionUI: FunctionComponent = ({ )} {canUseShortcuts && ( - {status.isReady ? ( + {status.isReady && status.previewURL ? ( - {figures.pointerSmall} (p) Open app preview + {figures.pointerSmall} (p){' '} + {terminalSupportsHyperlinks() ? ( + + ) : ( + 'Open app preview' + )} ) : null} {status.isReady && !status.appEmbedded && status.hasExtensions ? ( - {figures.pointerSmall} (c) Open Dev Console for extension previews + {figures.pointerSmall} (c){' '} + {terminalSupportsHyperlinks() ? ( + + ) : ( + 'Open Dev Console for extension previews' + )} ) : null} - {status.graphiqlURL && status.isReady ? ( + {status.isReady && status.graphiqlURL ? ( - {figures.pointerSmall} (g) Open GraphiQL (Admin API) + {figures.pointerSmall} (g){' '} + {terminalSupportsHyperlinks() ? ( + + ) : ( + 'Open GraphiQL (Admin API)' + )} ) : null} @@ -190,7 +205,7 @@ const DevSessionUI: FunctionComponent = ({ {isShuttingDownMessage} ) : ( <> - {status.isReady && ( + {status.isReady && !terminalSupportsHyperlinks() && ( <> {status.previewURL ? ( diff --git a/packages/cli-kit/src/public/node/system.ts b/packages/cli-kit/src/public/node/system.ts index 537275458e0..246127b5ba9 100644 --- a/packages/cli-kit/src/public/node/system.ts +++ b/packages/cli-kit/src/public/node/system.ts @@ -7,6 +7,7 @@ import {renderWarning} from './ui.js' import {platformAndArch} from './os.js' import {shouldDisplayColors, outputDebug} from './output.js' import {execa, execaCommand, ExecaChildProcess} from 'execa' +import supportsHyperlinks from 'supports-hyperlinks' import which from 'which' import {delimiter} from 'pathe' @@ -351,6 +352,10 @@ export async function sleep(seconds: number): Promise { * * @returns True if the standard input and output streams support prompting. */ +export function terminalSupportsHyperlinks(): boolean { + return supportsHyperlinks.stdout +} + export function terminalSupportsPrompting(): boolean { if (isTruthy(process.env.CI)) { return false From c0ab3486a138946bc586e291ea9f0657ddd99921 Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:52:04 -0400 Subject: [PATCH 2/4] Fix URL list visibility when shortcuts are unavailable Only hide the URL list when hyperlinked shortcuts are actually rendered (canUseShortcuts && terminalSupportsHyperlinks), not just when the terminal supports hyperlinks. Co-Authored-By: Claude Opus 4.6 --- .../app/src/cli/services/dev/ui/components/DevSessionUI.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx index dd87a71deaa..882bb30c3b3 100644 --- a/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx +++ b/packages/app/src/cli/services/dev/ui/components/DevSessionUI.tsx @@ -205,7 +205,7 @@ const DevSessionUI: FunctionComponent = ({ {isShuttingDownMessage} ) : ( <> - {status.isReady && !terminalSupportsHyperlinks() && ( + {status.isReady && !(canUseShortcuts && terminalSupportsHyperlinks()) && ( <> {status.previewURL ? ( From 958cd01ba3e2d258468db1d842dc72742964af40 Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:10:36 -0400 Subject: [PATCH 3/4] Add changeset for hyperlink shortcut labels Co-Authored-By: Claude Opus 4.6 --- .changeset/afraid-hairs-talk.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/afraid-hairs-talk.md diff --git a/.changeset/afraid-hairs-talk.md b/.changeset/afraid-hairs-talk.md new file mode 100644 index 00000000000..21101d3bd8c --- /dev/null +++ b/.changeset/afraid-hairs-talk.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': minor +--- + +Render footer links in `app dev` as hyperlinks, if supported by the terminal. From dbabaf13f929069592dbcdac97876cecdb96f8c2 Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:21:21 -0400 Subject: [PATCH 4/4] Add JSDoc to terminalSupportsHyperlinks and restore missing JSDoc on terminalSupportsPrompting Co-Authored-By: Claude Opus 4.6 --- packages/cli-kit/src/public/node/system.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/cli-kit/src/public/node/system.ts b/packages/cli-kit/src/public/node/system.ts index 246127b5ba9..a532dce70a0 100644 --- a/packages/cli-kit/src/public/node/system.ts +++ b/packages/cli-kit/src/public/node/system.ts @@ -348,14 +348,19 @@ export async function sleep(seconds: number): Promise { } /** - * Check if the standard input and output streams support prompting. + * Check if the terminal supports OSC 8 hyperlinks. * - * @returns True if the standard input and output streams support prompting. + * @returns True if the terminal supports hyperlinks. */ export function terminalSupportsHyperlinks(): boolean { return supportsHyperlinks.stdout } +/** + * Check if the standard input and output streams support prompting. + * + * @returns True if the standard input and output streams support prompting. + */ export function terminalSupportsPrompting(): boolean { if (isTruthy(process.env.CI)) { return false