feat: add multi-project support (#110)
* feat: Implement project management store with project path validation and synchronization - Added `useProjectsStore` for managing projects, including adding, removing, renaming, and validating project paths. - Implemented persistence for projects and active project ID using safe storage. - Introduced synchronization from desktop settings to keep project data consistent. - Enhanced session store to manage sessions by directory and added new methods for session management. - Updated todo store to fetch session todos based on the directory context. - Refactored server code to validate and resolve project directories for various API endpoints. - Added project entry validation and sanitization to ensure data integrity. * feat(settings): migrate legacy project settings and update settings loading logic * feat: enhance project management with directory-aware settings and improved agent/command source handling * feat: enhance session and project management with directory-aware settings and improved configuration refresh logic * feat: enhance project management with worktree manager integration and project directory resolution * feat: enhance agent groups store with project directory resolution and loading logic * feat: add heartbeat management and global wrapping for SSE blocks in agent and chat providers * feat: refactor command and project handling in useCommandsStore - Replaced useDirectoryStore with useProjectsStore to manage project paths. - Introduced getRequestDirectory function to determine the active project directory. - Updated command fetching to respect project-level scoping. - Enhanced error handling and logging for command configuration fetching. - Improved command configuration saving and updating to utilize project directory context. feat: enhance project path normalization in useProjectsStore - Added resolveTildePath function to expand paths starting with ~. - Updated normalizeProjectPath to utilize home directory for path expansion. fix: update permission handling in useSessionStore - Changed Permission type to PermissionRequest for clarity. - Updated respondToPermission method to use requestId instead of permissionId. refactor: improve permission utilities - Introduced types for PermissionAction and PermissionRule. - Enhanced getAgentDefinition and resolveConfigStore functions for better type safety. - Added resolvePermissionAction to streamline permission resolution logic. feat: add agent configuration retrieval endpoint - Implemented new API endpoint to fetch agent configuration based on project directory. - Enhanced getAgentPermissionSource to prioritize project-level permissions. chore: update SDK version in package.json files - Bumped @opencode-ai/sdk version to ^1.1.1 across all relevant package.json files. refactor: streamline bridge message handling - Updated handleBridgeMessage to accept directory parameter for agent and command requests. - Improved local API request handling to extract directory from query parameters and headers. feat: enhance project configuration management - Added functions to retrieve and merge project configuration paths. - Improved handling of existing project configuration files for agents and commands. * feat: enhance VSCode integration and session management - Added support for a sticky sidebar header background in light and dark themes. - Introduced functions to read VSCode workspace directory and check if running in VSCode. - Implemented detailed logging for session loading and creation processes. - Enhanced session filtering based on directory structure and canonical paths. - Added a new method to reorder projects and prevent modifications in VSCode workspace. - Improved error handling and logging for app initialization and markdown file parsing. - Updated API checks and health checks to ensure readiness before proceeding. - Refactored code for better readability and maintainability across various modules. * feat: improve agent and branch selection logic, enhance session management, and update multi-run creation response * feat: add worktree management actions in agent group detail and sidebar, including delete and keep only options * fix(ui): share IME guard and cover multi-run * fix(session): reduce maximum visible sessions in group from 7 to 5
This commit is contained in:
committed by
GitHub
parent
8aa379e313
commit
18c5b4c7b5
@@ -1,4 +1,4 @@
|
||||
import type { SettingsAPI, SettingsLoadResult, SettingsPayload } from '../../../ui/src/lib/api/types';
|
||||
import type { SettingsAPI, SettingsLoadResult, SettingsPayload } from '@openchamber/ui/lib/api/types';
|
||||
|
||||
// Use same endpoints as web - fetch interceptor handles URL rewriting
|
||||
const SETTINGS_ENDPOINT = '/api/config/settings';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { VSCodeAPI } from '../../../ui/src/lib/api/types';
|
||||
import type { VSCodeAPI } from '@openchamber/ui/lib/api/types';
|
||||
import { executeVSCodeCommand } from './bridge';
|
||||
|
||||
export const createVSCodeActionsAPI = (): VSCodeAPI => ({
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { createVSCodeAPIs } from './api';
|
||||
import { onCommand, onThemeChange, proxyApiRequest, sendBridgeMessage, startSseProxy, stopSseProxy } from './api/bridge';
|
||||
import type { RuntimeAPIs } from '../../ui/src/lib/api/types';
|
||||
import type { RuntimeAPIs } from '@openchamber/ui/lib/api/types';
|
||||
import {
|
||||
buildVSCodeThemeFromPalette,
|
||||
readVSCodeThemePalette,
|
||||
type VSCodeThemeKind,
|
||||
type VSCodeThemePayload,
|
||||
} from '../../ui/src/lib/theme/vscode/adapter';
|
||||
} from '@openchamber/ui/lib/theme/vscode/adapter';
|
||||
|
||||
type ConnectionStatus = 'connecting' | 'connected' | 'error' | 'disconnected';
|
||||
type PanelType = 'chat' | 'agentManager';
|
||||
|
||||
declare const __OPENCHAMBER_WEBVIEW_BUILD_TIME__: string;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__OPENCHAMBER_RUNTIME_APIS__?: RuntimeAPIs;
|
||||
@@ -31,7 +33,15 @@ declare global {
|
||||
}
|
||||
|
||||
console.log('[OpenChamber] VS Code webview starting...');
|
||||
console.log('[OpenChamber] VS Code webview build:', __OPENCHAMBER_WEBVIEW_BUILD_TIME__);
|
||||
console.log('[OpenChamber] Config:', window.__VSCODE_CONFIG__);
|
||||
try {
|
||||
if (window.localStorage.getItem('openchamber_stream_debug') === '1') {
|
||||
console.log('[OpenChamber] Debug: openchamber_stream_debug=1');
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
window.__OPENCHAMBER_RUNTIME_APIS__ = createVSCodeAPIs();
|
||||
|
||||
@@ -238,9 +248,19 @@ onThemeChange((payload) => {
|
||||
|
||||
const workspaceFolder = window.__VSCODE_CONFIG__?.workspaceFolder;
|
||||
if (workspaceFolder) {
|
||||
window.__OPENCHAMBER_HOME__ = workspaceFolder;
|
||||
const normalizeWorkspacePath = (value: string) => {
|
||||
const normalized = value.replace(/\\/g, '/');
|
||||
if (normalized === '/') {
|
||||
return '/';
|
||||
}
|
||||
return normalized.length > 1 ? normalized.replace(/\/+$/, '') : normalized;
|
||||
};
|
||||
|
||||
const normalizedWorkspaceFolder = normalizeWorkspacePath(workspaceFolder);
|
||||
window.__OPENCHAMBER_HOME__ = normalizedWorkspaceFolder;
|
||||
try {
|
||||
window.localStorage.setItem('lastDirectory', workspaceFolder);
|
||||
window.localStorage.setItem('lastDirectory', normalizedWorkspaceFolder);
|
||||
window.localStorage.setItem('homeDirectory', normalizedWorkspaceFolder);
|
||||
} catch (error) {
|
||||
console.warn('Failed to persist workspace folder', error);
|
||||
}
|
||||
@@ -370,8 +390,29 @@ const handleLocalApiRequest = async (url: URL, init?: RequestInit) => {
|
||||
const name = decodeURIComponent(encodedName);
|
||||
const verb = ((init?.method || 'GET') as string).toUpperCase();
|
||||
const body = init?.body ? JSON.parse(init.body as string) : {};
|
||||
const queryDirectory = url.searchParams.get('directory') || undefined;
|
||||
const headerDirectory = (() => {
|
||||
const headers = init?.headers;
|
||||
if (!headers) return undefined;
|
||||
if (headers instanceof Headers) {
|
||||
return headers.get('x-opencode-directory') || undefined;
|
||||
}
|
||||
if (Array.isArray(headers)) {
|
||||
const found = headers.find(([key]) => key.toLowerCase() === 'x-opencode-directory');
|
||||
return found?.[1] || undefined;
|
||||
}
|
||||
if (typeof headers === 'object') {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (key.toLowerCase() === 'x-opencode-directory' && typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})();
|
||||
const directory = queryDirectory || headerDirectory;
|
||||
try {
|
||||
const data = await sendBridgeMessage('api:config/agents', { method: verb, name, body });
|
||||
const data = await sendBridgeMessage('api:config/agents', { method: verb, name, body, directory });
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -384,8 +425,29 @@ const handleLocalApiRequest = async (url: URL, init?: RequestInit) => {
|
||||
const name = decodeURIComponent(encodedName);
|
||||
const verb = ((init?.method || 'GET') as string).toUpperCase();
|
||||
const body = init?.body ? JSON.parse(init.body as string) : {};
|
||||
const queryDirectory = url.searchParams.get('directory') || undefined;
|
||||
const headerDirectory = (() => {
|
||||
const headers = init?.headers;
|
||||
if (!headers) return undefined;
|
||||
if (headers instanceof Headers) {
|
||||
return headers.get('x-opencode-directory') || undefined;
|
||||
}
|
||||
if (Array.isArray(headers)) {
|
||||
const found = headers.find(([key]) => key.toLowerCase() === 'x-opencode-directory');
|
||||
return found?.[1] || undefined;
|
||||
}
|
||||
if (typeof headers === 'object') {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (key.toLowerCase() === 'x-opencode-directory' && typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})();
|
||||
const directory = queryDirectory || headerDirectory;
|
||||
try {
|
||||
const data = await sendBridgeMessage('api:config/commands', { method: verb, name, body });
|
||||
const data = await sendBridgeMessage('api:config/commands', { method: verb, name, body, directory });
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -671,7 +733,7 @@ onCommand('addToContext', (payload) => {
|
||||
const { text } = payload as { text: string };
|
||||
|
||||
// Import the store dynamically to avoid circular dependencies
|
||||
import('../../ui/src/stores/useSessionStore').then(({ useSessionStore }) => {
|
||||
import('@/stores/useSessionStore').then(({ useSessionStore }) => {
|
||||
const store = useSessionStore.getState();
|
||||
const currentText = store.pendingInputText || '';
|
||||
// Append to existing text with double newline separator
|
||||
@@ -685,8 +747,8 @@ onCommand('createSessionWithPrompt', (payload) => {
|
||||
const { prompt } = payload as { prompt: string };
|
||||
|
||||
Promise.all([
|
||||
import('../../ui/src/stores/useSessionStore'),
|
||||
import('../../ui/src/stores/useConfigStore'),
|
||||
import('@/stores/useSessionStore'),
|
||||
import('@/stores/useConfigStore'),
|
||||
]).then(([{ useSessionStore }, { useConfigStore }]) => {
|
||||
const sessionStore = useSessionStore.getState();
|
||||
const configStore = useConfigStore.getState();
|
||||
@@ -719,7 +781,7 @@ onCommand('createSessionWithPrompt', (payload) => {
|
||||
|
||||
// Listen for newSession command from extension title bar button
|
||||
onCommand('newSession', () => {
|
||||
import('../../ui/src/stores/useSessionStore').then(({ useSessionStore }) => {
|
||||
import('@/stores/useSessionStore').then(({ useSessionStore }) => {
|
||||
const store = useSessionStore.getState();
|
||||
store.openNewSessionDraft();
|
||||
});
|
||||
@@ -734,7 +796,7 @@ onCommand('showSettings', () => {
|
||||
window.dispatchEvent(new CustomEvent('openchamber:navigate', { detail: { view: 'settings' } }));
|
||||
});
|
||||
|
||||
import('../../ui/src/main')
|
||||
import('@/main')
|
||||
.then(async () => {
|
||||
await waitForUiMount();
|
||||
uiMounted = true;
|
||||
|
||||
Reference in New Issue
Block a user