Skip to content
Merged
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
4 changes: 2 additions & 2 deletions e2e/browser/boot-and-navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ test('app boots on mock and primary navigation works', async ({ page }) => {
await expect(page).toHaveURL(/#\/projects/);
await expect(page.getByRole('heading', { name: '历史' })).toBeVisible();

await goNav(page, '对话');
await goNav(page, '工作区');
await expect(page).toHaveURL(/#\/chat/);
await expect(
page.getByRole('textbox', { name: '消息输入' }).or(page.getByText('还没有可对话的 Agent')),
Expand Down Expand Up @@ -172,7 +172,7 @@ test('page title sits in the top bar; Chat has neither title nor in-app notifica
expect(Math.abs(createBox!.y + createBox!.height / 2 - (leadBox!.y + leadBox!.height / 2))).toBeLessThanOrEqual(8);
expect(Math.abs(leadBox!.y - connectionsTop)).toBeLessThanOrEqual(12);

await goNav(page, '对话');
await goNav(page, '工作区');
await expect(page).toHaveURL(/#\/chat/);
await expect(page.getByRole('button', { name: '通知' })).toHaveCount(0);
await expect(page.getByRole('heading', { level: 1 })).toHaveCount(0);
Expand Down
2 changes: 1 addition & 1 deletion e2e/browser/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export async function addClaudeApiKeyAndSwitch(page: Page): Promise<void> {
}

export async function openChatComposer(page: Page): Promise<void> {
await goNav(page, '对话');
await goNav(page, '工作区');
await expect(page.getByRole('textbox', { name: '消息输入' })).toBeVisible({
timeout: 20_000,
});
Expand Down
3 changes: 2 additions & 1 deletion src/lib/i18n/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ describe('translate / interpolate', () => {
expect(translate('en', 'common.save')).toBe('Save');
expect(translate('zh', 'common.save')).toBe('保存');
const t = createTranslator('en');
expect(t('nav.chat')).toBe('Workspace');
expect(t('nav.routes')).toBe('Routes');
expect(t('routes.page.title')).toBe('Routes');
expect(t('nav.dashboard')).toBe('Dashboard');
Expand All @@ -80,7 +81,7 @@ describe('translate / interpolate', () => {
expect(zhNav('nav.routes')).toBe('路由');
expect(zhNav('routes.page.title')).toBe('路由');
expect(zhNav('nav.settings')).toBe('设置');
expect(zhNav('nav.chat')).toBe('对话');
expect(zhNav('nav.chat')).toBe('工作区');
expect(zhNav('nav.agents')).toBe('Agent');
expect(zhNav('chrome.onboarding.enterDashboard')).toBe('进入总览');
expect(zhNav('chrome.onboarding.skipGuide')).toBe('跳过引导');
Expand Down
2 changes: 1 addition & 1 deletion src/lib/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const en = {
nav: {
workspace: 'Workspace',
manage: 'Manage',
chat: 'Chat',
chat: 'Workspace',
agents: 'Agents',
skills: 'Skills',
mcp: 'MCP',
Expand Down
2 changes: 1 addition & 1 deletion src/lib/i18n/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const zh = {
nav: {
workspace: '工作区',
manage: '管理',
chat: '对话',
chat: '工作区',
agents: 'Agent',
skills: '技能',
mcp: 'MCP',
Expand Down
52 changes: 47 additions & 5 deletions src/pages/chat/ChatSessionRail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { describe, expect, it, vi } from 'vitest';
import { TooltipProvider } from '@/components/ui/tooltip';
import type { Conversation } from '@/lib/types';
import { createTranslator } from '@/lib/i18n';
import { conversationRailHintView, conversationSemanticTitle } from './chat-model';
import {
conversationRailHintView,
conversationSemanticTitle,
type ConversationWorkspaceGroup,
} from './chat-model';
import { ChatSessionRail } from './ChatSessionRail';

vi.mock('@/components/shared/LanguageProvider', async () => {
Expand Down Expand Up @@ -34,12 +38,27 @@ function renderMarkup(node: ReactElement) {
return renderToStaticMarkup(createElement(TooltipProvider, null, node));
}

function workspaceGroup(
items: Conversation[],
partial?: Partial<ConversationWorkspaceGroup>,
): ConversationWorkspaceGroup {
const cwd = items[0]?.cwd ?? null;
return {
key: cwd ? `path:${cwd}` : 'unset',
label: cwd ? 'demo-project' : '未设置工作目录',
cwd,
pathLabel: null,
items,
...partial,
};
}

function rail(partial?: Partial<Parameters<typeof ChatSessionRail>[0]>) {
const item = conversation();
return createElement(ChatSessionRail, {
open: true,
listLoading: false,
groups: [{ key: 'today', label: '今天', items: [item] }],
groups: [workspaceGroup([item])],
conversations: [item],
filteredCount: 1,
query: '',
Expand All @@ -65,7 +84,7 @@ describe('ChatSessionRail titles', () => {
'Use your terminal to write exactly what I asked without clipping the title';
const html = renderMarkup(
rail({
groups: [{ key: 'today', label: '今天', items: [conversation({ title: full })] }],
groups: [workspaceGroup([conversation({ title: full })])],
conversations: [conversation({ title: full })],
firstUserContentById: { c1: full },
}),
Expand Down Expand Up @@ -94,7 +113,7 @@ describe('ChatSessionRail titles', () => {
renderMarkup(
rail({
activeId: active.id,
groups: [{ key: 'today', label: '今天', items: [active, clipped] }],
groups: [workspaceGroup([active, clipped])],
conversations: [active, clipped],
filteredCount: 2,
firstUserContentById,
Expand Down Expand Up @@ -133,7 +152,30 @@ describe('ChatSessionRail titles', () => {
expect(hint.meta).toContain('/workspace/demo-project');
const src = readFileSync(new URL('./ChatSessionRail.tsx', import.meta.url), 'utf8');
expect(src).toContain('conversationRailHintView(');
expect(src).not.toContain('cwdShortName');
expect(src).toContain('data-help="chat-workspace-group-label"');
});

it('groups sessions under a collapsible working-directory header', () => {
const html = renderMarkup(rail());
expect(html).toContain('data-help="chat-workspace-group"');
expect(html).toContain('data-help="chat-workspace-group-label"');
expect(html).toContain('demo-project');
expect(html).toContain('aria-expanded="true"');
expect(html).not.toContain('今天');
const src = readFileSync(new URL('./ChatSessionRail.tsx', import.meta.url), 'utf8');
expect(src).toContain('ChevronDown');
expect(src).toContain('ChevronRight');
});

it('shows the full path when two workspaces share a short name', () => {
const item = conversation();
const html = renderMarkup(
rail({
groups: [workspaceGroup([item], { pathLabel: '/home/alice/demo-project' })],
}),
);
expect(html).toContain('data-help="chat-workspace-group-path"');
expect(html).toContain('/home/alice/demo-project');
});

it('paints 新建对话 with the theme fill', () => {
Expand Down
73 changes: 61 additions & 12 deletions src/pages/chat/ChatSessionRail.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useRef } from 'react';
import { Loader2, PanelLeftClose, Plus, Trash2 } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { ChevronDown, ChevronRight, Loader2, PanelLeftClose, Plus, Trash2 } from 'lucide-react';
import { AgentLogo } from '@/components/shared/AgentLogo';
import { NavResizeHandle } from '@/components/layout/NavResizeHandle';
import { pageRhythm } from '@/components/layout/page-rhythm';
Expand Down Expand Up @@ -28,7 +28,7 @@ import {
conversationRailMarkColor,
conversationRailSelectedFill,
conversationTitle,
type ConversationDayGroup,
type ConversationWorkspaceGroup,
} from './chat-model';

export function ChatSessionRail({
Expand Down Expand Up @@ -56,7 +56,7 @@ export function ChatSessionRail({
}: {
open: boolean;
listLoading: boolean;
groups: ConversationDayGroup[];
groups: ConversationWorkspaceGroup[];
conversations: Conversation[];
filteredCount: number;
query: string;
Expand Down Expand Up @@ -85,6 +85,8 @@ export function ChatSessionRail({
const pending = conversations.find((c) => c.id === deleteConfirmId) ?? null;
const railRef = useRef<HTMLElement>(null);
const searchInputRef = useRef<HTMLInputElement>(null);
const [collapsedKeys, setCollapsedKeys] = useState<Set<string>>(() => new Set());
const searching = Boolean(query.trim());
useEffect(() => {
if (!open || !searchFocusNonce) return;
const timer = window.setTimeout(() => {
Expand All @@ -93,6 +95,17 @@ export function ChatSessionRail({
}, 0);
return () => window.clearTimeout(timer);
}, [open, searchFocusNonce]);
useEffect(() => {
if (!activeId) return;
const key = groups.find((group) => group.items.some((item) => item.id === activeId))?.key;
if (!key) return;
setCollapsedKeys((prev) => {
if (!prev.has(key)) return prev;
const next = new Set(prev);
next.delete(key);
return next;
});
}, [activeId, groups, historyRevealNonce]);
useEffect(() => {
if (!open || !historyRevealNonce) return;
const timer = window.setTimeout(() => {
Expand Down Expand Up @@ -180,12 +193,47 @@ export function ChatSessionRail({
<p className="text-meta text-muted">{t('chat.rail.noMatch')}</p>
</div>
) : (
groups.map((group) => (
<div key={group.key} className="mb-2">
<div className={cn('px-2 pb-1 pt-1.5', pageRhythm.sectionEyebrow)}>
{group.label}
</div>
{group.items.map((c) => {
groups.map((group) => {
const expanded = searching || !collapsedKeys.has(group.key);
return (
<div
key={group.key}
className="mb-2"
data-help="chat-workspace-group"
data-workspace-key={group.key}
>
<Hint label={group.cwd ?? group.label}>
<button
type="button"
className="flex w-full items-center gap-1 px-2 pb-1 pt-1.5 text-left text-meta font-medium text-muted"
aria-expanded={expanded}
onClick={() => {
setCollapsedKeys((prev) => {
const next = new Set(prev);
if (next.has(group.key)) next.delete(group.key);
else next.add(group.key);
return next;
});
}}
>
{expanded ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0" />
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
)}
<span className="min-w-0 flex-1">
<span className="block truncate" data-help="chat-workspace-group-label">
{group.label}
</span>
{group.pathLabel ? (
<span className="block truncate font-normal" data-help="chat-workspace-group-path">
{group.pathLabel}
</span>
) : null}
</span>
</button>
</Hint>
{expanded ? group.items.map((c) => {
const selected = activeId === c.id;
const sending = sendingConversationIds.includes(c.id);
return (
Expand Down Expand Up @@ -255,9 +303,10 @@ export function ChatSessionRail({
</Button>
</div>
);
})}
}) : null}
</div>
))
);
})
)}
</div>
<Dialog open={Boolean(deleteConfirmId)} onOpenChange={(next) => !next && onCancelDelete()}>
Expand Down
3 changes: 2 additions & 1 deletion src/pages/chat/chat-layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ describe('chat layout wiring', () => {
expect(rail).toContain('AgentLogo');
expect(rail).toContain('hint={false}');
expect(rail).not.toContain('conversationAgentLine');
expect(rail).not.toContain('cwdShortName');
expect(rail).toContain('data-help="chat-workspace-group"');
expect(rail).toContain('aria-expanded');
expect(rail).toContain('conversationTitle');
expect(rail).not.toContain('isBlankConversationDraft');
expect(rail).toContain("t('chat.rail.searchPlaceholder')");
Expand Down
Loading
Loading