From f4158b05ba8f3db990ab7e820910ea4634b1b51a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 24 Mar 2026 11:14:42 +0000 Subject: [PATCH] fix: prevent gallery open action from crashing TUI Co-authored-by: Dylan Boudro --- src/core/open-browser.test.ts | 44 +++++++++++++++++++++++++++++++++++ src/core/open-browser.ts | 23 ++++++++++++++++++ src/screens/GalleryDetail.tsx | 8 +++++-- 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/core/open-browser.test.ts create mode 100644 src/core/open-browser.ts diff --git a/src/core/open-browser.test.ts b/src/core/open-browser.test.ts new file mode 100644 index 0000000..a13c92f --- /dev/null +++ b/src/core/open-browser.test.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest' +import { openGenerationInBrowser, openUrlSafely } from './open-browser.js' + +describe('openGenerationInBrowser', () => { + it('returns null when browser open succeeds', async () => { + const seenUrls: string[] = [] + const result = await openGenerationInBrowser('gen_123', async (url) => { + seenUrls.push(url) + return undefined + }) + + expect(result).toBeNull() + expect(seenUrls).toEqual(['https://www.pixelmuse.studio/g/gen_123']) + }) + + it('returns error message when browser open fails', async () => { + const result = await openGenerationInBrowser('gen_456', async () => { + throw new Error('no browser available') + }) + + expect(result).toBe('no browser available') + }) + + it('returns fallback message for non-Error throws', async () => { + const result = await openGenerationInBrowser('gen_789', async () => { + throw { code: 'EFAIL' } + }) + + expect(result).toBe('Failed to open browser') + }) +}) + +describe('openUrlSafely', () => { + it('opens an arbitrary URL unchanged', async () => { + const seenUrls: string[] = [] + const result = await openUrlSafely('https://checkout.example/session/abc', async (url) => { + seenUrls.push(url) + return undefined + }) + + expect(result).toBeNull() + expect(seenUrls).toEqual(['https://checkout.example/session/abc']) + }) +}) diff --git a/src/core/open-browser.ts b/src/core/open-browser.ts new file mode 100644 index 0000000..eac55c0 --- /dev/null +++ b/src/core/open-browser.ts @@ -0,0 +1,23 @@ +import open from 'open' + +const OPEN_BROWSER_ERROR = 'Failed to open browser' + +export async function openUrlSafely( + url: string, + openUrl: (targetUrl: string) => Promise = open, +): Promise { + try { + await openUrl(url) + return null + } catch (err) { + return err instanceof Error ? err.message : OPEN_BROWSER_ERROR + } +} + +export async function openGenerationInBrowser( + generationId: string, + openUrl: (targetUrl: string) => Promise = open, +): Promise { + const url = `https://www.pixelmuse.studio/g/${generationId}` + return openUrlSafely(url, openUrl) +} diff --git a/src/screens/GalleryDetail.tsx b/src/screens/GalleryDetail.tsx index 5953137..ed8f93b 100644 --- a/src/screens/GalleryDetail.tsx +++ b/src/screens/GalleryDetail.tsx @@ -1,10 +1,10 @@ import React, { useState, useEffect } from 'react' import { Box, Text, useInput } from 'ink' import { ConfirmInput, Spinner } from '@inkjs/ui' -import open from 'open' import type { PixelmuseClient } from '../core/client.js' import type { Generation } from '../core/types.js' import { imageToBuffer, autoSave } from '../core/image.js' +import { openGenerationInBrowser } from '../core/open-browser.js' import ImagePreview from '../components/ImagePreview.js' interface Props { @@ -49,7 +49,11 @@ export default function GalleryDetail({ client, generationId, back }: Props) { if (confirming) return if (input === 'd') setConfirming(true) if (input === 'o' && generation) { - open(`https://www.pixelmuse.studio/g/${generation.id}`) + void openGenerationInBrowser(generation.id).then((openError) => { + if (openError) { + setError(openError) + } + }) } })