Initial public release

This commit is contained in:
Bohdan Triapitsyn
2025-12-07 19:32:53 +02:00
commit 4b2edf7318
319 changed files with 81600 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
import type { DiagnosticsAPI } from '@openchamber/ui/lib/api/types';
type LogResponse = {
fileName?: string;
content?: string;
};
const normalizePayload = (payload: LogResponse): { fileName: string; content: string } => ({
fileName: typeof payload.fileName === 'string' && payload.fileName.trim().length > 0 ? payload.fileName : 'desktop.log',
content: typeof payload.content === 'string' ? payload.content : '',
});
export const createDesktopDiagnosticsAPI = (): DiagnosticsAPI => ({
async downloadLogs() {
try {
const { safeInvoke } = await import('../lib/tauriCallbackManager');
const result = await safeInvoke<LogResponse>('fetch_desktop_logs', {}, {
timeout: 10000,
onCancel: () => {
console.warn('[DiagnosticsAPI] Fetch desktop logs operation timed out');
}
});
return normalizePayload(result ?? {});
} catch (error) {
if (error instanceof Error) {
throw error;
}
throw new Error('Failed to download desktop logs');
}
},
});
+114
View File
@@ -0,0 +1,114 @@
import { safeInvoke } from '../lib/tauriCallbackManager';
import type { DirectoryListResult, FileSearchQuery, FileSearchResult, FilesAPI } from '@openchamber/ui/lib/api/types';
type ListDirectoryResponse = DirectoryListResult & {
path?: string;
entries: Array<
DirectoryListResult['entries'][number] & {
isFile?: boolean;
isSymbolicLink?: boolean;
}
>;
};
type SearchFilesResponse = {
root: string;
count: number;
files: Array<{
name: string;
path: string;
relativePath: string;
extension?: string;
}>;
};
const normalizePath = (path: string): string => path.replace(/\\/g, '/');
const normalizeDirectoryPayload = (result: ListDirectoryResponse): DirectoryListResult => ({
directory: normalizePath(result.directory || result.path || ''),
entries: Array.isArray(result.entries)
? result.entries.map((entry) => ({
name: entry.name || '',
path: normalizePath(entry.path || ''),
isDirectory: entry.isDirectory ?? false,
size: entry.size ?? 0,
modified: (entry as { modified?: string }).modified ?? new Date().toISOString(),
}))
: [],
});
export const createDesktopFilesAPI = (): FilesAPI => ({
async listDirectory(path: string): Promise<DirectoryListResult> {
try {
const result = await safeInvoke<ListDirectoryResponse>('list_directory', {
path: normalizePath(path),
includeHidden: false
}, {
timeout: 10000,
onCancel: () => {
console.warn('[FilesAPI] List directory operation timed out');
}
});
return normalizeDirectoryPayload(result);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(message || 'Failed to list directory');
}
},
async search(payload: FileSearchQuery): Promise<FileSearchResult[]> {
try {
const normalizedDirectory =
typeof payload.directory === 'string' && payload.directory.length > 0
? normalizePath(payload.directory)
: undefined;
const result = await safeInvoke<SearchFilesResponse>('search_files', {
directory: normalizedDirectory,
query: payload.query,
max_results: payload.maxResults || 100
}, {
timeout: 15000,
onCancel: () => {
console.warn('[FilesAPI] Search files operation timed out');
}
});
if (!result || !Array.isArray(result.files)) {
return [];
}
return result.files.map<FileSearchResult>((file) => ({
path: normalizePath(file.path),
preview: file.relativePath ? [normalizePath(file.relativePath)] : undefined,
}));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(message || 'Failed to search files');
}
},
async createDirectory(path: string): Promise<{ success: boolean; path: string }> {
try {
const normalizedPath = normalizePath(path);
const result = await safeInvoke<{ success: boolean; path: string }>('create_directory', {
path: normalizedPath
}, {
timeout: 5000,
onCancel: () => {
console.warn('[FilesAPI] Create directory operation timed out');
}
});
return {
success: Boolean(result?.success),
path: result?.path ? normalizePath(result.path) : normalizedPath,
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(message || 'Failed to create directory');
}
},
});
+230
View File
@@ -0,0 +1,230 @@
import { safeInvoke } from '../lib/tauriCallbackManager';
import type {
GitAPI,
GitStatus,
GitDiffResponse,
GetGitDiffOptions,
GitFileDiffResponse,
GitBranch,
GitDeleteBranchPayload,
GitDeleteRemoteBranchPayload,
GeneratedCommitMessage,
GitWorktreeInfo,
GitAddWorktreePayload,
GitRemoveWorktreePayload,
CreateGitCommitOptions,
GitCommitResult,
GitPushResult,
GitPullResult,
GitLogOptions,
GitLogResponse,
GitCommitFilesResponse,
GitIdentitySummary,
GitIdentityProfile
} from '@openchamber/ui/lib/api/types';
async function safeGitInvoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
try {
return await safeInvoke<T>(command, args, {
timeout: 120000,
onCancel: () => {
console.warn(`[GitAPI] Git operation ${command} did not complete within 120s; it may still be running.`);
}
});
} catch (error) {
const message = typeof error === 'string' ? error : (error as Error).message || 'Unknown error';
throw new Error(message);
}
}
export const createDesktopGitAPI = (): GitAPI => ({
async checkIsGitRepository(directory: string): Promise<boolean> {
return safeGitInvoke<boolean>('check_is_git_repository', { directory });
},
async getGitStatus(directory: string): Promise<GitStatus> {
return safeGitInvoke<GitStatus>('get_git_status', { directory });
},
async getGitDiff(directory: string, options: GetGitDiffOptions): Promise<GitDiffResponse> {
const diff = await safeGitInvoke<string>('get_git_diff', {
directory,
pathStr: options.path,
staged: options.staged,
contextLines: options.contextLines
});
return { diff };
},
async getGitFileDiff(directory: string, options: { path: string }): Promise<GitFileDiffResponse> {
const [original, modified] = await safeGitInvoke<[string, string]>('get_git_file_diff', {
directory,
pathStr: options.path,
});
return {
original: original ?? '',
modified: modified ?? '',
path: options.path,
};
},
async revertGitFile(directory: string, filePath: string): Promise<void> {
return safeGitInvoke<void>('revert_git_file', { directory, filePath });
},
async isLinkedWorktree(directory: string): Promise<boolean> {
return safeGitInvoke<boolean>('is_linked_worktree', { directory });
},
async getGitBranches(directory: string): Promise<GitBranch> {
return safeGitInvoke<GitBranch>('get_git_branches', { directory });
},
async deleteGitBranch(directory: string, payload: GitDeleteBranchPayload): Promise<{ success: boolean }> {
await safeGitInvoke<void>('delete_git_branch', {
directory,
branch: payload.branch,
force: payload.force
});
return { success: true };
},
async deleteRemoteBranch(directory: string, payload: GitDeleteRemoteBranchPayload): Promise<{ success: boolean }> {
await safeGitInvoke<void>('delete_remote_branch', {
directory,
branch: payload.branch,
remote: payload.remote
});
return { success: true };
},
async generateCommitMessage(directory: string, files: string[]): Promise<{ message: GeneratedCommitMessage }> {
const response = await safeGitInvoke<{ message: GeneratedCommitMessage }>('generate_commit_message', {
directory,
files
});
return response;
},
async listGitWorktrees(directory: string): Promise<GitWorktreeInfo[]> {
return safeGitInvoke<GitWorktreeInfo[]>('list_git_worktrees', { directory });
},
async addGitWorktree(directory: string, payload: GitAddWorktreePayload): Promise<{ success: boolean; path: string; branch: string }> {
await safeGitInvoke<void>('add_git_worktree', {
directory,
pathStr: payload.path,
branch: payload.branch,
createBranch: payload.createBranch
});
return { success: true, path: payload.path, branch: payload.branch };
},
async removeGitWorktree(directory: string, payload: GitRemoveWorktreePayload): Promise<{ success: boolean }> {
await safeGitInvoke<void>('remove_git_worktree', {
directory,
pathStr: payload.path,
force: payload.force
});
return { success: true };
},
async ensureOpenChamberIgnored(directory: string): Promise<void> {
return safeGitInvoke<void>('ensure_openchamber_ignored', { directory });
},
async createGitCommit(directory: string, message: string, options?: CreateGitCommitOptions): Promise<GitCommitResult> {
return safeGitInvoke<GitCommitResult>('create_git_commit', {
directory,
message,
addAll: options?.addAll,
files: options?.files
});
},
async gitPush(directory: string, options?: { remote?: string; branch?: string; options?: string[] | Record<string, unknown> }): Promise<GitPushResult> {
return safeGitInvoke<GitPushResult>('git_push', {
directory,
remote: options?.remote,
branch: options?.branch,
options: options?.options
});
},
async gitPull(directory: string, options?: { remote?: string; branch?: string }): Promise<GitPullResult> {
return safeGitInvoke<GitPullResult>('git_pull', {
directory,
remote: options?.remote,
branch: options?.branch
});
},
async gitFetch(directory: string, options?: { remote?: string; branch?: string }): Promise<{ success: boolean }> {
await safeGitInvoke<void>('git_fetch', {
directory,
remote: options?.remote
});
return { success: true };
},
async checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> {
await safeGitInvoke<void>('checkout_branch', { directory, branch });
return { success: true, branch };
},
async createBranch(directory: string, name: string, startPoint?: string): Promise<{ success: boolean; branch: string }> {
await safeGitInvoke<void>('create_branch', {
directory,
name,
startPoint
});
return { success: true, branch: name };
},
async getGitLog(directory: string, options?: GitLogOptions): Promise<GitLogResponse> {
return safeGitInvoke<GitLogResponse>('get_git_log', {
directory,
maxCount: options?.maxCount,
from: options?.from,
to: options?.to,
file: options?.file
});
},
async getCommitFiles(directory: string, hash: string): Promise<GitCommitFilesResponse> {
return safeGitInvoke<GitCommitFilesResponse>('get_commit_files', {
directory,
hash
});
},
async getCurrentGitIdentity(directory: string): Promise<GitIdentitySummary | null> {
try {
return await safeGitInvoke<GitIdentitySummary>('get_current_git_identity', { directory });
} catch {
return null;
}
},
async setGitIdentity(directory: string, profileId: string): Promise<{ success: boolean; profile: GitIdentityProfile }> {
const profile = await safeGitInvoke<GitIdentityProfile>('set_git_identity', { directory, profileId });
return { success: true, profile };
},
async getGitIdentities(): Promise<GitIdentityProfile[]> {
return safeGitInvoke<GitIdentityProfile[]>('get_git_identities');
},
async createGitIdentity(profile: GitIdentityProfile): Promise<GitIdentityProfile> {
return safeGitInvoke<GitIdentityProfile>('create_git_identity', { profile });
},
async updateGitIdentity(id: string, updates: GitIdentityProfile): Promise<GitIdentityProfile> {
return safeGitInvoke<GitIdentityProfile>('update_git_identity', { id, updates });
},
async deleteGitIdentity(id: string): Promise<void> {
return safeGitInvoke<void>('delete_git_identity', { id });
},
});
+57
View File
@@ -0,0 +1,57 @@
import type { RuntimeAPIs, TerminalHandlers } from '@openchamber/ui/lib/api/types';
import { createDesktopTerminalAPI } from './terminal';
import { createDesktopGitAPI } from './git';
import { createDesktopFilesAPI } from './files';
import { createDesktopSettingsAPI } from './settings';
import { createDesktopPermissionsAPI } from './permissions';
import { createDesktopDiagnosticsAPI } from './diagnostics';
import { createDesktopNotificationsAPI } from './notifications';
import { createDesktopToolsAPI } from './tools';
const activeTerminalConnections = new Set<string>();
export const createDesktopAPIs = (): RuntimeAPIs & { cleanup?: () => void } => {
const terminalAPI = createDesktopTerminalAPI();
const originalConnect = terminalAPI.connect.bind(terminalAPI);
const wrappedTerminalAPI = {
...terminalAPI,
connect: (sessionId: string, handlers: TerminalHandlers) => {
activeTerminalConnections.add(sessionId);
const connection = originalConnect(sessionId, handlers);
const originalClose = connection.close;
return {
...connection,
close: () => {
activeTerminalConnections.delete(sessionId);
originalClose();
},
};
},
};
return {
runtime: { platform: 'desktop', isDesktop: true, label: 'tauri-bootstrap' },
terminal: wrappedTerminalAPI,
git: createDesktopGitAPI(),
files: createDesktopFilesAPI(),
settings: createDesktopSettingsAPI(),
permissions: createDesktopPermissionsAPI(),
notifications: createDesktopNotificationsAPI(),
diagnostics: createDesktopDiagnosticsAPI(),
tools: createDesktopToolsAPI(),
cleanup: () => {
console.info('[DesktopAPIs] Performing cleanup...');
const activeConnections = Array.from(activeTerminalConnections);
activeConnections.forEach(sessionId => {
console.info(`[DesktopAPIs] Closing terminal session: ${sessionId}`);
activeTerminalConnections.delete(sessionId);
});
console.info(`[DesktopAPIs] Cleanup completed, closed ${activeConnections.length} terminal connections`);
},
};
};
+58
View File
@@ -0,0 +1,58 @@
import type { NotificationsAPI, NotificationPayload } from '@openchamber/ui/lib/api/types';
import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
import { safeInvoke } from '../lib/tauriCallbackManager';
export const requestInitialNotificationPermission = async (): Promise<void> => {
try {
if (typeof window !== 'undefined' && 'Notification' in window) {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.warn('[notifications] Notification permission not granted');
}
}
} catch (error) {
console.error('[notifications] Failed to request permission:', error);
}
};
export const createDesktopNotificationsAPI = (): NotificationsAPI => ({
async notifyAgentCompletion(payload?: NotificationPayload): Promise<boolean> {
try {
let granted = await isPermissionGranted();
if (!granted) {
const permission = await requestPermission();
granted = permission === 'granted';
}
if (!granted) {
console.warn('[notifications] Cannot send notification: Permission denied');
return false;
}
await safeInvoke(
'desktop_notify',
{ payload },
{
timeout: 5000,
onCancel: () => {
console.warn('[NotificationsAPI] Notify operation timed out');
},
},
);
return true;
} catch (error) {
console.error('[notifications] Failed to send notification:', error);
return false;
}
},
async canNotify(): Promise<boolean> {
try {
return await isPermissionGranted();
} catch (error) {
console.warn('[notifications] Failed to check notification permission:', error);
return false;
}
}
});
+49
View File
@@ -0,0 +1,49 @@
import type { DirectoryPermissionRequest, DirectoryPermissionResult, PermissionsAPI, StartAccessingResult } from '@openchamber/ui/lib/api/types';
export const createDesktopPermissionsAPI = (): PermissionsAPI => ({
async requestDirectoryAccess(request: DirectoryPermissionRequest): Promise<DirectoryPermissionResult> {
try {
const { safeInvoke } = await import('../lib/tauriCallbackManager');
const result = await safeInvoke<DirectoryPermissionResult>('request_directory_access', { request }, {
timeout: 30000,
onCancel: () => {
console.warn('[PermissionsAPI] Request directory access operation timed out');
}
});
return result;
} catch (error) {
console.error('[desktop] Error requesting directory access:', error);
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
},
async startAccessingDirectory(path: string): Promise<StartAccessingResult> {
try {
const { safeInvoke } = await import('../lib/tauriCallbackManager');
const result = await safeInvoke<StartAccessingResult>('start_accessing_directory', { path }, {
timeout: 10000,
onCancel: () => {
console.warn('[PermissionsAPI] Start accessing directory operation timed out');
}
});
return result;
} catch (error) {
console.error('[desktop] Error starting directory access:', error);
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
},
async stopAccessingDirectory(path: string): Promise<StartAccessingResult> {
try {
const { safeInvoke } = await import('../lib/tauriCallbackManager');
const result = await safeInvoke<StartAccessingResult>('stop_accessing_directory', { path }, {
timeout: 5000,
onCancel: () => {
console.warn('[PermissionsAPI] Stop accessing directory operation timed out');
}
});
return result;
} catch (error) {
console.error('[desktop] Error stopping directory access:', error);
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
},
});
+58
View File
@@ -0,0 +1,58 @@
import type { SettingsAPI, SettingsLoadResult, SettingsPayload } from '@openchamber/ui/lib/api/types';
const sanitizePayload = (data: unknown): SettingsPayload => {
if (!data || typeof data !== 'object') {
return {};
}
return data as SettingsPayload;
};
export const createDesktopSettingsAPI = (): SettingsAPI => ({
async load(): Promise<SettingsLoadResult> {
try {
const { safeInvoke } = await import('../lib/tauriCallbackManager');
const result = await safeInvoke<{ settings: unknown; source: 'desktop' | 'web' }>('load_settings', {}, {
timeout: 5000,
onCancel: () => {
console.warn('[SettingsAPI] Load settings operation timed out');
}
});
return {
settings: sanitizePayload(result.settings),
source: result.source,
};
} catch (error) {
throw new Error(`Failed to load settings: ${error instanceof Error ? error.message : String(error)}`);
}
},
async save(changes: Partial<SettingsPayload>): Promise<SettingsPayload> {
try {
const { safeInvoke } = await import('../lib/tauriCallbackManager');
const result = await safeInvoke<unknown>('save_settings', { changes }, {
timeout: 5000,
onCancel: () => {
console.warn('[SettingsAPI] Save settings operation timed out');
}
});
return sanitizePayload(result);
} catch (error) {
throw new Error(`Failed to save settings: ${error instanceof Error ? error.message : String(error)}`);
}
},
async restartOpenCode(): Promise<{ restarted: boolean }> {
try {
const { safeInvoke } = await import('../lib/tauriCallbackManager');
const result = await safeInvoke<{ restarted: boolean }>('restart_opencode', {}, {
timeout: 10000,
onCancel: () => {
console.warn('[SettingsAPI] Restart OpenCode operation timed out');
}
});
return { restarted: result.restarted };
} catch (error) {
throw new Error(`Failed to restart OpenCode: ${error instanceof Error ? error.message : String(error)}`);
}
},
});
+123
View File
@@ -0,0 +1,123 @@
import { safeInvoke, safeListen } from '../lib/tauriCallbackManager';
import type {
TerminalAPI,
TerminalHandlers,
CreateTerminalOptions,
ResizeTerminalPayload,
TerminalSession,
TerminalStreamEvent
} from '@openchamber/ui/lib/api/types';
async function safeTerminalInvoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
try {
return await safeInvoke<T>(command, args, {
timeout: 10000,
onCancel: () => {
console.warn(`[TerminalAPI] Command ${command} timed out`);
}
});
} catch (error) {
const message = typeof error === 'string' ? error : (error as Error).message || 'Unknown error';
throw new Error(message);
}
}
export const createDesktopTerminalAPI = (): TerminalAPI => ({
async createSession(options: CreateTerminalOptions): Promise<TerminalSession> {
const cols = options.cols ?? 80;
const rows = options.rows ?? 24;
const res = await safeTerminalInvoke<{ session_id: string }>('create_terminal_session', {
payload: {
cols,
rows,
cwd: options.cwd
}
});
return {
sessionId: res.session_id,
cols,
rows
};
},
connect(sessionId: string, handlers: TerminalHandlers) {
let unlistenFn: (() => void) | undefined;
let cancelled = false;
let isConnected = false;
const stopListening = () => {
if (unlistenFn) {
unlistenFn();
unlistenFn = undefined;
isConnected = false;
}
};
const startListening = async () => {
try {
const unlisten = await safeListen<TerminalStreamEvent>(`terminal://${sessionId}`, (event) => {
if (cancelled) {
return;
}
handlers.onEvent(event.payload);
if (event.payload?.type === 'exit') {
stopListening();
}
});
if (cancelled) {
unlisten();
return;
}
unlistenFn = unlisten;
isConnected = true;
handlers.onEvent({ type: 'connected' });
} catch (err) {
console.error('Failed to listen to terminal events:', err);
if (!cancelled) {
handlers.onError?.(err instanceof Error ? err : new Error(String(err)));
}
}
};
startListening();
return {
close: () => {
cancelled = true;
stopListening();
},
isConnected: () => isConnected,
};
},
async sendInput(sessionId: string, input: string): Promise<void> {
await safeTerminalInvoke('send_terminal_input', {
sessionId,
session_id: sessionId,
data: input,
});
},
async resize(payload: ResizeTerminalPayload): Promise<void> {
await safeTerminalInvoke('resize_terminal', {
sessionId: payload.sessionId,
session_id: payload.sessionId,
cols: payload.cols,
rows: payload.rows,
});
},
async close(sessionId: string): Promise<void> {
await safeTerminalInvoke('close_terminal', {
sessionId,
session_id: sessionId,
});
},
});
+22
View File
@@ -0,0 +1,22 @@
import type { ToolsAPI } from '@openchamber/ui/lib/api/types';
export const createDesktopToolsAPI = (): ToolsAPI => ({
async getAvailableTools(): Promise<string[]> {
const response = await fetch('/api/experimental/tool/ids');
if (!response.ok) {
throw new Error(`Tools API returned ${response.status} ${response.statusText}`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error('Tools API returned invalid data format');
}
return data
.filter((tool: unknown): tool is string => typeof tool === 'string' && tool !== 'invalid')
.sort();
},
});
+105
View File
@@ -0,0 +1,105 @@
export interface UpdateInfo {
available: boolean;
version?: string;
currentVersion: string;
body?: string;
date?: string;
}
export interface UpdateProgress {
downloaded: number;
total?: number;
}
interface Update {
version: string;
body?: string;
date?: string;
downloadAndInstall: (
onEvent?: (event: DownloadEvent) => void
) => Promise<void>;
}
type DownloadEvent =
| { event: 'Started'; data: { contentLength?: number } }
| { event: 'Progress'; data: { chunkLength: number } }
| { event: 'Finished' };
let cachedUpdate: Update | null = null;
export async function checkForUpdates(): Promise<UpdateInfo> {
try {
const { check } = await import('@tauri-apps/plugin-updater');
const update = await check();
cachedUpdate = update;
if (!update) {
return {
available: false,
currentVersion: await getCurrentVersion(),
};
}
return {
available: true,
version: update.version,
currentVersion: await getCurrentVersion(),
body: update.body ?? undefined,
date: update.date ?? undefined,
};
} catch (error) {
console.error('[updater] Failed to check for updates:', error);
return {
available: false,
currentVersion: await getCurrentVersion(),
};
}
}
export async function downloadUpdate(
onProgress?: (progress: UpdateProgress) => void
): Promise<void> {
let update = cachedUpdate;
if (!update) {
const { check } = await import('@tauri-apps/plugin-updater');
const checked = await check();
if (!checked) {
throw new Error('No update available');
}
update = checked;
cachedUpdate = checked;
}
let downloaded = 0;
let total: number | undefined;
await update.downloadAndInstall((event: DownloadEvent) => {
switch (event.event) {
case 'Started':
total = event.data.contentLength;
onProgress?.({ downloaded: 0, total });
break;
case 'Progress':
downloaded += event.data.chunkLength;
onProgress?.({ downloaded, total });
break;
case 'Finished':
onProgress?.({ downloaded: total ?? downloaded, total });
break;
}
});
}
export async function restartToUpdate(): Promise<void> {
const { relaunch } = await import('@tauri-apps/plugin-process');
await relaunch();
}
async function getCurrentVersion(): Promise<string> {
try {
const { getVersion } = await import('@tauri-apps/api/app');
return await getVersion();
} catch {
return 'unknown';
}
}