Initial public release
This commit is contained in:
@@ -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');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
@@ -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`);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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) };
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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)}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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();
|
||||
},
|
||||
});
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user