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:
Bohdan Triapitsyn
2026-02-16 14:15:19 +02:00
committed by GitHub
parent 12606b9e53
commit 47c943b487
42 changed files with 4874 additions and 1163 deletions
+27 -78
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { CodeMirrorEditor, type BlockWidgetDef } from '@/components/ui/CodeMirrorEditor';
import { InlineCommentCard, InlineCommentInput } from '@/components/comments';
import { CodeMirrorEditor } from '@/components/ui/CodeMirrorEditor';
import { useFloatingComments } from '@/components/comments/useFloatingComments';
import { PreviewToggleButton } from './PreviewToggleButton';
import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer';
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
@@ -130,6 +130,8 @@ export const PlanView: React.FC = () => {
const [lineSelection, setLineSelection] = React.useState<SelectedLineRange | null>(null);
const [commentText, setCommentText] = React.useState('');
const [editingDraftId, setEditingDraftId] = React.useState<string | null>(null);
const editorViewRef = React.useRef<EditorView | null>(null);
const editorWrapperRef = React.useRef<HTMLDivElement | null>(null);
const MD_VIEWER_MODE_KEY = 'openchamber:plan:md-viewer-mode';
@@ -377,88 +379,33 @@ export const PlanView: React.FC = () => {
};
}, []);
const blockWidgets = React.useMemo(() => {
if (mdViewMode === 'preview') return [];
const planFileLabel = displayPath ? displayPath.split('/').pop() || 'plan' : 'plan';
const planFileDrafts = React.useMemo(() => {
const sessionKey = getSessionKey();
if (!sessionKey) return [];
const sessionDrafts = allDrafts[sessionKey] ?? [];
const fileLabel = displayPath ? displayPath.split('/').pop() || 'plan' : 'plan';
const fileDrafts = sessionDrafts.filter((d) => d.source === 'plan' && d.fileLabel === fileLabel);
return sessionDrafts.filter((d) => d.source === 'plan' && d.fileLabel === planFileLabel);
}, [getSessionKey, allDrafts, planFileLabel]);
const widgets: BlockWidgetDef[] = [];
// Add saved drafts
fileDrafts.forEach((draft) => {
if (draft.id === editingDraftId) {
// Always show edit input (even on mobile)
widgets.push({
afterLine: draft.endLine,
id: `edit-${draft.id}`,
content: (
<InlineCommentInput
fileLabel={fileLabel}
lineRange={{ start: draft.startLine, end: draft.endLine }}
initialText={commentText}
onSave={handleSaveComment}
onCancel={handleCancelComment}
isEditing={true}
/>
),
});
} else {
// Show saved cards on all devices
widgets.push({
afterLine: draft.endLine,
id: `draft-${draft.id}`,
content: (
<InlineCommentCard
draft={draft}
onEdit={() => {
setLineSelection({ start: draft.startLine, end: draft.endLine });
setCommentText(draft.text);
setEditingDraftId(draft.id);
}}
onDelete={() => removeDraft(draft.sessionKey, draft.id)}
/>
),
});
}
});
// Add new comment input if selecting AND not editing an existing draft
if (lineSelection && !editingDraftId && !isDragging) {
widgets.push({
afterLine: lineSelection.end,
id: 'plan-new-comment-input',
content: (
<InlineCommentInput
fileLabel={fileLabel}
lineRange={lineSelection}
initialText={commentText} // Usually empty for new, unless restored?
onSave={handleSaveComment}
onCancel={handleCancelComment}
isEditing={false}
/>
),
});
}
return widgets;
}, [
mdViewMode,
getSessionKey,
allDrafts,
displayPath,
const floatingComments = useFloatingComments({
editorView: editorViewRef.current,
wrapperRef: editorWrapperRef,
fileDrafts: planFileDrafts,
editingDraftId,
lineSelection,
commentText,
handleSaveComment,
handleCancelComment,
removeDraft,
lineSelection,
isDragging,
]);
fileLabel: planFileLabel,
onSaveComment: handleSaveComment,
onCancelComment: handleCancelComment,
onEditDraft: (draft) => {
setLineSelection({ start: draft.startLine, end: draft.endLine });
setCommentText(draft.text);
setEditingDraftId(draft.id);
},
onDeleteDraft: (draft) => removeDraft(draft.sessionKey, draft.id),
});
return (
<div className="relative flex h-full min-h-0 min-w-0 w-full flex-col overflow-hidden bg-background">
@@ -564,7 +511,7 @@ export const PlanView: React.FC = () => {
</ErrorBoundary>
</div>
) : (
<div className="relative h-full">
<div className="relative h-full" ref={editorWrapperRef}>
<CodeMirrorEditor
value={content}
onChange={() => {
@@ -573,13 +520,14 @@ export const PlanView: React.FC = () => {
readOnly={true}
className="h-full [&_.cm-scroller]:pb-[var(--oc-plan-comment-pad)] [&_.cm-scroller]:relative"
extensions={editorExtensions}
onViewReady={(view) => { editorViewRef.current = view; }}
onViewDestroy={() => { editorViewRef.current = null; }}
highlightLines={lineSelection
? {
start: Math.min(lineSelection.start, lineSelection.end),
end: Math.max(lineSelection.start, lineSelection.end),
}
: undefined}
blockWidgets={blockWidgets}
lineNumbersConfig={{
domEventHandlers: {
mousedown: (view, line, event) => {
@@ -633,6 +581,7 @@ export const PlanView: React.FC = () => {
},
}}
/>
{floatingComments}
</div>
)}
</div>