From be0a41172a0a1d814babefe31568db71111d0bfa Mon Sep 17 00:00:00 2001 From: NightWatcher314 Date: Fri, 15 May 2026 22:34:59 +0800 Subject: [PATCH] feat(web): add directory quick session action (#624) * fix(codex): support app-server plan mode * fix(codex): broaden plan mode compatibility checks * feat(web): add directory quick session action * fix(web): hide quick session action for unknown directory --- .../SessionList.directory-action.test.tsx | 97 +++++++++++++++++++ web/src/components/SessionList.tsx | 21 +++- web/src/lib/locales/en.ts | 1 + web/src/lib/locales/zh-CN.ts | 1 + web/src/router.tsx | 9 ++ 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 web/src/components/SessionList.directory-action.test.tsx diff --git a/web/src/components/SessionList.directory-action.test.tsx b/web/src/components/SessionList.directory-action.test.tsx new file mode 100644 index 00000000..2b76e432 --- /dev/null +++ b/web/src/components/SessionList.directory-action.test.tsx @@ -0,0 +1,97 @@ +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { ReactNode } from 'react' +import type { SessionSummary } from '@/types/api' +import { I18nProvider } from '@/lib/i18n-context' +import { SessionList } from './SessionList' + +afterEach(() => cleanup()) + +function makeSession(overrides: Partial & { id: string }): SessionSummary { + return { + active: false, + thinking: false, + activeAt: 0, + updatedAt: 0, + metadata: null, + todoProgress: null, + pendingRequestsCount: 0, + model: null, + effort: null, + ...overrides + } +} + +function renderWithProviders(children: ReactNode) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + } + }) + + return render( + + + {children} + + + ) +} + +describe('SessionList directory action', () => { + it('starts a new session with the project machine and directory', () => { + const onNewSessionInDirectory = vi.fn() + const session = makeSession({ + id: 'session-1', + updatedAt: Date.now(), + metadata: { + path: '/home/ubuntu', + machineId: 'machine-1', + name: 'Greeting', + flavor: 'codex', + } + }) + + renderWithProviders( + + ) + + fireEvent.click(screen.getByRole('button', { name: 'New session in this directory' })) + + expect(onNewSessionInDirectory).toHaveBeenCalledWith({ + machineId: 'machine-1', + directory: '/home/ubuntu', + }) + }) + + it('hides the directory action for sessions without path metadata', () => { + renderWithProviders( + + ) + + expect(screen.queryByRole('button', { name: 'New session in this directory' })).toBeNull() + }) +}) diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index 66644fea..0c64b565 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -687,6 +687,7 @@ export function SessionList(props: { sessions: SessionSummary[] onSelect: (sessionId: string) => void onNewSession: () => void + onNewSessionInDirectory?: (args: { machineId: string | null; directory: string }) => void onBrowse?: () => void onRefresh: () => void isLoading: boolean @@ -696,7 +697,7 @@ export function SessionList(props: { selectedSessionId?: string | null }) { const { t } = useTranslation() - const { renderHeader = true, api, selectedSessionId, machineLabelsById = {} } = props + const { renderHeader = true, api, selectedSessionId, machineLabelsById = {}, onNewSessionInDirectory } = props const [searchQuery, setSearchQuery] = useState('') const normalizedQuery = normalizeSearch(searchQuery) const isSearching = normalizedQuery.length > 0 @@ -905,6 +906,7 @@ export function SessionList(props: { const visibleGroupSessions = getVisibleGroupSessions(group) const hiddenSessionCount = group.sessions.length - visibleGroupSessions.length const sessionGroupExpanded = isSessionGroupExpanded(group) + const canStartInGroupDirectory = group.directory !== 'Other' return (
+ {onNewSessionInDirectory && canStartInGroupDirectory ? ( + + ) : null} ({group.sessions.length}) diff --git a/web/src/lib/locales/en.ts b/web/src/lib/locales/en.ts index 79163f2e..deba651a 100644 --- a/web/src/lib/locales/en.ts +++ b/web/src/lib/locales/en.ts @@ -52,6 +52,7 @@ export default { 'sessions.search.noResults': 'No sessions match your search.', 'sessions.group.showMore': 'Show {n} more', 'sessions.group.showLess': 'Show less', + 'sessions.group.new': 'New session in this directory', // Session list 'session.item.path': 'path', diff --git a/web/src/lib/locales/zh-CN.ts b/web/src/lib/locales/zh-CN.ts index 0f67e4d6..82fc8c03 100644 --- a/web/src/lib/locales/zh-CN.ts +++ b/web/src/lib/locales/zh-CN.ts @@ -52,6 +52,7 @@ export default { 'sessions.search.noResults': '没有匹配的会话。', 'sessions.group.showMore': '再显示 {n} 个', 'sessions.group.showLess': '收起', + 'sessions.group.new': '在此目录新建会话', // Session list 'session.item.path': '路径', diff --git a/web/src/router.tsx b/web/src/router.tsx index 58e68e57..28e4f729 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -150,6 +150,14 @@ function SessionsPage() { const selectedSessionId = sessionMatch && sessionMatch.sessionId !== 'new' ? sessionMatch.sessionId : null const isSessionsIndex = pathname === '/sessions' || pathname === '/sessions/' const sidebar = useSidebarResize() + const handleNewSessionInDirectory = useCallback((args: { machineId: string | null; directory: string }) => { + navigate({ + to: '/sessions/new', + search: args.machineId + ? { directory: args.directory, machineId: args.machineId } + : { directory: args.directory } + }) + }, [navigate]) return (
@@ -205,6 +213,7 @@ function SessionsPage() { params: { sessionId }, })} onNewSession={() => navigate({ to: '/sessions/new' })} + onNewSessionInDirectory={handleNewSessionInDirectory} onBrowse={() => navigate({ to: '/browse' })} onRefresh={handleRefresh} isLoading={isLoading}