Build & Push Frontend Docker Image / build-and-push (push) Canceled after 0s
Build Validation / validate (push) Canceled after 6s
Build & Push Backend Docker Image / build-and-push (push) Failing after 10m10s
Build & Push Gateway Docker Image / build-and-push (push) Failing after 10m9s
A professional workspace for AI coding agents with: - Real-time streaming chat with SSE - Session management (fork, clone, tree navigation) - MCP tools browser & metadata panel - Skills browser (global, project, package sources) - Voice input & STT provider configuration - Integrated terminal & file explorer - Model selector with capability metadata - Keyboard-first UI with command palette - 8 languages (en, pt-BR, es, ja, zh-CN, ar, bn, hi) - Tauri 2 desktop app - React 19 + TypeScript + Vite 8 + Tailwind CSS 4 - Pi Agent extension runtime Originally forked from OpenCodeUI, now an independent project. License: GPL-3.0-only
190 lines
5.8 KiB
TypeScript
190 lines
5.8 KiB
TypeScript
import { STORAGE_KEY_NOTIFICATIONS_ENABLED } from '../constants/storage'
|
|
import {
|
|
exportLayoutBackup,
|
|
exportNotificationEventSettingsBackup,
|
|
exportNotificationPreferencesBackup,
|
|
exportServerSettingsBackup,
|
|
exportServiceSettingsBackup,
|
|
exportThemeBackup,
|
|
exportUpdateSettingsBackup,
|
|
importLayoutBackup,
|
|
importNotificationEventSettingsBackup,
|
|
importNotificationPreferencesBackup,
|
|
importServerSettingsBackup,
|
|
importServiceSettingsBackup,
|
|
importThemeBackup,
|
|
importUpdateSettingsBackup,
|
|
type LayoutBackup,
|
|
type NotificationEventSettingsBackup,
|
|
type NotificationPreferencesBackup,
|
|
type ServerSettingsBackup,
|
|
type ServiceSettingsBackup,
|
|
type ThemeBackup,
|
|
type UpdateSettingsBackup,
|
|
} from '../store'
|
|
import { exportKeybindingBackup, importKeybindingBackup, type KeybindingBackup } from '../store/keybindingStore'
|
|
import { exportSoundBackup, importSoundBackup, type SoundBackup } from '../store/soundStore'
|
|
import {
|
|
exportPerServerStorageBackup,
|
|
importPerServerStorageBackup,
|
|
type PerServerStorageBackup,
|
|
} from './perServerStorage'
|
|
|
|
const BACKUP_KIND = 'settings-backup'
|
|
const BACKUP_SCHEMA_VERSION = 2
|
|
|
|
export interface NotificationBackup {
|
|
browserNotificationsEnabled: boolean
|
|
toast: NotificationPreferencesBackup
|
|
events: NotificationEventSettingsBackup
|
|
}
|
|
|
|
export interface SettingsBackupModules {
|
|
theme: ThemeBackup
|
|
layout: LayoutBackup
|
|
servers: ServerSettingsBackup
|
|
perServerStorage: PerServerStorageBackup
|
|
service: ServiceSettingsBackup
|
|
keybindings: KeybindingBackup
|
|
notifications: NotificationBackup
|
|
sound: SoundBackup
|
|
update: UpdateSettingsBackup
|
|
}
|
|
|
|
export interface SettingsBackupFile {
|
|
app: 'OpenCodeUI'
|
|
kind: typeof BACKUP_KIND
|
|
schemaVersion: typeof BACKUP_SCHEMA_VERSION
|
|
createdAt: string
|
|
modules: SettingsBackupModules
|
|
}
|
|
|
|
function exportNotificationBackup(): NotificationBackup {
|
|
return {
|
|
browserNotificationsEnabled: localStorage.getItem(STORAGE_KEY_NOTIFICATIONS_ENABLED) === 'true',
|
|
toast: exportNotificationPreferencesBackup(),
|
|
events: exportNotificationEventSettingsBackup(),
|
|
}
|
|
}
|
|
|
|
function importNotificationBackup(raw: unknown): void {
|
|
const parsed = raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : undefined
|
|
const browserNotificationsEnabled = parsed?.browserNotificationsEnabled === true
|
|
|
|
if (browserNotificationsEnabled) {
|
|
localStorage.setItem(STORAGE_KEY_NOTIFICATIONS_ENABLED, 'true')
|
|
} else {
|
|
localStorage.removeItem(STORAGE_KEY_NOTIFICATIONS_ENABLED)
|
|
}
|
|
|
|
importNotificationPreferencesBackup(parsed?.toast)
|
|
importNotificationEventSettingsBackup(parsed?.events)
|
|
}
|
|
|
|
function normalizeBackupFile(raw: unknown): SettingsBackupFile {
|
|
if (!raw || typeof raw !== 'object') {
|
|
throw new Error('Invalid backup file')
|
|
}
|
|
|
|
const parsed = raw as Record<string, unknown>
|
|
if (parsed.app !== 'OpenCodeUI' || parsed.kind !== BACKUP_KIND || parsed.schemaVersion !== BACKUP_SCHEMA_VERSION) {
|
|
throw new Error('Unsupported backup format')
|
|
}
|
|
|
|
if (!parsed.modules || typeof parsed.modules !== 'object') {
|
|
throw new Error('Missing backup modules')
|
|
}
|
|
|
|
const modules = parsed.modules as Record<string, unknown>
|
|
const requiredModules: Array<keyof SettingsBackupModules> = [
|
|
'theme',
|
|
'layout',
|
|
'servers',
|
|
'perServerStorage',
|
|
'service',
|
|
'keybindings',
|
|
'notifications',
|
|
'sound',
|
|
'update',
|
|
]
|
|
|
|
for (const id of requiredModules) {
|
|
if (!(id in modules)) {
|
|
throw new Error(`Missing backup module: ${id}`)
|
|
}
|
|
}
|
|
|
|
return {
|
|
app: 'OpenCodeUI',
|
|
kind: BACKUP_KIND,
|
|
schemaVersion: BACKUP_SCHEMA_VERSION,
|
|
createdAt: typeof parsed.createdAt === 'string' ? parsed.createdAt : new Date().toISOString(),
|
|
modules: modules as unknown as SettingsBackupModules,
|
|
}
|
|
}
|
|
|
|
function buildBackupFileName(createdAt: string): string {
|
|
const safeTimestamp = createdAt.replace(/[:]/g, '-').replace(/\.\d+Z$/, 'Z')
|
|
return `opencodeui-settings-backup-${safeTimestamp}.json`
|
|
}
|
|
|
|
export async function exportSettingsBackup(): Promise<{ fileName: string; data: Uint8Array }> {
|
|
const createdAt = new Date().toISOString()
|
|
const backup: SettingsBackupFile = {
|
|
app: 'OpenCodeUI',
|
|
kind: BACKUP_KIND,
|
|
schemaVersion: BACKUP_SCHEMA_VERSION,
|
|
createdAt,
|
|
modules: {
|
|
theme: exportThemeBackup(),
|
|
layout: exportLayoutBackup(),
|
|
servers: exportServerSettingsBackup(),
|
|
perServerStorage: exportPerServerStorageBackup(),
|
|
service: exportServiceSettingsBackup(),
|
|
keybindings: exportKeybindingBackup(),
|
|
notifications: exportNotificationBackup(),
|
|
sound: await exportSoundBackup(),
|
|
update: exportUpdateSettingsBackup(),
|
|
},
|
|
}
|
|
|
|
return {
|
|
fileName: buildBackupFileName(createdAt),
|
|
data: new TextEncoder().encode(`${JSON.stringify(backup, null, 2)}\n`),
|
|
}
|
|
}
|
|
|
|
export async function importSettingsBackup(file: File): Promise<void> {
|
|
const text = await file.text()
|
|
let parsed: unknown
|
|
|
|
try {
|
|
parsed = JSON.parse(text)
|
|
} catch {
|
|
throw new Error('Invalid backup file')
|
|
}
|
|
|
|
const backup = normalizeBackupFile(parsed)
|
|
|
|
importThemeBackup(backup.modules.theme)
|
|
importLayoutBackup(backup.modules.layout)
|
|
importServerSettingsBackup(backup.modules.servers)
|
|
importPerServerStorageBackup(backup.modules.perServerStorage)
|
|
importServiceSettingsBackup(backup.modules.service)
|
|
importKeybindingBackup(backup.modules.keybindings)
|
|
importNotificationBackup(backup.modules.notifications)
|
|
await importSoundBackup(backup.modules.sound)
|
|
importUpdateSettingsBackup(backup.modules.update)
|
|
}
|
|
|
|
export function previewBackupMeta(file: File): Promise<{ createdAt: string | null }> {
|
|
return file.text().then(text => {
|
|
try {
|
|
const parsed = JSON.parse(text) as Record<string, unknown>
|
|
return { createdAt: typeof parsed.createdAt === 'string' ? parsed.createdAt : null }
|
|
} catch {
|
|
return { createdAt: null }
|
|
}
|
|
})
|
|
}
|