Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"@paper-design/shaders-react": "^0.0.76",
"@peculiar/asn1-schema": "2.8.0",
"@peculiar/asn1-x509": "2.8.0",
"@pierre/diffs": "1.3.6",
"@qdrant/js-client-rest": "1.17.0",
"@radix-ui/react-accordion": "1.2.12",
"@radix-ui/react-avatar": "1.1.11",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { DropdownMenuItem } from '@/components/ui/dropdown-menu';
import type * as DropdownMenuComponents from '@/components/ui/dropdown-menu';
import type * as DropdownMenuPrimitives from '@radix-ui/react-dropdown-menu';
import { CloudAgentWorkspaceTabs } from './CloudAgentWorkspaceTabs';
import { CHAT_TAB_ID, terminalTabId, type WorkspaceTabId } from './terminal-tabs';
import { CHAT_TAB_ID, fileTabId, terminalTabId, type WorkspaceTabId } from './workspace-tabs';
import { Tabs, TabsContent } from '@/components/ui/tabs';
import type { StoredSession } from './types';

Object.assign(globalThis, { React });
Expand Down Expand Up @@ -75,6 +76,8 @@ function renderWorkspaceTabs(
onCreateChat: () => undefined,
onRenameChat: async () => undefined,
terminals: [],
files: [],
onCloseFile: () => undefined,
terminalStatuses: {},
canCreateTerminal: false,
onSelectTab: () => undefined,
Expand All @@ -83,7 +86,22 @@ function renderWorkspaceTabs(
...overrides,
};

return renderToStaticMarkup(createElement(CloudAgentWorkspaceTabs, props));
const worktreeId =
props.worktreeId === undefined
? props.chatSessions.find(chat => chat.sessionId === props.currentSessionId)?.worktreeId
: props.worktreeId;
const value =
props.activeTabId === CHAT_TAB_ID && worktreeId && props.currentSessionId
? `chat:${props.currentSessionId}`
: props.activeTabId;
return renderToStaticMarkup(
createElement(
Tabs,
{ value },
createElement(CloudAgentWorkspaceTabs, props),
createElement(TabsContent, { value }, 'Selected workspace panel')
)
);
}

function findButtonMarkup(html: string, text: string): string {
Expand Down Expand Up @@ -117,6 +135,23 @@ function getSessionMenuItemProps(title: string): ComponentProps<typeof DropdownM
}

describe('CloudAgentWorkspaceTabs', () => {
it('composes selected file and sibling chat values with their shared Radix panels', () => {
for (const activeTabId of [CHAT_TAB_ID, fileTabId('src/file.ts')] as const) {
const html = renderWorkspaceTabs({
activeTabId,
files: [{ path: 'src/file.ts' }],
worktreeId: 'worktree_shared',
});
const active = (html.match(/<button\b[^>]*>/g) ?? []).filter(
button => button.includes('role="tab"') && button.includes('data-state="active"')
);
expect(active).toHaveLength(1);
const controls = active[0]?.match(/aria-controls="([^"]+)"/)?.[1];
expect(controls).toBeDefined();
expect(html).toContain(`id="${controls}"`);
expect(html).toContain('Selected workspace panel');
}
});
it('renders complete grouped chat titles and selects only the current session tab', () => {
const firstTitle = 'Investigate the complete authentication regression across every provider';
const secondTitle = 'Fix the separate billing synchronization flow';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, type RefObject } from 'react';
import { SessionStatusIndicator } from '@/components/shared/SessionStatusIndicator';
import { TimeAgo } from '@/components/shared/TimeAgo';
import { Button } from '@/components/ui/button';
Expand All @@ -12,14 +12,22 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { ChevronDown, LoaderCircle, MessageSquare, Plus, Terminal, X } from 'lucide-react';
import {
ChevronDown,
FileDiff,
LoaderCircle,
MessageSquare,
Plus,
Terminal,
X,
} from 'lucide-react';
import { toast } from 'sonner';
import { SessionPrIndicator } from './SessionPrIndicator';
import { CHAT_TAB_ID, terminalTabId } from './terminal-tabs';
import type { TerminalWorkspaceTab, WorkspaceTabId } from './terminal-tabs';
import { CHAT_TAB_ID, fileTabId, terminalTabId } from './workspace-tabs';
import type { FileWorkspaceTab, TerminalWorkspaceTab, WorkspaceTabId } from './workspace-tabs';
import type { StoredSession } from './types';

type TerminalStatusSummary = {
Expand Down Expand Up @@ -51,6 +59,7 @@ const renameHint = 'Double-click to rename.';

export function CloudAgentWorkspaceTabs({
activeTabId,
activeTabRef,
chatSessions,
currentSessionId,
worktreeId,
Expand All @@ -63,6 +72,8 @@ export function CloudAgentWorkspaceTabs({
onRenameChat,
deletingSessionIds = [],
terminals,
files,
onCloseFile,
terminalStatuses,
canCreateTerminal,
onSelectTab,
Expand All @@ -71,6 +82,9 @@ export function CloudAgentWorkspaceTabs({
className,
}: {
activeTabId: WorkspaceTabId;
activeTabRef?: RefObject<HTMLButtonElement | null>;
files: FileWorkspaceTab[];
onCloseFile: (path: string) => void;
chatSessions: StoredSession[];
currentSessionId: string | null;
worktreeId?: string | null;
Expand Down Expand Up @@ -99,7 +113,8 @@ export function CloudAgentWorkspaceTabs({
const touchStartRef = useRef<RenameTap | null>(null);
const lastTapRef = useRef<RenameTap | null>(null);
const lastPointerTypeRef = useRef('');
const selectedTabRef = useRef<HTMLButtonElement>(null);
const localSelectedTabRef = useRef<HTMLButtonElement>(null);
const selectedTabRef = activeTabRef ?? localSelectedTabRef;
const activeTabWrapperRef = useRef<HTMLDivElement>(null);
const tabListRef = useRef<HTMLDivElement>(null);
const tabOptionsTriggerRef = useRef<HTMLButtonElement>(null);
Expand Down Expand Up @@ -152,7 +167,7 @@ export function CloudAgentWorkspaceTabs({
resizeObserver.observe(tabList);
if (activeTabWrapperRef.current) resizeObserver.observe(activeTabWrapperRef.current);
return () => resizeObserver.disconnect();
}, [selectedValue, visibleChatSessions.length, terminals.length]);
}, [selectedValue, visibleChatSessions.length, terminals.length, files.length, selectedTabRef]);

const handleStartRename = (session: StoredSession, trigger: HTMLButtonElement) => {
if (
Expand Down Expand Up @@ -203,29 +218,8 @@ export function CloudAgentWorkspaceTabs({
}
};

const handleValueChange = (value: string) => {
if (value === CHAT_TAB_ID) {
onSelectTab(CHAT_TAB_ID);
return;
}

const selectedChat = visibleChatSessions.find(session => value === `chat:${session.sessionId}`);
if (selectedChat) {
if (activeTabId !== CHAT_TAB_ID) onSelectTab(CHAT_TAB_ID);
if (selectedChat.sessionId !== currentSessionId) onSelectChat(selectedChat.sessionId);
return;
}

const selectedTerminal = terminals.find(terminal => value === terminalTabId(terminal.id));
if (selectedTerminal) onSelectTab(terminalTabId(selectedTerminal.id));
};

return (
<Tabs
value={selectedValue}
onValueChange={handleValueChange}
className={cn('flex min-w-0 max-w-full items-center gap-1 overflow-hidden', className)}
>
<div className={cn('flex min-w-0 max-w-full items-center gap-1 overflow-hidden', className)}>
<TabsList
ref={tabListRef}
aria-label="Cloud Agent workspace"
Expand Down Expand Up @@ -471,6 +465,40 @@ export function CloudAgentWorkspaceTabs({
</div>
);
})}
{files.map(tab => {
const tabId = fileTabId(tab.path);
const active = activeTabId === tabId;
const name = tab.path.slice(tab.path.lastIndexOf('/') + 1);
return (
<div
key={tabId}
ref={active ? activeTabWrapperRef : undefined}
className={cn(tabClassName, active && activeTabClassName)}
>
<TabsTrigger
ref={active ? selectedTabRef : undefined}
value={tabId}
title={tab.path}
className={tabTriggerClassName}
>
<FileDiff className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span className="max-w-48 truncate">{name}</span>
</TabsTrigger>
<Button
variant="ghost"
size="icon"
className={tabActionClassName}
aria-label={`Close ${name}`}
onClick={() => {
onCloseFile(tab.path);
requestAnimationFrame(() => selectedTabRef.current?.focus());
}}
>
<X className="h-3.5 w-3.5" aria-hidden="true" />
</Button>
</div>
);
})}
</TabsList>

<div className="flex shrink-0 items-center gap-1 py-1 pr-1">
Expand Down Expand Up @@ -597,6 +625,6 @@ export function CloudAgentWorkspaceTabs({
</Button>
) : null}
</div>
</Tabs>
</div>
);
}
Loading
Loading