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
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { Message, Part } from '@opencode-ai/sdk/v2';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import ChatMessage from './ChatMessage';
|
||||
import { PermissionCard } from './PermissionCard';
|
||||
@@ -10,6 +11,7 @@ import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatScro
|
||||
import { filterSyntheticParts } from '@/lib/messages/synthetic';
|
||||
import { detectTurns, type Turn } from './hooks/useTurnGrouping';
|
||||
import { TurnGroupingProvider, useMessageNeighbors, useTurnGroupingContextForMessage, useTurnGroupingContextStatic, useLastTurnMessageIds } from './contexts/TurnGroupingContext';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
|
||||
interface ChatMessageEntry {
|
||||
info: Message;
|
||||
@@ -211,7 +213,7 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||
onMessageContentChange('permission');
|
||||
}, [permissions, questions, onMessageContentChange]);
|
||||
|
||||
const displayMessages = React.useMemo(() => {
|
||||
const baseDisplayMessages = React.useMemo(() => {
|
||||
const seenIds = new Set<string>();
|
||||
return messages
|
||||
.filter((message) => {
|
||||
@@ -238,6 +240,101 @@ const MessageList: React.FC<MessageListProps> = ({
|
||||
});
|
||||
}, [messages]);
|
||||
|
||||
const activeRetryStatus = useSessionStore(
|
||||
useShallow((state) => {
|
||||
const sessionId = state.currentSessionId;
|
||||
if (!sessionId) return null;
|
||||
const status = state.sessionStatus?.get(sessionId);
|
||||
if (!status || status.type !== 'retry') return null;
|
||||
const rawMessage = typeof status.message === 'string' ? status.message.trim() : '';
|
||||
return {
|
||||
sessionId,
|
||||
message: rawMessage || 'Quota limit reached. Retrying automatically.',
|
||||
confirmedAt: status.confirmedAt,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const displayMessages = React.useMemo(() => {
|
||||
if (!activeRetryStatus) {
|
||||
return baseDisplayMessages;
|
||||
}
|
||||
|
||||
const retryError = {
|
||||
name: 'SessionRetry',
|
||||
message: activeRetryStatus.message,
|
||||
data: { message: activeRetryStatus.message },
|
||||
};
|
||||
|
||||
const resolveRole = (message: ChatMessageEntry): string | null => {
|
||||
const info = message.info as unknown as { clientRole?: string | null | undefined; role?: string | null | undefined };
|
||||
return (typeof info.clientRole === 'string' ? info.clientRole : null)
|
||||
?? (typeof info.role === 'string' ? info.role : null)
|
||||
?? null;
|
||||
};
|
||||
|
||||
let lastUserIndex = -1;
|
||||
for (let index = baseDisplayMessages.length - 1; index >= 0; index -= 1) {
|
||||
if (resolveRole(baseDisplayMessages[index]) === 'user') {
|
||||
lastUserIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastUserIndex < 0) {
|
||||
return baseDisplayMessages;
|
||||
}
|
||||
|
||||
// Prefer attaching retry error to the assistant message in the current turn (if one exists)
|
||||
// to avoid rendering a separate header-only placeholder + error block.
|
||||
let targetAssistantIndex = -1;
|
||||
for (let index = baseDisplayMessages.length - 1; index > lastUserIndex; index -= 1) {
|
||||
if (resolveRole(baseDisplayMessages[index]) === 'assistant') {
|
||||
targetAssistantIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetAssistantIndex >= 0) {
|
||||
const existing = baseDisplayMessages[targetAssistantIndex];
|
||||
const existingInfo = existing.info as unknown as { error?: unknown };
|
||||
if (existingInfo.error) {
|
||||
return baseDisplayMessages;
|
||||
}
|
||||
|
||||
return baseDisplayMessages.map((message, index) => {
|
||||
if (index !== targetAssistantIndex) {
|
||||
return message;
|
||||
}
|
||||
return {
|
||||
...message,
|
||||
info: {
|
||||
...(message.info as unknown as Record<string, unknown>),
|
||||
error: retryError,
|
||||
} as unknown as Message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const eventTime = typeof activeRetryStatus.confirmedAt === 'number' ? activeRetryStatus.confirmedAt : Date.now();
|
||||
const syntheticId = `synthetic_retry_notice_${activeRetryStatus.sessionId}`;
|
||||
const synthetic: ChatMessageEntry = {
|
||||
info: {
|
||||
id: syntheticId,
|
||||
sessionID: activeRetryStatus.sessionId,
|
||||
role: 'assistant',
|
||||
time: { created: eventTime, completed: eventTime },
|
||||
finish: 'stop',
|
||||
error: retryError,
|
||||
} as unknown as Message,
|
||||
parts: [],
|
||||
};
|
||||
|
||||
const next = baseDisplayMessages.slice();
|
||||
next.splice(lastUserIndex + 1, 0, synthetic);
|
||||
return next;
|
||||
}, [activeRetryStatus, baseDisplayMessages]);
|
||||
|
||||
const { turns, ungroupedMessages } = React.useMemo(() => {
|
||||
const groupedTurns = detectTurns(displayMessages);
|
||||
const groupedMessageIds = new Set<string>();
|
||||
|
||||
Reference in New Issue
Block a user