diff --git a/src/core/browser.test.ts b/src/core/browser.test.ts new file mode 100644 index 0000000..b22e06a --- /dev/null +++ b/src/core/browser.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect, vi } from 'vitest' +import { openGenerationInBrowser } from './browser.js' + +describe('openGenerationInBrowser', () => { + it('opens the generation URL and returns null on success', async () => { + const openFn = vi.fn().mockResolvedValue(undefined) + const result = await openGenerationInBrowser('gen_123', openFn) + + expect(openFn).toHaveBeenCalledOnce() + expect(openFn).toHaveBeenCalledWith('https://www.pixelmuse.studio/g/gen_123') + expect(result).toBeNull() + }) + + it('returns the error message when open fails', async () => { + const openFn = vi.fn().mockRejectedValue(new Error('No browser available')) + const result = await openGenerationInBrowser('gen_123', openFn) + + expect(result).toBe('No browser available') + }) +}) diff --git a/src/core/browser.ts b/src/core/browser.ts new file mode 100644 index 0000000..e74fd49 --- /dev/null +++ b/src/core/browser.ts @@ -0,0 +1,20 @@ +import open from 'open' + +type OpenFunction = (target: string) => Promise | unknown + +export async function openGenerationInBrowser( + generationId: string, + openFn: OpenFunction = open, +): Promise { + const generationUrl = `https://www.pixelmuse.studio/g/${generationId}` + + try { + await Promise.resolve(openFn(generationUrl)) + return null + } catch (error: unknown) { + if (error instanceof Error && error.message) { + return error.message + } + return 'Unknown error' + } +} diff --git a/src/core/index.ts b/src/core/index.ts index c1d5d55..133e8c5 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -38,3 +38,4 @@ export { timeAgo } from './utils.js' export { initiateDeviceAuth, pollForToken } from './device-auth.js' export { detectEditors, configureMcp, type EditorInfo } from './mcp-config.js' export { buildMacClipboardArgs } from './clipboard.js' +export { openGenerationInBrowser } from './browser.js' diff --git a/src/screens/GalleryDetail.tsx b/src/screens/GalleryDetail.tsx index 5953137..dc9df66 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/browser.js' import ImagePreview from '../components/ImagePreview.js' interface Props { @@ -19,6 +19,7 @@ export default function GalleryDetail({ client, generationId, back }: Props) { const [loading, setLoading] = useState(true) const [confirming, setConfirming] = useState(false) const [error, setError] = useState(null) + const [actionError, setActionError] = useState(null) useEffect(() => { let cancelled = false @@ -49,7 +50,12 @@ 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}`) + setActionError(null) + void openGenerationInBrowser(generation.id).then((openError) => { + if (openError) { + setActionError(`Could not open browser: ${openError}`) + } + }) } }) @@ -113,6 +119,7 @@ export default function GalleryDetail({ client, generationId, back }: Props) { ) : ( [o] open in browser | [d] delete | esc back )} + {actionError && {actionError}} ) }