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
@@ -143,6 +143,7 @@ export interface GitFileDiffResponse {
|
||||
original: string;
|
||||
modified: string;
|
||||
path: string;
|
||||
isBinary?: boolean;
|
||||
}
|
||||
|
||||
export interface GetGitFileDiffOptions {
|
||||
@@ -486,6 +487,8 @@ export interface ProjectEntry {
|
||||
id: string;
|
||||
path: string;
|
||||
label?: string;
|
||||
icon?: string | null;
|
||||
color?: string | null;
|
||||
addedAt?: number;
|
||||
lastOpenedAt?: number;
|
||||
sidebarCollapsed?: boolean;
|
||||
|
||||
@@ -464,6 +464,7 @@ class OpencodeService {
|
||||
'application/toml',
|
||||
'application/x-sh',
|
||||
'application/x-shellscript',
|
||||
'application/octet-stream',
|
||||
];
|
||||
|
||||
return textBasedTypes.includes(lowerMime);
|
||||
@@ -683,21 +684,43 @@ class OpencodeService {
|
||||
throw new Error('Message must have at least one part (text or file)');
|
||||
}
|
||||
|
||||
// Use SDK session.prompt() method
|
||||
// DON'T send messageID - let server generate it (fixes Claude empty response issue)
|
||||
await this.client.session.prompt({
|
||||
sessionID: params.id,
|
||||
...(this.currentDirectory ? { directory: this.currentDirectory } : {}),
|
||||
// messageID intentionally omitted - server will generate
|
||||
model: {
|
||||
providerID: params.providerID,
|
||||
modelID: params.modelID
|
||||
// Use async prompt endpoint so the client doesn't block waiting
|
||||
// for model work (SSE will deliver output/status).
|
||||
// This avoids 504s from proxy timeouts on long-running turns.
|
||||
const base = this.baseUrl.replace(/\/+$/, '');
|
||||
const url = new URL(`${base}/session/${encodeURIComponent(params.id)}/prompt_async`);
|
||||
if (this.currentDirectory) {
|
||||
url.searchParams.set('directory', this.currentDirectory);
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
accept: 'application/json',
|
||||
},
|
||||
agent: params.agent,
|
||||
variant: params.variant,
|
||||
parts
|
||||
body: JSON.stringify({
|
||||
model: {
|
||||
providerID: params.providerID,
|
||||
modelID: params.modelID,
|
||||
},
|
||||
agent: params.agent,
|
||||
variant: params.variant,
|
||||
parts,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let detail = '';
|
||||
try {
|
||||
detail = await response.text();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const suffix = detail && detail.trim().length > 0 ? `: ${detail.trim()}` : '';
|
||||
throw new Error(`Failed to send message (${response.status})${suffix}`);
|
||||
}
|
||||
|
||||
// Return temporary ID for optimistic UI
|
||||
// Real messageID will come from server via SSE events
|
||||
return tempMessageId;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
RiCodeBoxLine,
|
||||
RiTerminalBoxLine,
|
||||
RiRocketLine,
|
||||
RiFlaskLine,
|
||||
RiGamepadLine,
|
||||
RiBriefcaseLine,
|
||||
RiHomeLine,
|
||||
RiGlobalLine,
|
||||
RiLeafLine,
|
||||
RiShieldLine,
|
||||
RiPaletteLine,
|
||||
RiServerLine,
|
||||
RiSmartphoneLine,
|
||||
RiDatabase2Line,
|
||||
RiLightbulbLine,
|
||||
RiMusicLine,
|
||||
RiCameraLine,
|
||||
RiBookOpenLine,
|
||||
RiHeartLine,
|
||||
type RemixiconComponentType,
|
||||
} from '@remixicon/react';
|
||||
|
||||
export const PROJECT_ICONS: Array<{ key: string; Icon: RemixiconComponentType; label: string }> = [
|
||||
{ key: 'code', Icon: RiCodeBoxLine, label: 'Code' },
|
||||
{ key: 'terminal', Icon: RiTerminalBoxLine, label: 'Terminal' },
|
||||
{ key: 'rocket', Icon: RiRocketLine, label: 'Rocket' },
|
||||
{ key: 'flask', Icon: RiFlaskLine, label: 'Lab' },
|
||||
{ key: 'gamepad', Icon: RiGamepadLine, label: 'Game' },
|
||||
{ key: 'briefcase', Icon: RiBriefcaseLine, label: 'Work' },
|
||||
{ key: 'home', Icon: RiHomeLine, label: 'Home' },
|
||||
{ key: 'globe', Icon: RiGlobalLine, label: 'Web' },
|
||||
{ key: 'leaf', Icon: RiLeafLine, label: 'Nature' },
|
||||
{ key: 'shield', Icon: RiShieldLine, label: 'Security' },
|
||||
{ key: 'palette', Icon: RiPaletteLine, label: 'Design' },
|
||||
{ key: 'server', Icon: RiServerLine, label: 'Server' },
|
||||
{ key: 'phone', Icon: RiSmartphoneLine, label: 'Mobile' },
|
||||
{ key: 'database', Icon: RiDatabase2Line, label: 'Data' },
|
||||
{ key: 'lightbulb', Icon: RiLightbulbLine, label: 'Idea' },
|
||||
{ key: 'music', Icon: RiMusicLine, label: 'Music' },
|
||||
{ key: 'camera', Icon: RiCameraLine, label: 'Media' },
|
||||
{ key: 'book', Icon: RiBookOpenLine, label: 'Docs' },
|
||||
{ key: 'heart', Icon: RiHeartLine, label: 'Favorite' },
|
||||
];
|
||||
|
||||
export const PROJECT_ICON_MAP: Record<string, RemixiconComponentType> = Object.fromEntries(
|
||||
PROJECT_ICONS.map((i) => [i.key, i.Icon])
|
||||
);
|
||||
|
||||
export const PROJECT_COLORS: Array<{ key: string; label: string; cssVar: string }> = [
|
||||
{ key: 'keyword', label: 'Purple', cssVar: 'var(--syntax-keyword)' },
|
||||
{ key: 'string', label: 'Green', cssVar: 'var(--syntax-string)' },
|
||||
{ key: 'number', label: 'Pink', cssVar: 'var(--syntax-number)' },
|
||||
{ key: 'type', label: 'Gold', cssVar: 'var(--syntax-type)' },
|
||||
{ key: 'constant', label: 'Cyan', cssVar: 'var(--syntax-constant)' },
|
||||
{ key: 'comment', label: 'Muted', cssVar: 'var(--syntax-comment)' },
|
||||
{ key: 'error', label: 'Red', cssVar: 'var(--status-error)' },
|
||||
{ key: 'primary', label: 'Blue', cssVar: 'var(--primary)' },
|
||||
{ key: 'success', label: 'Green', cssVar: 'var(--status-success)' },
|
||||
];
|
||||
|
||||
export const PROJECT_COLOR_MAP: Record<string, string> = Object.fromEntries(
|
||||
PROJECT_COLORS.map((c) => [c.key, c.cssVar])
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
export const SEMANTIC_TYPOGRAPHY = {
|
||||
markdown: '0.9375rem',
|
||||
code: '0.9063rem',
|
||||
code: '0.8125rem',
|
||||
uiHeader: '0.9375rem',
|
||||
uiLabel: '0.8750rem',
|
||||
meta: '0.875rem',
|
||||
|
||||
Reference in New Issue
Block a user