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
+29 -6
View File
@@ -56,7 +56,7 @@ interface UseChatScrollManagerResult {
handleMessageContentChange: (reason?: ContentChangeReason) => void;
getAnimationHandlers: (messageId: string) => AnimationHandlers;
showScrollButton: boolean;
scrollToBottom: (options?: { instant?: boolean }) => void;
scrollToBottom: (options?: { instant?: boolean; force?: boolean }) => void;
spacerHeight: number;
pendingAnchorId: string | null;
hasActiveAnchor: boolean;
@@ -116,6 +116,7 @@ export const useChatScrollManager = ({
const anchorIdRef = React.useRef<string | null>(null);
const hasAnchoredOnceRef = React.useRef<boolean>(false);
const userScrollOverrideRef = React.useRef<boolean>(false);
const currentPhase = currentSessionId
? sessionActivityPhase?.get(currentSessionId) ?? 'idle'
@@ -238,13 +239,30 @@ export const useChatScrollManager = ({
}
}, [pendingAnchorId]);
const scrollToBottom = React.useCallback((options?: { instant?: boolean }) => {
const scrollToBottom = React.useCallback((options?: { instant?: boolean; force?: boolean }) => {
const container = scrollRef.current;
if (!container) return;
const distanceFromBottom = container.scrollHeight - container.scrollTop - container.clientHeight;
const shouldRespectUserScroll =
userScrollOverrideRef.current &&
currentPhase === 'idle' &&
!isSyncing &&
!options?.force &&
distanceFromBottom > DEFAULT_SCROLL_BUTTON_THRESHOLD;
if (shouldRespectUserScroll) {
return;
}
if (options?.force) {
userScrollOverrideRef.current = false;
}
const bottom = container.scrollHeight - container.clientHeight;
scrollEngine.scrollToPosition(Math.max(0, bottom), options);
}, [scrollEngine]);
}, [currentPhase, isSyncing, scrollEngine]);
const scrollToNewAnchor = React.useCallback((messageId: string) => {
if (lastScrolledAnchorIdRef.current === messageId) {
@@ -294,12 +312,16 @@ export const useChatScrollManager = ({
});
}, [scrollEngine, updateSpacerHeight]);
const handleScrollEvent = React.useCallback(() => {
const handleScrollEvent = React.useCallback((event?: Event) => {
const container = scrollRef.current;
if (!container || !currentSessionId) {
return;
}
if (event?.isTrusted) {
userScrollOverrideRef.current = true;
}
scrollEngine.handleScroll();
updateScrollButtonVisibility();
@@ -328,10 +350,10 @@ export const useChatScrollManager = ({
const container = scrollRef.current;
if (!container) return;
container.addEventListener('scroll', handleScrollEvent, { passive: true });
container.addEventListener('scroll', handleScrollEvent as EventListener, { passive: true });
return () => {
container.removeEventListener('scroll', handleScrollEvent);
container.removeEventListener('scroll', handleScrollEvent as EventListener);
};
}, [handleScrollEvent]);
@@ -376,6 +398,7 @@ export const useChatScrollManager = ({
spacerHeightRef.current = 0;
setSpacerHeight(0);
userScrollOverrideRef.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- only run on session change, not message changes
}, [currentSessionId, sessionMessages.length]);