LiteChat provides comprehensive Git integration enabling version control workflows, conversation synchronization, and collaborative development directly within the browser. All Git operations are performed using isomorphic-git on the Virtual File System.
- Git Tools Module (
src/controls/modules/GitToolsModule.ts) - AI tools for Git operations - VFS Git Operations (
src/lib/litechat/vfs-git-operations.ts) - Git operations on VFS - Conversation Sync - Links conversations to Git repositories
- Sync VFS - Dedicated
sync_reposVirtual File System for Git operations
LiteChat uses isomorphic-git for browser-based Git operations:
import git from 'isomorphic-git';
import http from 'isomorphic-git/http/web';
// All operations work directly on the VFS filesystem
await git.clone({
fs,
http,
dir: '/repo',
url: 'https://github.com/user/repo.git'
});Git repositories are configured in Settings → Git → Sync Repositories:
interface SyncRepo {
id: string;
name: string;
url: string; // HTTPS Git URL
username?: string; // Git username (optional)
password?: string; // Personal access token
createdAt: Date;
}- Public Repositories: No authentication required
- Private Repositories: Username/password or username/token
- Personal Access Tokens: Recommended for private repositories
- GitHub: Use personal access tokens instead of passwords
// Add sync repository
emitter.emit(conversationEvent.addSyncRepoRequest, {
name: "My Project",
url: "https://github.com/user/project.git",
username: "user",
password: "ghp_token123"
});
// Update repository
emitter.emit(conversationEvent.updateSyncRepoRequest, {
id: "repo-id",
updates: { password: "new-token" }
});
// Delete repository
emitter.emit(conversationEvent.deleteSyncRepoRequest, {
id: "repo-id"
});Conversations can be linked to configured sync repositories:
// Link conversation to repository
emitter.emit(conversationEvent.linkConversationToRepoRequest, {
conversationId: "conv-id",
repoId: "repo-id"
});
// Unlink conversation
emitter.emit(conversationEvent.unlinkConversationFromRepoRequest, {
conversationId: "conv-id"
});Conversations are synchronized as JSON files within the repository:
repository/
├── .litechat/
│ └── conversations/
│ ├── conv-123.json
│ ├── conv-456.json
│ └── metadata.json
├── src/
├── docs/
└── README.md
{
"id": "conv-123",
"title": "Implement user authentication",
"projectId": "project-abc",
"metadata": {
"tags": ["feature", "auth"],
"syncedAt": "2024-01-01T12:00:00Z"
},
"interactions": [
{
"id": "int-1",
"type": "message.user_assistant",
"prompt": {
"text": "How should I implement authentication?",
"metadata": {}
},
"response": "Here's how you can implement authentication...",
"status": "COMPLETED",
"startedAt": "2024-01-01T12:00:00Z",
"endedAt": "2024-01-01T12:01:30Z"
}
]
}// Sync specific conversation
emitter.emit(conversationEvent.syncConversationRequest, {
conversationId: "conv-id"
});
// Sync all linked conversations for a repository
emitter.emit(conversationEvent.syncAllConversationsRequest, {
repoId: "repo-id"
});The Git Tools Module provides AI-accessible Git operations:
// git_status tool
{
"name": "git_status",
"description": "Get the current status of the Git repository",
"parameters": {
"type": "object",
"properties": {
"detailed": {
"type": "boolean",
"description": "Include detailed file status"
}
}
}
}// git_add tool
{
"name": "git_add",
"description": "Stage files for commit",
"parameters": {
"type": "object",
"properties": {
"files": {
"type": "array",
"items": { "type": "string" },
"description": "File paths to stage"
}
},
"required": ["files"]
}
}// git_commit tool
{
"name": "git_commit",
"description": "Create a commit with staged changes",
"parameters": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Commit message"
},
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"email": { "type": "string" }
}
}
},
"required": ["message"]
}
}// git_push tool
{
"name": "git_push",
"description": "Push commits to remote repository",
"parameters": {
"type": "object",
"properties": {
"remote": {
"type": "string",
"default": "origin"
},
"branch": {
"type": "string",
"default": "main"
}
}
}
}
// git_pull tool
{
"name": "git_pull",
"description": "Pull changes from remote repository",
"parameters": {
"type": "object",
"properties": {
"remote": { "type": "string", "default": "origin" },
"branch": { "type": "string", "default": "main" }
}
}
}// Git status tool implementation
const gitStatusImplementation = async (
{ detailed = false }: { detailed?: boolean },
context: ToolContext
) => {
try {
const fs = await VfsOps.initializeFsOp("sync_repos");
if (!fs) throw new Error("VFS not available");
const status = await git.statusMatrix({
fs,
dir: "/repository",
});
const result = status.map(([filepath, headStatus, workdirStatus, stageStatus]) => ({
file: filepath,
status: getFileStatus(headStatus, workdirStatus, stageStatus),
staged: stageStatus !== 0,
modified: workdirStatus !== headStatus
}));
return {
success: true,
files: result,
summary: `${result.length} files tracked`
};
} catch (error) {
return {
success: false,
error: error.message
};
}
};Core Git operations are implemented in vfs-git-operations.ts:
// Clone repository
export const cloneRepositoryOp = async (
url: string,
targetDir: string,
auth?: { username: string; password: string },
options?: { fsInstance?: typeof fs }
): Promise<void> => {
const fsToUse = options?.fsInstance ?? fs;
await git.clone({
fs: fsToUse,
http,
dir: targetDir,
url,
...(auth && {
onAuth: () => auth
})
});
};
// Get repository status
export const getStatusOp = async (
repoDir: string,
options?: { fsInstance?: typeof fs }
): Promise<GitFileStatus[]> => {
const fsToUse = options?.fsInstance ?? fs;
const status = await git.statusMatrix({
fs: fsToUse,
dir: repoDir
});
return status.map(parseFileStatus);
};
// Create commit
export const commitOp = async (
repoDir: string,
message: string,
author: { name: string; email: string },
options?: { fsInstance?: typeof fs }
): Promise<string> => {
const fsToUse = options?.fsInstance ?? fs;
const sha = await git.commit({
fs: fsToUse,
dir: repoDir,
message,
author
});
return sha;
};// Authentication for private repositories
const authCallback = (url: string, auth: AuthInfo) => {
return {
username: auth.username,
password: auth.password // Personal access token
};
};
// Used in Git operations
await git.push({
fs,
http,
dir: repoDir,
remote: "origin",
ref: "main",
onAuth: () => authCallback(repoUrl, authInfo)
});Settings → Git provides:
- User Configuration: Git user name and email for commits
- Sync Repositories: Add, edit, and delete sync repositories
- Repository Status: View sync status and last sync times
In conversation settings:
- Link Repository: Dropdown to select configured sync repository
- Sync Status: Visual indicator of sync state
- Manual Sync: Button to trigger immediate sync
- Sync History: Log of sync operations and results
Projects can be configured with default sync repositories:
interface ProjectSettings {
defaultSyncRepoId?: string; // Auto-link new conversations
autoSync?: boolean; // Automatic sync on conversation updates
syncBranch?: string; // Target branch for syncing
}type SyncStatus =
| "not_synced" // Never synced
| "synced" // Up to date
| "pending" // Sync in progress
| "conflict" // Merge conflict
| "error"; // Sync failed
interface ConversationSyncStatus {
conversationId: string;
repoId: string;
status: SyncStatus;
lastSyncAt?: Date;
lastError?: string;
conflictDetails?: ConflictInfo;
}When sync conflicts occur:
- Detection: Compare local and remote conversation timestamps
- User Choice: Present conflict resolution options
- Resolution Strategies:
- Keep local version
- Use remote version
- Manual merge (show diff)
- Create branch for local changes
// Configure automatic sync
const autoSyncSettings = {
enabled: true,
interval: 300, // 5 minutes
onConversationUpdate: true,
onInteractionComplete: true
};
// Trigger conditions
- New interaction completed
- Conversation title changed
- Conversation metadata updated
- Manual trigger from UI// Authentication errors
- Invalid credentials
- Token expired
- Repository access denied
// Network errors
- No internet connection
- Repository server unavailable
- Timeout during operations
// Git errors
- Merge conflicts
- Repository not found
- Invalid Git URL
- File permission issues
// VFS errors
- Filesystem not initialized
- Insufficient storage space
- Corrupted repository state// Automatic retry logic
const retryConfig = {
maxRetries: 3,
backoffMs: 1000,
retryableErrors: [
"NetworkError",
"TimeoutError",
"TemporaryError"
]
};
// Manual recovery options
- Retry failed operation
- Reset repository state
- Re-clone repository
- Update authentication- Credentials stored encrypted in IndexedDB
- No server-side credential storage
- Personal access tokens recommended over passwords
- Automatic credential validation
- HTTPS-only repository URLs
- No SSH key support (browser limitation)
- Sandboxed Git operations within VFS
- No arbitrary command execution
- Conversations only synced to explicitly configured repositories
- No automatic cloud sync without user consent
- Local-first with optional remote sync
- Full user control over sync scope and timing
-
Define Tool Schema:
const customGitTool = z.object({ operation: z.enum(["log", "diff", "branch"]), options: z.record(z.any()).optional() });
-
Implement Tool Logic:
const implementation = async (params, context) => { const fs = await VfsOps.initializeFsOp("sync_repos"); // Implement Git operation return result; };
-
Register Tool:
modApi.registerTool("custom_git", schema, implementation);
// Custom sync handlers
const customSyncHandler = {
canHandle: (repoUrl: string) => repoUrl.includes("custom-git.com"),
authenticate: (credentials) => customAuth(credentials),
sync: (conversation, repoConfig) => customSyncLogic(conversation, repoConfig)
};
// Register custom handler
registerSyncHandler(customSyncHandler);// Mock Git operations for testing
const mockGitOps = {
clone: jest.fn(),
commit: jest.fn(),
push: jest.fn(),
status: jest.fn()
};
// Test sync workflow
test("conversation sync workflow", async () => {
const conversation = createTestConversation();
const repo = createTestRepo();
await syncConversation(conversation, repo);
expect(mockGitOps.commit).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining(conversation.title)
})
);
});{
"name": "Development Repo",
"url": "https://github.com/user/litechat-dev.git",
"username": "user",
"password": "ghp_development_token",
"autoSync": true,
"branch": "conversations"
}{
"name": "Team Conversations",
"url": "https://github.com/team/project-conversations.git",
"username": "bot-user",
"password": "ghp_team_access_token",
"autoSync": false, // Manual sync for review
"conflictStrategy": "manual"
}{
"name": "Personal Backup",
"url": "https://github.com/user/litechat-backup.git",
"username": "user",
"password": "ghp_backup_token",
"autoSync": true,
"syncInterval": 900, // 15 minutes
"includeMetadata": true
}