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]);
+2
View File
@@ -16,3 +16,5 @@ export const useRuntimeAPI = <TValue,>(selector: RuntimeAPISelector<TValue>): TV
};
export const useIsDesktopRuntime = (): boolean => useRuntimeAPI((api) => api.runtime.isDesktop);
export const useIsVSCodeRuntime = (): boolean => useRuntimeAPI((api) => api.runtime.isVSCode);
+9 -19
View File
@@ -75,25 +75,15 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => {
error: null,
});
const [mockMode, setMockMode] = useState(shouldMockUpdate);
const [mockState, setMockState] = useState<UpdateState | null>(null);
// Check for mock mode changes (for console toggling)
useEffect(() => {
const interval = setInterval(() => {
const shouldMock = shouldMockUpdate();
if (shouldMock !== mockMode) {
setMockMode(shouldMock);
if (shouldMock) {
const config = getMockConfig();
if (config) {
setMockState(createMockUpdate(config));
}
}
}
}, 500);
return () => clearInterval(interval);
}, [mockMode]);
// Only check mock mode once at startup - no polling
const [mockMode] = useState(shouldMockUpdate);
const [mockState, setMockState] = useState<UpdateState | null>(() => {
if (shouldMockUpdate()) {
const config = getMockConfig();
return config ? createMockUpdate(config) : null;
}
return null;
});
const checkForUpdates = useCallback(async () => {
if (mockMode) {