diff --git a/src/renderer/components/settings/NotificationTriggerSettings/utils/trigger.ts b/src/renderer/components/settings/NotificationTriggerSettings/utils/trigger.ts index e0d42b77..8db17b0d 100644 --- a/src/renderer/components/settings/NotificationTriggerSettings/utils/trigger.ts +++ b/src/renderer/components/settings/NotificationTriggerSettings/utils/trigger.ts @@ -2,13 +2,15 @@ * Utility functions for notification triggers. */ +import { generateUUID } from '@renderer/utils/uuid'; + import type { NotificationTrigger, TriggerContentType, TriggerMode } from '@renderer/types/data'; /** * Generates a UUID v4 for new triggers. */ export function generateId(): string { - return crypto.randomUUID(); + return generateUUID(); } /** diff --git a/src/renderer/components/settings/sections/WorkspaceSection.tsx b/src/renderer/components/settings/sections/WorkspaceSection.tsx index aab157af..6f35d65b 100644 --- a/src/renderer/components/settings/sections/WorkspaceSection.tsx +++ b/src/renderer/components/settings/sections/WorkspaceSection.tsx @@ -15,6 +15,7 @@ import { useCallback, useEffect, useState } from 'react'; import { api } from '@renderer/api'; import { confirm } from '@renderer/components/common/ConfirmDialog'; import { useStore } from '@renderer/store'; +import { generateUUID } from '@renderer/utils/uuid'; import { Edit2, Loader2, Plus, Save, Server, Trash2, X } from 'lucide-react'; import { SettingsSectionHeader } from '../components/SettingsSectionHeader'; @@ -102,7 +103,7 @@ export const WorkspaceSection = (): React.JSX.Element => { const handleAdd = async (): Promise => { const newProfile: SshConnectionProfile = { - id: crypto.randomUUID(), + id: generateUUID(), name: formName.trim(), host: formHost.trim(), port: parseInt(formPort, 10) || 22, diff --git a/src/renderer/store/slices/paneSlice.ts b/src/renderer/store/slices/paneSlice.ts index 85d6628e..73f1368b 100644 --- a/src/renderer/store/slices/paneSlice.ts +++ b/src/renderer/store/slices/paneSlice.ts @@ -4,6 +4,7 @@ */ import { MAX_PANES } from '@renderer/types/panes'; +import { generateUUID } from '@renderer/utils/uuid'; import { createEmptyPane, @@ -140,7 +141,7 @@ export const createPaneSlice: StateCreator = (set, }; // Create new pane with the tab - const newPaneId = crypto.randomUUID(); + const newPaneId = generateUUID(); const newPane = { ...createEmptyPane(newPaneId), tabs: [tab], @@ -277,7 +278,7 @@ export const createPaneSlice: StateCreator = (set, newSourceActiveTabId = newSourceTabs[oldIndex]?.id ?? newSourceTabs[oldIndex - 1]?.id ?? null; } - const newPaneId = crypto.randomUUID(); + const newPaneId = generateUUID(); const newPane = { ...createEmptyPane(newPaneId), tabs: [tab], diff --git a/src/renderer/store/slices/tabSlice.ts b/src/renderer/store/slices/tabSlice.ts index 7cb9f4b9..23bdafd7 100644 --- a/src/renderer/store/slices/tabSlice.ts +++ b/src/renderer/store/slices/tabSlice.ts @@ -12,6 +12,7 @@ import { findTabBySessionAndProject, truncateLabel, } from '@renderer/types/tabs'; +import { generateUUID } from '@renderer/utils/uuid'; import { findPane, @@ -172,7 +173,7 @@ export const createTabSlice: StateCreator = (set, ge // Create new tab with generated id and timestamp const newTab: Tab = { ...tab, - id: crypto.randomUUID(), + id: generateUUID(), label: truncateLabel(tab.label), createdAt: Date.now(), }; @@ -365,7 +366,7 @@ export const createTabSlice: StateCreator = (set, ge if (!focusedPane) return; const newTab: Tab = { - id: crypto.randomUUID(), + id: generateUUID(), type: 'dashboard', label: 'Dashboard', createdAt: Date.now(), diff --git a/src/renderer/types/tabs.ts b/src/renderer/types/tabs.ts index 4e60603e..5e08dc71 100644 --- a/src/renderer/types/tabs.ts +++ b/src/renderer/types/tabs.ts @@ -3,6 +3,8 @@ * Based on specs/001-tabbed-layout-dashboard/contracts/tab-state.ts */ +import { generateUUID } from '@renderer/utils/uuid'; + import type { Session } from './data'; import type { TriggerColor } from '@shared/constants/triggerColors'; @@ -52,7 +54,7 @@ export interface SearchNavigationPayload { * The nonce ensures repeated clicks produce new navigations. */ export interface TabNavigationRequest { - /** Unique nonce per click/action (crypto.randomUUID) */ + /** Unique nonce per click/action (generateUUID) */ id: string; /** Kind of navigation */ kind: 'error' | 'search' | 'autoBottom'; @@ -194,7 +196,7 @@ export function createErrorNavigationRequest( highlightColor?: TriggerColor ): TabNavigationRequest { return { - id: crypto.randomUUID(), + id: generateUUID(), kind: 'error', source, highlight: highlightColor ?? 'red', @@ -209,7 +211,7 @@ export function createSearchNavigationRequest( payload: SearchNavigationPayload ): TabNavigationRequest { return { - id: crypto.randomUUID(), + id: generateUUID(), kind: 'search', source: 'commandPalette', highlight: 'yellow', diff --git a/src/renderer/utils/uuid.ts b/src/renderer/utils/uuid.ts new file mode 100644 index 00000000..43cf0105 --- /dev/null +++ b/src/renderer/utils/uuid.ts @@ -0,0 +1,25 @@ +/** + * Generate a UUID v4 string. + * + * `crypto.randomUUID()` is only available in **secure contexts** (HTTPS or + * localhost). When the app is served over plain HTTP on a LAN IP (e.g. + * Docker accessed from another machine), the browser will not expose + * `randomUUID`. This helper falls back to `crypto.getRandomValues()` which + * is available in all modern browsers regardless of secure context. + * + * @see https://github.com/matt1398/claude-devtools/issues/132 + */ +export function generateUUID(): string { + if (typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + // Fallback: construct a v4 UUID from getRandomValues + const bytes = crypto.getRandomValues(new Uint8Array(16)); + // Set version (4) and variant (RFC 4122) + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +}