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
+70
View File
@@ -0,0 +1,70 @@
import type { DirectoryListResult, FileSearchQuery, FileSearchResult, FilesAPI } from '@openchamber/ui/lib/api/types';
const normalizePath = (path: string): string => path.replace(/\\/g, '/');
export const createWebFilesAPI = (): FilesAPI => ({
async listDirectory(path: string): Promise<DirectoryListResult> {
const target = normalizePath(path);
const response = await fetch('/api/fs/list', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: target }),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: response.statusText }));
throw new Error(error.error || 'Failed to list directory');
}
return response.json();
},
async search(payload: FileSearchQuery): Promise<FileSearchResult[]> {
const response = await fetch('/api/fs/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
directory: normalizePath(payload.directory),
query: payload.query,
maxResults: payload.maxResults,
}),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: response.statusText }));
throw new Error(error.error || 'Failed to search files');
}
const results = (await response.json()) as unknown;
if (!Array.isArray(results)) {
return [];
}
return results
.filter((item): item is FileSearchResult => !!item && typeof item === 'object' && typeof (item as { path?: string }).path === 'string')
.map((item) => ({
path: normalizePath((item as FileSearchResult).path),
score: (item as FileSearchResult).score,
preview: (item as FileSearchResult).preview,
}));
},
async createDirectory(path: string): Promise<{ success: boolean; path: string }> {
const target = normalizePath(path);
const response = await fetch('/api/fs/mkdir', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: target }),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: response.statusText }));
throw new Error(error.error || 'Failed to create directory');
}
const result = await response.json();
return {
success: Boolean(result?.success),
path: typeof result?.path === 'string' ? normalizePath(result.path) : target,
};
},
});
+41
View File
@@ -0,0 +1,41 @@
import * as gitApiHttp from '@openchamber/ui/lib/gitApiHttp';
import type {
GitAPI,
CreateGitCommitOptions,
GitLogOptions,
} from '@openchamber/ui/lib/api/types';
export const createWebGitAPI = (): GitAPI => ({
checkIsGitRepository: gitApiHttp.checkIsGitRepository,
getGitStatus: gitApiHttp.getGitStatus,
getGitDiff: gitApiHttp.getGitDiff,
getGitFileDiff: gitApiHttp.getGitFileDiff,
revertGitFile: gitApiHttp.revertGitFile,
isLinkedWorktree: gitApiHttp.isLinkedWorktree,
getGitBranches: gitApiHttp.getGitBranches,
deleteGitBranch: gitApiHttp.deleteGitBranch as GitAPI['deleteGitBranch'],
deleteRemoteBranch: gitApiHttp.deleteRemoteBranch as GitAPI['deleteRemoteBranch'],
generateCommitMessage: gitApiHttp.generateCommitMessage,
listGitWorktrees: gitApiHttp.listGitWorktrees,
addGitWorktree: gitApiHttp.addGitWorktree as GitAPI['addGitWorktree'],
removeGitWorktree: gitApiHttp.removeGitWorktree as GitAPI['removeGitWorktree'],
ensureOpenChamberIgnored: gitApiHttp.ensureOpenChamberIgnored,
createGitCommit(directory: string, message: string, options?: CreateGitCommitOptions) {
return gitApiHttp.createGitCommit(directory, message, options);
},
gitPush: gitApiHttp.gitPush,
gitPull: gitApiHttp.gitPull,
gitFetch: gitApiHttp.gitFetch,
checkoutBranch: gitApiHttp.checkoutBranch,
createBranch: gitApiHttp.createBranch,
getGitLog(directory: string, options?: GitLogOptions) {
return gitApiHttp.getGitLog(directory, options);
},
getCommitFiles: gitApiHttp.getCommitFiles,
getCurrentGitIdentity: gitApiHttp.getCurrentGitIdentity,
setGitIdentity: gitApiHttp.setGitIdentity,
getGitIdentities: gitApiHttp.getGitIdentities,
createGitIdentity: gitApiHttp.createGitIdentity,
updateGitIdentity: gitApiHttp.updateGitIdentity,
deleteGitIdentity: gitApiHttp.deleteGitIdentity,
});
+19
View File
@@ -0,0 +1,19 @@
import type { RuntimeAPIs } from '@openchamber/ui/lib/api/types';
import { createWebTerminalAPI } from './terminal';
import { createWebGitAPI } from './git';
import { createWebFilesAPI } from './files';
import { createWebSettingsAPI } from './settings';
import { createWebPermissionsAPI } from './permissions';
import { createWebNotificationsAPI } from './notifications';
import { createWebToolsAPI } from './tools';
export const createWebAPIs = (): RuntimeAPIs => ({
runtime: { platform: 'web', isDesktop: false, label: 'web' },
terminal: createWebTerminalAPI(),
git: createWebGitAPI(),
files: createWebFilesAPI(),
settings: createWebSettingsAPI(),
permissions: createWebPermissionsAPI(),
notifications: createWebNotificationsAPI(),
tools: createWebToolsAPI(),
});
+32
View File
@@ -0,0 +1,32 @@
import type { NotificationPayload, NotificationsAPI } from '@openchamber/ui/lib/api/types';
const notifyWithWebAPI = async (payload?: NotificationPayload): Promise<boolean> => {
if (typeof Notification === 'undefined') {
console.info('Notifications not supported in this environment', payload);
return false;
}
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.warn('Notification permission not granted');
return false;
}
try {
new Notification(payload?.title ?? 'OpenChamber', {
body: payload?.body,
tag: payload?.tag,
});
return true;
} catch (error) {
console.warn('Failed to send notification', error);
return false;
}
};
export const createWebNotificationsAPI = (): NotificationsAPI => ({
async notifyAgentCompletion(payload?: NotificationPayload): Promise<boolean> {
return notifyWithWebAPI(payload);
},
canNotify: () => (typeof Notification !== 'undefined' ? Notification.permission === 'granted' : false),
});
+15
View File
@@ -0,0 +1,15 @@
import type { DirectoryPermissionRequest, PermissionsAPI, StartAccessingResult } from '@openchamber/ui/lib/api/types';
export const createWebPermissionsAPI = (): PermissionsAPI => ({
async requestDirectoryAccess(request: DirectoryPermissionRequest) {
return { success: true, path: request.path };
},
async startAccessingDirectory(path: string): Promise<StartAccessingResult> {
void path;
return { success: true };
},
async stopAccessingDirectory(path: string): Promise<StartAccessingResult> {
void path;
return { success: true };
},
});
+58
View File
@@ -0,0 +1,58 @@
import type { SettingsAPI, SettingsLoadResult, SettingsPayload } from '@openchamber/ui/lib/api/types';
const SETTINGS_ENDPOINT = '/api/config/settings';
const RELOAD_ENDPOINT = '/api/config/reload';
const sanitizePayload = (data: unknown): SettingsPayload => {
if (!data || typeof data !== 'object') {
return {};
}
return data as SettingsPayload;
};
export const createWebSettingsAPI = (): SettingsAPI => ({
async load(): Promise<SettingsLoadResult> {
const response = await fetch(SETTINGS_ENDPOINT, {
method: 'GET',
headers: { Accept: 'application/json' },
});
if (!response.ok) {
throw new Error(`Failed to load settings: ${response.statusText}`);
}
const payload = sanitizePayload(await response.json().catch(() => ({})));
return {
settings: payload,
source: 'web',
};
},
async save(changes: Partial<SettingsPayload>): Promise<SettingsPayload> {
const response = await fetch(SETTINGS_ENDPOINT, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(changes),
});
if (!response.ok) {
const error = await response.json().catch(() => ({ error: response.statusText }));
throw new Error(error.error || 'Failed to save settings');
}
const payload = sanitizePayload(await response.json().catch(() => ({})));
return payload;
},
async restartOpenCode(): Promise<{ restarted: boolean }> {
const response = await fetch(RELOAD_ENDPOINT, { method: 'POST' });
if (!response.ok) {
const error = await response.json().catch(() => ({ error: response.statusText }));
throw new Error(error.error || 'Failed to restart OpenCode');
}
return { restarted: true };
},
});
+56
View File
@@ -0,0 +1,56 @@
import {
connectTerminalStream,
createTerminalSession,
resizeTerminal,
sendTerminalInput,
closeTerminal,
} from '@openchamber/ui/lib/terminalApi';
import type {
TerminalAPI,
TerminalHandlers,
TerminalStreamOptions,
CreateTerminalOptions,
ResizeTerminalPayload,
TerminalSession,
} from '@openchamber/ui/lib/api/types';
const getRetryPolicy = (options?: TerminalStreamOptions) => {
const retry = options?.retry;
return {
maxRetries: retry?.maxRetries ?? 3,
initialRetryDelay: retry?.initialDelayMs ?? 1000,
maxRetryDelay: retry?.maxDelayMs ?? 8000,
connectionTimeout: options?.connectionTimeoutMs ?? 10000,
};
};
export const createWebTerminalAPI = (): TerminalAPI => ({
async createSession(options: CreateTerminalOptions): Promise<TerminalSession> {
return createTerminalSession(options);
},
connect(sessionId: string, handlers: TerminalHandlers, options?: TerminalStreamOptions) {
const unsubscribe = connectTerminalStream(
sessionId,
handlers.onEvent,
handlers.onError,
getRetryPolicy(options)
);
return {
close: () => unsubscribe(),
};
},
async sendInput(sessionId: string, input: string): Promise<void> {
await sendTerminalInput(sessionId, input);
},
async resize(payload: ResizeTerminalPayload): Promise<void> {
await resizeTerminal(payload.sessionId, payload.cols, payload.rows);
},
async close(sessionId: string): Promise<void> {
await closeTerminal(sessionId);
},
});
+22
View File
@@ -0,0 +1,22 @@
import type { ToolsAPI } from '@openchamber/ui/lib/api/types';
export const createWebToolsAPI = (): 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();
},
});
+13
View File
@@ -0,0 +1,13 @@
import { createWebAPIs } from './api';
import type { RuntimeAPIs } from '@openchamber/ui/lib/api/types';
import '@openchamber/ui/index.css';
import '@openchamber/ui/styles/fonts';
declare global {
interface Window {
__OPENCHAMBER_RUNTIME_APIS__?: RuntimeAPIs;
}
}
window.__OPENCHAMBER_RUNTIME_APIS__ = createWebAPIs();
import('@openchamber/ui/main');