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