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
+115
-16
@@ -3426,15 +3426,12 @@ const startGlobalEventWatcher = async () => {
|
||||
|
||||
// Update authoritative session state from OpenCode events
|
||||
if (payload && payload.type === 'session.status') {
|
||||
const status = payload.properties?.status;
|
||||
const sessionId = payload.properties?.sessionID ?? payload.properties?.sessionId;
|
||||
const eventId = payload.properties?.eventId || `sse-${Date.now()}`;
|
||||
|
||||
if (typeof sessionId === 'string' && status?.type) {
|
||||
updateSessionState(sessionId, status.type, eventId, {
|
||||
attempt: status.attempt,
|
||||
message: status.message,
|
||||
next: status.next
|
||||
const update = extractSessionStatusUpdate(payload);
|
||||
if (update) {
|
||||
updateSessionState(update.sessionId, update.type, update.eventId || `sse-${Date.now()}`, {
|
||||
attempt: update.attempt,
|
||||
message: update.message,
|
||||
next: update.next,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3686,6 +3683,84 @@ function parseSseDataPayload(block) {
|
||||
}
|
||||
}
|
||||
|
||||
function extractSessionStatusUpdate(payload) {
|
||||
if (!payload || typeof payload !== 'object' || payload.type !== 'session.status') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const props = payload.properties ?? {};
|
||||
const status =
|
||||
props.status ??
|
||||
props.session?.status ??
|
||||
props.sessionInfo?.status;
|
||||
const metadata =
|
||||
props.metadata ??
|
||||
(typeof status === 'object' && status !== null ? status.metadata : null);
|
||||
|
||||
const sessionId = props.sessionID ?? props.sessionId;
|
||||
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const statusType =
|
||||
typeof status === 'string'
|
||||
? status
|
||||
: typeof status?.type === 'string'
|
||||
? status.type
|
||||
: typeof status?.status === 'string'
|
||||
? status.status
|
||||
: typeof props.type === 'string'
|
||||
? props.type
|
||||
: typeof props.phase === 'string'
|
||||
? props.phase
|
||||
: typeof props.state === 'string'
|
||||
? props.state
|
||||
: null;
|
||||
|
||||
const normalizedType =
|
||||
statusType === 'idle' || statusType === 'busy' || statusType === 'retry'
|
||||
? statusType
|
||||
: null;
|
||||
|
||||
if (!normalizedType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const attempt =
|
||||
typeof status?.attempt === 'number'
|
||||
? status.attempt
|
||||
: typeof props.attempt === 'number'
|
||||
? props.attempt
|
||||
: typeof metadata?.attempt === 'number'
|
||||
? metadata.attempt
|
||||
: undefined;
|
||||
const message =
|
||||
typeof status?.message === 'string'
|
||||
? status.message
|
||||
: typeof props.message === 'string'
|
||||
? props.message
|
||||
: typeof metadata?.message === 'string'
|
||||
? metadata.message
|
||||
: undefined;
|
||||
const next =
|
||||
typeof status?.next === 'number'
|
||||
? status.next
|
||||
: typeof props.next === 'number'
|
||||
? props.next
|
||||
: typeof metadata?.next === 'number'
|
||||
? metadata.next
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
type: normalizedType,
|
||||
attempt,
|
||||
message,
|
||||
next,
|
||||
eventId: typeof props.eventId === 'string' ? props.eventId : null,
|
||||
};
|
||||
}
|
||||
|
||||
function emitDesktopNotification(payload) {
|
||||
if (!ENV_DESKTOP_NOTIFY) {
|
||||
return;
|
||||
@@ -3759,13 +3834,10 @@ function deriveSessionActivityTransitions(payload) {
|
||||
}
|
||||
|
||||
if (payload.type === 'session.status') {
|
||||
const status = payload.properties?.status;
|
||||
const sessionId = payload.properties?.sessionID ?? payload.properties?.sessionId;
|
||||
const statusType = status?.type;
|
||||
|
||||
if (typeof sessionId === 'string' && sessionId.length > 0 && typeof statusType === 'string') {
|
||||
const phase = statusType === 'busy' || statusType === 'retry' ? 'busy' : 'idle';
|
||||
return [{ sessionId, phase }];
|
||||
const update = extractSessionStatusUpdate(payload);
|
||||
if (update) {
|
||||
const phase = update.type === 'busy' || update.type === 'retry' ? 'busy' : 'idle';
|
||||
return [{ sessionId: update.sessionId, phase }];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6059,6 +6131,20 @@ async function main(options = {}) {
|
||||
const payload = parseSseDataPayload(block);
|
||||
// Cache session titles from session.updated/session.created events (global stream)
|
||||
maybeCacheSessionInfoFromEvent(payload);
|
||||
|
||||
// Keep server-authoritative session state fresh even if the
|
||||
// background watcher is disconnected.
|
||||
if (payload && payload.type === 'session.status') {
|
||||
const update = extractSessionStatusUpdate(payload);
|
||||
if (update) {
|
||||
updateSessionState(update.sessionId, update.type, update.eventId || `proxy-${Date.now()}`, {
|
||||
attempt: update.attempt,
|
||||
message: update.message,
|
||||
next: update.next,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const transitions = deriveSessionActivityTransitions(payload);
|
||||
if (transitions && transitions.length > 0) {
|
||||
for (const activity of transitions) {
|
||||
@@ -6187,6 +6273,18 @@ async function main(options = {}) {
|
||||
const payload = parseSseDataPayload(block);
|
||||
// Cache session titles from session.updated/session.created events (per-session stream)
|
||||
maybeCacheSessionInfoFromEvent(payload);
|
||||
|
||||
if (payload && payload.type === 'session.status') {
|
||||
const update = extractSessionStatusUpdate(payload);
|
||||
if (update) {
|
||||
updateSessionState(update.sessionId, update.type, update.eventId || `proxy-${Date.now()}`, {
|
||||
attempt: update.attempt,
|
||||
message: update.message,
|
||||
next: update.next,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const transitions = deriveSessionActivityTransitions(payload);
|
||||
if (transitions && transitions.length > 0) {
|
||||
for (const activity of transitions) {
|
||||
@@ -8902,6 +9000,7 @@ async function main(options = {}) {
|
||||
original: result.original,
|
||||
modified: result.modified,
|
||||
path: result.path,
|
||||
isBinary: Boolean(result.isBinary),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get git file diff:', error);
|
||||
|
||||
Reference in New Issue
Block a user