feat: vscode extension (#59)

* feat: add initial VS Code extension plan and implementation tasks

* feat(vscode): added initial version of an Openchamber VSCode extension

* feat(vscode): enhance VS Code extension with theme integration and session management

* feat(vscode): implement connection status handling and overlay in VSCode layout

* feat: move extension to secondary sidebar

* chore: upgrade @opencode-ai/sdk to 1.0.150

* vscode: editor bridge, file picker, click-to-open in tool parts

* vscode: layout session lifecycle, theme sync, typography overrides

* ui: compact mode for vscode, model search, autocomplete width fixes

* perf: scroll force flag, raf placeholder, git polling backoff

* ui: tool output styling, markdown code block fix, gitignore

* refactor: update typography handling for VSCode runtime, remove unused styles

* docs: update README with VS Code extension details and add extension image

* docs: update changelog with new features and performance improvements
This commit is contained in:
Bohdan Triapitsyn
2025-12-13 16:34:17 +02:00
committed by GitHub
parent 610ccf4c62
commit bb72c0fb0c
76 changed files with 6097 additions and 296 deletions
@@ -107,9 +107,17 @@ type SessionGroup = {
interface SessionSidebarProps {
mobileVariant?: boolean;
onSessionSelected?: (sessionId: string) => void;
allowReselect?: boolean;
hideDirectoryControls?: boolean;
}
export const SessionSidebar: React.FC<SessionSidebarProps> = ({ mobileVariant = false }) => {
export const SessionSidebar: React.FC<SessionSidebarProps> = ({
mobileVariant = false,
onSessionSelected,
allowReselect = false,
hideDirectoryControls = false,
}) => {
const [editingId, setEditingId] = React.useState<string | null>(null);
const [editTitle, setEditTitle] = React.useState('');
const [copiedSessionId, setCopiedSessionId] = React.useState<string | null>(null);
@@ -336,9 +344,14 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({ mobileVariant =
if (disabled) {
return;
}
if (!allowReselect && sessionId === currentSessionId) {
onSessionSelected?.(sessionId);
return;
}
setCurrentSession(sessionId);
onSessionSelected?.(sessionId);
},
[setCurrentSession],
[allowReselect, currentSessionId, onSessionSelected, setCurrentSession],
);
const handleSaveEdit = React.useCallback(async () => {
@@ -922,48 +935,50 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({ mobileVariant =
mobileVariant ? '' : isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar',
)}
>
<div className="h-14 select-none px-2 flex-shrink-0">
<div className="flex h-full items-center gap-0">
<button
type="button"
onClick={handleOpenDirectoryDialog}
className={cn(
'group flex min-w-0 flex-1 items-center gap-2 rounded-md px-0 py-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
!isDesktopRuntime && 'hover:bg-sidebar/20',
)}
aria-label="Change project directory"
title={directoryTooltip || '/'}
>
<span
className={cn(
'flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground group-hover:text-foreground',
!isDesktopRuntime && 'bg-sidebar/60',
)}
>
<RiFolder6Line className="h-[1.125rem] w-[1.125rem] translate-y-px" />
</span>
<div className="min-w-0 flex-1 overflow-hidden">
<p className="truncate whitespace-nowrap typography-ui font-semibold text-muted-foreground group-hover:text-foreground">
{displayDirectory || '/'}
</p>
</div>
</button>
{isGitRepo ? (
{!hideDirectoryControls && (
<div className="h-14 select-none px-2 flex-shrink-0">
<div className="flex h-full items-center gap-0">
<button
type="button"
onClick={handleOpenWorktreeManager}
onClick={handleOpenDirectoryDialog}
className={cn(
'inline-flex h-10 w-7 flex-shrink-0 items-center justify-center rounded-xl text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
!isDesktopRuntime && 'bg-sidebar/60 hover:bg-sidebar',
'group flex min-w-0 flex-1 items-center gap-2 rounded-md px-0 py-1 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
!isDesktopRuntime && 'hover:bg-sidebar/20',
)}
aria-label="Manage worktrees"
aria-label="Change project directory"
title={directoryTooltip || '/'}
>
<RiGitRepositoryLine className="h-[1.125rem] w-[1.125rem] translate-y-px" />
<span
className={cn(
'flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground group-hover:text-foreground',
!isDesktopRuntime && 'bg-sidebar/60',
)}
>
<RiFolder6Line className="h-[1.125rem] w-[1.125rem] translate-y-px" />
</span>
<div className="min-w-0 flex-1 overflow-hidden">
<p className="truncate whitespace-nowrap typography-ui font-semibold text-muted-foreground group-hover:text-foreground">
{displayDirectory || '/'}
</p>
</div>
</button>
) : null}
{isGitRepo ? (
<button
type="button"
onClick={handleOpenWorktreeManager}
className={cn(
'inline-flex h-10 w-7 flex-shrink-0 items-center justify-center rounded-xl text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50',
!isDesktopRuntime && 'bg-sidebar/60 hover:bg-sidebar',
)}
aria-label="Manage worktrees"
>
<RiGitRepositoryLine className="h-[1.125rem] w-[1.125rem] translate-y-px" />
</button>
) : null}
</div>
</div>
</div>
)}
<ScrollableOverlay
outerClassName="flex-1 min-h-0"
@@ -971,6 +986,16 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({ mobileVariant =
>
{groupedSessions.length === 0 ? (
emptyState
) : hideDirectoryControls && groupedSessions.length === 1 && groupedSessions[0].isMain ? (
<div className="space-y-[0.6rem] py-1">
{groupedSessions[0].sessions.length === 0 ? (
<div className="py-1 text-left typography-micro text-muted-foreground">
No sessions yet.
</div>
) : (
groupedSessions[0].sessions.map((node) => renderSessionNode(node, 0, groupedSessions[0].directory))
)}
</div>
) : (
groupedSessions.map((group) => (
<div key={group.id} className="relative">
@@ -997,30 +1022,32 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({ mobileVariant =
<span className="typography-micro font-medium text-muted-foreground truncate group-hover/header:text-foreground">
{group.label}
</span>
<span
role="button"
tabIndex={0}
aria-disabled={isCreatingSession}
className={cn(
'inline-flex h-5 w-5 items-center justify-center rounded-md text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground',
isCreatingSession && 'opacity-40 cursor-default',
)}
aria-label="Create session in this group"
onClick={(e) => {
if (isCreatingSession) return;
e.stopPropagation();
handleCreateSessionInGroup(group.directory);
}}
onKeyDown={(e) => {
if (isCreatingSession) return;
if (e.key === 'Enter' || e.key === ' ') {
{!hideDirectoryControls && (
<span
role="button"
tabIndex={0}
aria-disabled={isCreatingSession}
className={cn(
'inline-flex h-5 w-5 items-center justify-center rounded-md text-muted-foreground/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 hover:text-foreground',
isCreatingSession && 'opacity-40 cursor-default',
)}
aria-label="Create session in this group"
onClick={(e) => {
if (isCreatingSession) return;
e.stopPropagation();
handleCreateSessionInGroup(group.directory);
}
}}
>
<RiAddLine className="h-4.5 w-4.5" />
</span>
}}
onKeyDown={(e) => {
if (isCreatingSession) return;
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
handleCreateSessionInGroup(group.directory);
}
}}
>
<RiAddLine className="h-4.5 w-4.5" />
</span>
)}
</div>
</button>