Files
PiAgentUI/extensions/piagentui-settings-store.ts
T
Evandro b3489e1ede
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
🎉 Initial commit: PiAgentUI — AI Agent Desktop & Web UI
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
2026-06-01 00:50:06 -03:00

215 lines
6.5 KiB
TypeScript

import * as fs from 'node:fs'
import * as path from 'node:path'
import { createRequire } from 'node:module'
const nodeRequire = createRequire(import.meta.url)
const { DatabaseSync } = nodeRequire('node:sqlite') as typeof import('node:sqlite')
type DatabaseSyncInstance = InstanceType<typeof DatabaseSync>
export type SttProviderKind = 'openai' | 'openai-compatible'
export type SttMode = 'file' | 'realtime'
export type SttInsertMode = 'append' | 'replace'
export type TtsResponseFormat = 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'
export type StoredSttSettings = {
enabled?: boolean
providerKind?: SttProviderKind
mode?: SttMode
baseUrl?: string
apiKey?: string
apiKeyRequired?: boolean
transcriptionEndpoint?: string
transcriptionModel?: string
language?: string
insertMode?: SttInsertMode
}
export type StoredTtsSettings = {
enabled?: boolean
providerKind?: SttProviderKind
baseUrl?: string
apiKey?: string
apiKeyRequired?: boolean
speechEndpoint?: string
model?: string
voice?: string
responseFormat?: TtsResponseFormat
speed?: number
}
export type PiAgentUiLocalSettings = {
schemaVersion?: number
network?: {
lanAccessEnabled?: boolean
}
stt?: StoredSttSettings
tts?: StoredTtsSettings
}
export interface PiAgentUiSettingsPaths {
databasePath: string
legacyJsonPath: string
}
type SettingsNamespace = 'network' | 'stt' | 'tts'
const SETTINGS_SCHEMA_VERSION = 1
const SETTINGS_KEYS: Record<SettingsNamespace, string> = {
network: 'access',
stt: 'settings',
tts: 'settings',
}
export function resolvePiAgentUiSettingsPaths(
homeDir = process.env.USERPROFILE || process.env.HOME || '',
): PiAgentUiSettingsPaths {
const settingsDir = path.join(homeDir, '.pi', 'agent')
return {
databasePath: path.join(settingsDir, 'piagentui.db'),
legacyJsonPath: path.join(settingsDir, 'piagentui-settings.json'),
}
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function parseJsonObject(value: string | undefined): Record<string, unknown> | undefined {
if (!value) return undefined
try {
const parsed = JSON.parse(value)
return isObject(parsed) ? parsed : undefined
} catch {
return undefined
}
}
function normalizeSettings(raw: unknown): PiAgentUiLocalSettings {
if (!isObject(raw)) return {}
const next: PiAgentUiLocalSettings = { schemaVersion: SETTINGS_SCHEMA_VERSION }
if (isObject(raw.network)) {
next.network = { lanAccessEnabled: raw.network.lanAccessEnabled === true }
}
if (isObject(raw.stt)) {
next.stt = raw.stt as StoredSttSettings
}
if (isObject(raw.tts)) {
next.tts = raw.tts as StoredTtsSettings
}
return next
}
function readLegacyJsonSettings(legacyJsonPath: string): PiAgentUiLocalSettings | null {
try {
if (!fs.existsSync(legacyJsonPath)) return null
return normalizeSettings(JSON.parse(fs.readFileSync(legacyJsonPath, 'utf8')))
} catch {
return null
}
}
export class PiAgentUiSettingsStore {
private readonly db: DatabaseSyncInstance
constructor(private readonly paths: PiAgentUiSettingsPaths) {
fs.mkdirSync(path.dirname(paths.databasePath), { recursive: true })
this.db = new DatabaseSync(paths.databasePath)
this.initialize()
this.importLegacyJsonOnce()
}
close(): void {
this.db.close()
}
getMeta(key: string): string | null {
const row = this.db.prepare('SELECT value FROM app_meta WHERE key = ?').get(key) as { value?: string } | undefined
return typeof row?.value === 'string' ? row.value : null
}
setMeta(key: string, value: string): void {
this.db
.prepare(
`INSERT INTO app_meta (key, value, updated_at)
VALUES (?, ?, ?)
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
)
.run(key, value, Date.now())
}
getSettings(): PiAgentUiLocalSettings {
return {
schemaVersion: SETTINGS_SCHEMA_VERSION,
network: this.getNamespace('network') as PiAgentUiLocalSettings['network'],
stt: this.getNamespace('stt') as StoredSttSettings | undefined,
tts: this.getNamespace('tts') as StoredTtsSettings | undefined,
}
}
saveSettings(settings: PiAgentUiLocalSettings): void {
const normalized = normalizeSettings(settings)
this.db.exec('BEGIN')
try {
if (normalized.network) this.setNamespace('network', normalized.network)
if (normalized.stt) this.setNamespace('stt', normalized.stt)
if (normalized.tts) this.setNamespace('tts', normalized.tts)
this.setMeta('schema_version', String(SETTINGS_SCHEMA_VERSION))
this.db.exec('COMMIT')
} catch (error) {
this.db.exec('ROLLBACK')
throw error
}
}
private initialize(): void {
this.db.exec('PRAGMA journal_mode = WAL')
this.db.exec('PRAGMA foreign_keys = ON')
this.db.exec(`
CREATE TABLE IF NOT EXISTS app_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
namespace TEXT NOT NULL,
key TEXT NOT NULL,
value_json TEXT NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (namespace, key)
);
`)
this.setMeta('schema_version', String(SETTINGS_SCHEMA_VERSION))
}
private importLegacyJsonOnce(): void {
if (this.getMeta('legacy_json_imported_at')) return
const legacy = readLegacyJsonSettings(this.paths.legacyJsonPath)
if (!legacy) return
this.saveSettings(legacy)
this.setMeta('legacy_json_imported_at', new Date().toISOString())
}
private getNamespace(namespace: SettingsNamespace): Record<string, unknown> | undefined {
const row = this.db
.prepare('SELECT value_json FROM settings WHERE namespace = ? AND key = ?')
.get(namespace, SETTINGS_KEYS[namespace]) as { value_json?: string } | undefined
return parseJsonObject(row?.value_json)
}
private setNamespace(namespace: SettingsNamespace, value: unknown): void {
this.db
.prepare(
`INSERT INTO settings (namespace, key, value_json, updated_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(namespace, key) DO UPDATE SET value_json = excluded.value_json, updated_at = excluded.updated_at`,
)
.run(namespace, SETTINGS_KEYS[namespace], JSON.stringify(value), Date.now())
}
}
export function createPiAgentUiSettingsStore(paths = resolvePiAgentUiSettingsPaths()): PiAgentUiSettingsStore {
return new PiAgentUiSettingsStore(paths)
}