feat: Add Diff View and File History Tracking

- Implemented a new Diff view in the Workbench to track file changes
- Added file history tracking with version control and change tracking
- Created a FileModifiedDropdown to browse and manage modified files
- Enhanced ActionRunner to support file history persistence
- Updated Workbench and BaseChat components to support new diff view functionality
- Added support for inline and side-by-side diff view modes
This commit is contained in:
Toddyclipsgg
2025-02-16 23:10:15 -03:00
parent 744b6c2433
commit 382bf2c9a3
14 changed files with 1356 additions and 375 deletions

View File

@@ -1,7 +1,7 @@
import type { WebContainer } from '@webcontainer/api';
import { path } from '~/utils/path';
import { path as nodePath } from '~/utils/path';
import { atom, map, type MapStore } from 'nanostores';
import type { ActionAlert, BoltAction } from '~/types/actions';
import type { ActionAlert, BoltAction, FileHistory } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable';
import type { ActionCallbackData } from './message-parser';
@@ -276,9 +276,9 @@ export class ActionRunner {
}
const webcontainer = await this.#webcontainer;
const relativePath = path.relative(webcontainer.workdir, action.filePath);
const relativePath = nodePath.relative(webcontainer.workdir, action.filePath);
let folder = path.dirname(relativePath);
let folder = nodePath.dirname(relativePath);
// remove trailing slashes
folder = folder.replace(/\/+$/g, '');
@@ -304,4 +304,31 @@ export class ActionRunner {
this.actions.setKey(id, { ...actions[id], ...newState });
}
async getFileHistory(filePath: string): Promise<FileHistory | null> {
try {
const webcontainer = await this.#webcontainer;
const historyPath = this.#getHistoryPath(filePath);
const content = await webcontainer.fs.readFile(historyPath, 'utf-8');
return JSON.parse(content);
} catch (error) {
return null;
}
}
async saveFileHistory(filePath: string, history: FileHistory) {
const webcontainer = await this.#webcontainer;
const historyPath = this.#getHistoryPath(filePath);
await this.#runFileAction({
type: 'file',
filePath: historyPath,
content: JSON.stringify(history),
changeSource: 'auto-save'
} as any);
}
#getHistoryPath(filePath: string) {
return nodePath.join('.history', filePath);
}
}

View File

@@ -10,18 +10,16 @@ import { FilesStore, type FileMap } from './files';
import { PreviewsStore } from './previews';
import { TerminalStore } from './terminal';
import JSZip from 'jszip';
import fileSaver from 'file-saver';
import pkg from 'file-saver';
const { saveAs } = pkg;
import { Octokit, type RestEndpointMethodTypes } from '@octokit/rest';
import { path } from '~/utils/path';
import * as nodePath from 'node:path';
import { extractRelativePath } from '~/utils/diff';
import { description } from '~/lib/persistence';
import Cookies from 'js-cookie';
import { createSampler } from '~/utils/sampler';
import type { ActionAlert } from '~/types/actions';
// Destructure saveAs from the CommonJS module
const { saveAs } = fileSaver;
export interface ArtifactState {
id: string;
title: string;
@@ -34,7 +32,7 @@ export type ArtifactUpdateState = Pick<ArtifactState, 'title' | 'closed'>;
type Artifacts = MapStore<Record<string, ArtifactState>>;
export type WorkbenchViewType = 'code' | 'preview';
export type WorkbenchViewType = 'code' | 'diff' | 'preview';
export class WorkbenchStore {
#previewsStore = new PreviewsStore(webcontainer);
@@ -332,7 +330,7 @@ export class WorkbenchStore {
if (data.action.type === 'file') {
const wc = await webcontainer;
const fullPath = path.join(wc.workdir, data.action.filePath);
const fullPath = nodePath.join(wc.workdir, data.action.filePath);
if (this.selectedFile.value !== fullPath) {
this.setSelectedFile(fullPath);
@@ -437,13 +435,7 @@ export class WorkbenchStore {
return syncedFiles;
}
async pushToGitHub(
repoName: string,
commitMessage?: string,
githubUsername?: string,
ghToken?: string,
isPrivate: boolean = false,
) {
async pushToGitHub(repoName: string, commitMessage?: string, githubUsername?: string, ghToken?: string) {
try {
// Use cookies if username and token are not provided
const githubToken = ghToken || Cookies.get('githubToken');
@@ -467,7 +459,7 @@ export class WorkbenchStore {
// Repository doesn't exist, so create a new one
const { data: newRepo } = await octokit.repos.createForAuthenticatedUser({
name: repoName,
private: isPrivate,
private: false,
auto_init: true,
});
repo = newRepo;
@@ -545,7 +537,7 @@ export class WorkbenchStore {
sha: newCommit.sha,
});
return repo.html_url; // Return the URL instead of showing alert
alert(`Repository created and code pushed: ${repo.html_url}`);
} catch (error) {
console.error('Error pushing to GitHub:', error);
throw error; // Rethrow the error for further handling