feat: improve VS Code editor chat actions

Targets active editor chat before falling back to sidebar
Adds session open-in-editor action in the VS Code sidebar
Uses attachment badges for VS Code file and selection context
This commit is contained in:
Bohdan Triapitsyn
2026-05-19 15:53:54 +03:00
parent 25037ccab9
commit c81441ecfc
13 changed files with 389 additions and 30 deletions
@@ -187,6 +187,19 @@ export const VSCodeLayout: React.FC = () => {
void vscodeApi.executeCommand('openchamber.setActiveSession', currentSessionId, activeSessionTitle);
}, [activeSessionTitle, currentSessionId, runtimeApis.vscode]);
React.useEffect(() => {
if (viewMode !== 'editor' || !currentSessionId || !activeSessionTitle) {
return;
}
const vscodeApi = runtimeApis.vscode;
if (!vscodeApi) {
return;
}
void vscodeApi.executeCommand('openchamber.updateSessionEditorTitle', currentSessionId, activeSessionTitle);
}, [activeSessionTitle, currentSessionId, runtimeApis.vscode, viewMode]);
// If the active session disappears (e.g., deleted), go back to sessions list
React.useEffect(() => {
if (viewMode === 'editor') {
@@ -709,7 +722,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
<button
type="button"
aria-label={t('sessions.switcher.openAria')}
className="flex min-w-0 flex-1 items-center rounded-md px-1 py-0.5 -my-0.5 text-left transition-colors hover:bg-interactive-hover/60 focus-visible:outline-none focus-visible:bg-interactive-hover/60"
className="inline-flex min-w-0 max-w-full items-center rounded-md px-1 py-0.5 -my-0.5 text-left transition-colors hover:bg-interactive-hover/60 focus-visible:outline-none focus-visible:bg-interactive-hover/60"
>
<span className="text-sm font-medium truncate" title={title}>{title}</span>
</button>
@@ -717,6 +730,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
) : (
<h1 className="text-sm font-medium truncate flex-1" title={title}>{title}</h1>
)}
<div className="min-w-0 flex-1" />
{onNewSession && (
<button
onClick={onNewSession}
@@ -32,6 +32,7 @@ import { useI18n } from '@/lib/i18n';
import { parseMultiRunSessionTitle } from '@/lib/multirun/title';
import { MultiRunFusionDialog } from '@/components/multirun/MultiRunFusionDialog';
import { FusionIcon } from '@/components/icons/FusionIcon';
import { RuntimeAPIContext } from '@/contexts/runtimeAPIContext';
type Folder = { id: string; name: string; sessionIds: string[] };
@@ -261,19 +262,25 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
const isMinimalMode = displayMode === 'minimal';
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
const isElectron = React.useMemo(() => canUseElectronDesktopIPC(), []);
const runtimeApis = React.useContext(RuntimeAPIContext);
const revealOnHoverClass = isVSCode
? 'group-hover:opacity-100 group-hover:pointer-events-auto'
: 'group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto';
const hideOnHoverClass = isVSCode
? 'group-hover:opacity-0'
: 'group-hover:opacity-0 group-focus-within:opacity-0';
const showOpenInEditorAction = isVSCode;
const showQuickArchiveAction = !archivedBucket && !mobileVariant;
const revealPaddingClass = isMinimalMode
? (isVSCode
? 'group-hover:pr-2'
: 'group-hover:pr-2 group-focus-within:pr-2')
: (isVSCode
? (showQuickArchiveAction ? 'group-hover:pr-12' : 'group-hover:pr-5')
? (showQuickArchiveAction && showOpenInEditorAction
? 'group-hover:pr-18'
: showQuickArchiveAction || showOpenInEditorAction
? 'group-hover:pr-12'
: 'group-hover:pr-5')
: (showQuickArchiveAction ? 'group-hover:pr-12 group-focus-within:pr-12' : 'group-hover:pr-5 group-focus-within:pr-5'));
const alwaysActionPaddingClass = showQuickArchiveAction ? 'pr-13' : 'pr-7';
const suppressNextSelectRef = React.useRef(false);
@@ -609,6 +616,22 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
handleDeleteSession(session, { archivedBucket });
};
const handleOpenInEditorPointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
};
const handleOpenInEditorMouseDown = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
};
const handleOpenInEditorClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
void runtimeApis?.vscode?.executeCommand('openchamber.openSessionInEditor', session.id, sessionTitle);
};
const handleRowSelect = (event?: React.MouseEvent<HTMLButtonElement>) => {
if (suppressNextSelectRef.current) {
suppressNextSelectRef.current = false;
@@ -936,6 +959,29 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
</TooltipContent>
</Tooltip>
) : null}
{showOpenInEditorAction ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className={cn(
'inline-flex items-center justify-center rounded-md text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 transition-opacity',
isMinimalMode && !alwaysShowActions ? 'h-4 w-4' : 'h-6 w-6',
)}
aria-label={t('sessions.sidebar.session.actions.openInEditor')}
onPointerDown={handleOpenInEditorPointerDown}
onMouseDown={handleOpenInEditorMouseDown}
onClick={handleOpenInEditorClick}
onKeyDown={(event) => event.stopPropagation()}
>
<Icon name="external-link" className={cn(isMinimalMode && !alwaysShowActions ? 'h-2.5 w-2.5' : 'h-3.5 w-3.5')} />
</button>
</TooltipTrigger>
<TooltipContent side="left" sideOffset={8}>
{t('sessions.sidebar.session.actions.openInEditor')}
</TooltipContent>
</Tooltip>
) : null}
<DropdownMenu open={isMenuOpen} onOpenChange={handleMenuOpenChange}>
<DropdownMenuTrigger asChild>
<button