feat(ui): redesign workspace shell with context panel, tabbed sidebars, and faster diff UX (#433)
* feat: tabbed right sidebar, context panel, floating diff comments * fix: auto-close left sidebar when context panel opens - Increase default context panel width from 520 to 600 pixels - Increase sidebar minimum width from 200 to 300 pixels - Replace collapsible component with custom button in diff view * refactoring: rework sidebars, tabs, and file tree layout - Rewrite AnimatedTabs as segment-style with sliding indicator - Upgrade SidebarFilesTree to match FilesView features (context menus, git status, file icons, CRUD dialogs, fuzzy search ranking) - Restructure FilesView header: tabs row + actions row, remove breadcrumbs - Show relative path in context panel header, track active tab - Allow left sidebar to stay open alongside context panel - Hide diff/files tabs from header on desktop (mobile-only) - Move chevron after group name in session sidebar - Compact tab heights in right sidebar and git view - Size PreviewToggleButton to match other action buttons - Remove directory loading spinner from folder icons * feat: add project icon and color customization - Enable users to assign custom icons to projects - Allow users to choose accent colors for projects - Stabilize repo status UI during project switching * feat: add scroll fade indicators to editor tabs * style: reduce spacing and icon sizes in header * style: adjust tab component padding from uniform to vertical-horizontal * feat: Add session state indicators to project tabs * feat: Enhance session status handling and improve UI responsiveness * fix: preserve upstream tracking on branch rename * fix: improve initial remote selection for pull requests - Uses saved remote name from previous session when available - Selects remote based on tracking branch when possible - Falls back to origin or first available remote * perf(diff): faster highlight, stable stacked scroll - split/unified Pierre worker pools; prefer shiki-wasm - align diff CSS line-height; disable scroll anchoring; drop WebKit compositing hacks - harden stacked pin/align (cancel on user scroll/input); prevent overscroll - make overlay scrollbar MutationObserver optional; disable for diff container * feat: handle binary files in diff view * fix: adjust project tabs layout and drag regions * style: update drag overlay visual styling * feat: enable number keys to switch projects in the sidebar * fix: recognize octet-stream as text-based MIME type * feat: add keyboard navigation to context panel * feat: add session pinning to sidebar - Pin important sessions to keep them at the top - Pinned sessions persist across browser sessions * refactor: move context usage display from chat input to header
This commit is contained in:
committed by
GitHub
parent
12606b9e53
commit
47c943b487
@@ -1316,6 +1316,8 @@ export async function getRangeFiles(directory, { base, head } = {}) {
|
||||
|
||||
const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp', 'avif'];
|
||||
|
||||
const BINARY_SNIFF_BYTES = 8192;
|
||||
|
||||
function isImageFile(filePath) {
|
||||
const ext = filePath.split('.').pop()?.toLowerCase();
|
||||
return IMAGE_EXTENSIONS.includes(ext || '');
|
||||
@@ -1337,6 +1339,67 @@ function getImageMimeType(filePath) {
|
||||
return mimeMap[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
const parseIsBinaryFromNumstat = (raw) => {
|
||||
const text = String(raw || '').trim();
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Expected format: <added>\t<deleted>\t<path>
|
||||
const firstLine = text.split('\n').map((line) => line.trim()).find(Boolean) || '';
|
||||
const [added, deleted] = firstLine.split('\t');
|
||||
return added === '-' || deleted === '-';
|
||||
};
|
||||
|
||||
const looksBinaryBySniff = async (absolutePath) => {
|
||||
try {
|
||||
const handle = await fsp.open(absolutePath, 'r');
|
||||
try {
|
||||
const buffer = Buffer.alloc(BINARY_SNIFF_BYTES);
|
||||
const { bytesRead } = await handle.read(buffer, 0, BINARY_SNIFF_BYTES, 0);
|
||||
if (bytesRead <= 0) {
|
||||
return false;
|
||||
}
|
||||
return buffer.subarray(0, bytesRead).includes(0);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const isBinaryDiff = async (directoryPath, filePath, staged) => {
|
||||
// Fast path: ask git for numstat. For binary, it returns "-\t-\t<path>".
|
||||
const args = ['diff', '--numstat'];
|
||||
if (staged) {
|
||||
args.push('--cached');
|
||||
}
|
||||
args.push('--', filePath);
|
||||
|
||||
const result = await runGitCommand(directoryPath, args);
|
||||
if (parseIsBinaryFromNumstat(result.stdout)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback for untracked files (diff output is empty): use --no-index against /dev/null
|
||||
if (!staged) {
|
||||
const tracked = await runGitCommand(directoryPath, ['ls-files', '--error-unmatch', '--', filePath]).then((r) => r.success);
|
||||
if (!tracked) {
|
||||
const noIndex = await runGitCommand(directoryPath, ['diff', '--no-index', '--numstat', '--', '/dev/null', filePath]);
|
||||
if (parseIsBinaryFromNumstat(noIndex.stdout) || parseIsBinaryFromNumstat(noIndex.stderr) || parseIsBinaryFromNumstat(noIndex.message)) {
|
||||
return true;
|
||||
}
|
||||
const text = `${noIndex.stdout || ''}\n${noIndex.stderr || ''}\n${noIndex.message || ''}`.toLowerCase();
|
||||
if (text.includes('binary files') || text.includes('git binary patch')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export async function getFileDiff(directory, { path: filePath, staged = false } = {}) {
|
||||
if (!directory || !filePath) {
|
||||
throw new Error('directory and path are required for getFileDiff');
|
||||
@@ -1347,6 +1410,20 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
|
||||
const isImage = isImageFile(filePath);
|
||||
const mimeType = isImage ? getImageMimeType(filePath) : null;
|
||||
|
||||
if (!isImage) {
|
||||
const absolutePath = path.join(directoryPath, filePath);
|
||||
const isBinaryBySniff = await looksBinaryBySniff(absolutePath);
|
||||
const isBinary = isBinaryBySniff || (await isBinaryDiff(directoryPath, filePath, staged));
|
||||
if (isBinary) {
|
||||
return {
|
||||
original: '',
|
||||
modified: '',
|
||||
path: filePath,
|
||||
isBinary: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let original = '';
|
||||
try {
|
||||
if (isImage) {
|
||||
@@ -1396,6 +1473,7 @@ export async function getFileDiff(directory, { path: filePath, staged = false }
|
||||
original,
|
||||
modified,
|
||||
path: filePath,
|
||||
isBinary: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2381,8 +2459,42 @@ export async function renameBranch(directory, oldName, newName) {
|
||||
const git = await createGit(directory);
|
||||
|
||||
try {
|
||||
const normalizedOldName = cleanBranchName(String(oldName || '').trim());
|
||||
const normalizedNewName = cleanBranchName(String(newName || '').trim());
|
||||
|
||||
const previousRemote = await git
|
||||
.raw(['config', '--get', `branch.${normalizedOldName}.remote`])
|
||||
.then((value) => String(value || '').trim())
|
||||
.catch(() => '');
|
||||
const previousMerge = await git
|
||||
.raw(['config', '--get', `branch.${normalizedOldName}.merge`])
|
||||
.then((value) => String(value || '').trim())
|
||||
.catch(() => '');
|
||||
|
||||
// Use git branch -m command to rename the branch
|
||||
await git.raw(['branch', '-m', oldName, newName]);
|
||||
|
||||
if (previousRemote && previousMerge && normalizedNewName) {
|
||||
const previousMergeBranch = cleanBranchName(previousMerge);
|
||||
const nextMergeBranch =
|
||||
previousMergeBranch === normalizedOldName
|
||||
? normalizedNewName
|
||||
: previousMergeBranch;
|
||||
const upstream = normalizeUpstreamTarget(previousRemote, nextMergeBranch);
|
||||
|
||||
if (upstream) {
|
||||
try {
|
||||
await runGitCommandOrThrow(
|
||||
directory,
|
||||
['branch', `--set-upstream-to=${upstream.full}`, normalizedNewName],
|
||||
`Failed to set upstream to ${upstream.full}`
|
||||
);
|
||||
} catch {
|
||||
await setBranchTrackingFallback(directory, normalizedNewName, upstream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, branch: newName };
|
||||
} catch (error) {
|
||||
console.error('Failed to rename branch:', error);
|
||||
|
||||
Reference in New Issue
Block a user