From 4424004308a5d754d063b463fe148af5a7d2cde6 Mon Sep 17 00:00:00 2001 From: spike Date: Wed, 2 Sep 2026 09:33:19 +0800 Subject: [PATCH 01/11] feat: improve mobile project chat actions Model: gpt-5.4 Amp-Thread-ID: https://ampcode.com/threads/T-01a05fa8-bf70-7328-be77-9fcbf097f344 Co-authored-by: Amp --- locales/en.json | 1 + locales/zh_CN.json | 1 + .../src/components/chat/chat-landing.tsx | 25 +++++ .../components/mobile/mobile-chat-list.tsx | 45 ++++++++- .../components/mobile/mobile-home-screen.tsx | 67 +++++++------- .../mobile/mobile-project-screen.tsx | 68 +++++++------- .../src/stories/MobileChatList.stories.tsx | 2 + .../src/stories/MobileHomeScreen.stories.tsx | 2 + .../mobile-chat-list-project-actions.test.tsx | 92 +++++++++++++++++++ 9 files changed, 229 insertions(+), 74 deletions(-) create mode 100644 packages/components/tests/mobile-chat-list-project-actions.test.tsx diff --git a/locales/en.json b/locales/en.json index 49f6eaa58..fe4ec151b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -378,6 +378,7 @@ "chat.mobileHome.localProjectSearchPlaceholder": "Search projects", "chat.mobileHome.machineSelector": "Machines", "chat.mobileHome.newChatAriaLabel": "New chat", + "chat.mobileHome.newChatInProjectAriaLabel": "New chat in {{projectLabel}}", "chat.mobileHome.onboarding.description": "Download the desktop app to get started.", "chat.mobileHome.onboarding.downloadButton": "Download Lody", "chat.mobileHome.onboarding.title": "Lody runs on your computer", diff --git a/locales/zh_CN.json b/locales/zh_CN.json index 39f27f10b..06e60f18c 100644 --- a/locales/zh_CN.json +++ b/locales/zh_CN.json @@ -378,6 +378,7 @@ "chat.mobileHome.localProjectSearchPlaceholder": "搜索项目", "chat.mobileHome.machineSelector": "机器", "chat.mobileHome.newChatAriaLabel": "新建对话", + "chat.mobileHome.newChatInProjectAriaLabel": "在 {{projectLabel}} 中新建对话", "chat.mobileHome.onboarding.description": "下载桌面客户端并启动后即可开始。", "chat.mobileHome.onboarding.downloadButton": "下载 Lody", "chat.mobileHome.onboarding.title": "Lody 运行在你的电脑上", diff --git a/packages/components/src/components/chat/chat-landing.tsx b/packages/components/src/components/chat/chat-landing.tsx index 3b21a7529..96d917c7a 100644 --- a/packages/components/src/components/chat/chat-landing.tsx +++ b/packages/components/src/components/chat/chat-landing.tsx @@ -5372,6 +5372,26 @@ function WorkspaceChatLanding({ }, [navigate, workspaceSlug] ); + const handleMobileNewChatInProject = useCallback( + (project: { kind: 'local' | 'github'; projectKey: string }) => { + if (project.kind === 'github') { + setSelectedRepo(project.projectKey); + setContextType('github'); + setMobileNewChatOpen(true); + return; + } + + const entry = visibleLocalProjectMap.get(project.projectKey); + if (!entry) return; + handleSelectedLocalProjectChange({ + machineId: entry.machineId, + localProjectId: entry.project.id, + }); + setContextType('local'); + setMobileNewChatOpen(true); + }, + [handleSelectedLocalProjectChange, visibleLocalProjectMap] + ); /* Pull-to-refresh on the mobile home list. Drives a manual catch-up via `runtime.repo.sync()` — the same path the SSE reconnect loop uses, just user-initiated. Swallow errors here so a flaky network @@ -6327,6 +6347,10 @@ function WorkspaceChatLanding({ archiveToggleLabel: t('chat.mobileHome.archiveToggleLabel', '归档'), filterBarToggleLabel: t('chat.mobileHome.filterBarToggleLabel', '过滤器'), newChatAriaLabel: t('chat.mobileHome.newChatAriaLabel', '新建对话'), + newChatInProjectAriaLabel: (projectLabel) => + t('chat.mobileHome.newChatInProjectAriaLabel', '在 {{projectLabel}} 中新建对话', { + projectLabel, + }), searchAriaLabel: t('common.search', '搜索'), clearSearchAriaLabel: t('common.clear', '清空'), /* The home screen otherwise falls back to hard-coded @@ -6408,6 +6432,7 @@ function WorkspaceChatLanding({ onChatTogglePin={handleMobileChatTogglePin} onChatArchive={handleMobileChatArchive} onChatRestore={handleMobileChatRestore} + onNewChatInProject={handleMobileNewChatInProject} onChatPermanentDelete={handleMobileChatPermanentDelete} onSettingsOpen={() => { void navigate({ diff --git a/packages/components/src/components/mobile/mobile-chat-list.tsx b/packages/components/src/components/mobile/mobile-chat-list.tsx index 4e9d8a3b6..77317b356 100644 --- a/packages/components/src/components/mobile/mobile-chat-list.tsx +++ b/packages/components/src/components/mobile/mobile-chat-list.tsx @@ -7,6 +7,7 @@ import { FolderOpen, LockKeyhole, MessageCircle, + SquarePen, } from 'lucide-react'; import { useAtomValue, useSetAtom } from 'jotai'; import { useTranslation } from 'react-i18next'; @@ -333,6 +334,23 @@ function NoProjectBucketLeading() { ); } +function ProjectNewChatButton({ label, onClick }: { label: string; onClick: () => void }) { + return ( + + ); +} + /* Shared selection-state shape that `MobileChatList` builds once at the top and forwards down through `MobileChatListCard` to each `ConversationRow`. Lifting the state up here means "select all" @@ -806,6 +824,8 @@ export function MobileChatList({ privateLabel, privateHelpAriaLabel, onPrivateHelp, + onNewChatInProject, + newChatInProjectAriaLabel, }: { chats: MobileConversationItem[]; groupBy?: MobileChatGroupBy; @@ -844,6 +864,10 @@ export function MobileChatList({ privateLabel?: string; privateHelpAriaLabel?: string; onPrivateHelp?: () => void; + /** Adds a compose action to each local/GitHub project heading while + grouping by project. Chat-only, pinned, and date headings omit it. */ + onNewChatInProject?: (project: { kind: 'local' | 'github'; projectKey: string }) => void; + newChatInProjectAriaLabel?: (projectLabel: string) => string; /** @deprecated Conversation rows are single-line; branch/project meta is no longer shown. Kept for call-site compatibility. */ rowSecondaryField?: 'branch' | 'project'; @@ -1073,6 +1097,25 @@ export function MobileChatList({ labeledItem != null && (labeledItem.kind === 'local' || (labeledItem.kind !== 'github' && !labeledItem.projectAvatarUrl)); + const projectKind = labeledItem?.kind; + const projectKey = labeledItem?.projectKey; + const projectNewChat = + !selectionToolbarActive && + onNewChatInProject && + (projectKind === 'local' || projectKind === 'github') && + projectKey ? ( + onNewChatInProject({ kind: projectKind, projectKey })} + /> + ) : null; + const projectTrailing = + trailing || projectNewChat ? ( +
+ {trailing} + {projectNewChat} +
+ ) : undefined; let groupedHeadingNode: ReactNode; if (id === PINNED_BUCKET_ID || groupBy === 'date' || groupBy === 'none') { @@ -1107,7 +1150,7 @@ export function MobileChatList({ expanded={expanded} onToggle={onToggle} compactTop={compactTop} - trailing={trailing} + trailing={projectTrailing} isPrivate={labeledItem.isPrivateProject} privateLabel={privateLabel} privateHelpAriaLabel={privateHelpAriaLabel} diff --git a/packages/components/src/components/mobile/mobile-home-screen.tsx b/packages/components/src/components/mobile/mobile-home-screen.tsx index 116c1612c..b8a6c2265 100644 --- a/packages/components/src/components/mobile/mobile-home-screen.tsx +++ b/packages/components/src/components/mobile/mobile-home-screen.tsx @@ -355,6 +355,8 @@ export type MobileHomeScreenLabels = { emptySearch?: string; /** ARIA label for the standalone new-conversation chip in the dock. */ newChatAriaLabel?: string; + /** ARIA label for a project heading's compose action. */ + newChatInProjectAriaLabel?: (projectLabel: string) => string; conversationCount?: (count: number) => string; }; @@ -458,6 +460,8 @@ export type MobileHomeScreenProps = { /** Restore (un-archive) a chat from the swipe-to-reveal drawer shown on rows of the *archived* Chat list. */ onChatRestore?: (chatId: string) => void; + /** Starts a new chat with the selected project preconfigured. */ + onNewChatInProject?: (project: { kind: 'local' | 'github'; projectKey: string }) => void; /** Hands a batch of chat ids to the parent for *permanent* deletion. Only invoked from the multi-select toolbar shown in the archive view's selection mode. The list itself owns the selection + @@ -706,6 +710,7 @@ function HeaderSearchInput({ placeholder, ariaLabel, clearAriaLabel, + trailing, className, }: { value: string; @@ -713,10 +718,11 @@ function HeaderSearchInput({ placeholder: string; ariaLabel: string; clearAriaLabel: string; + trailing?: ReactNode; className?: string; }) { return ( - + {trailing} + ); } -/* Chat-list filter toggle — lives on the first group heading's trailing - edge (not next to search). Active tint when the pill bar is open; a - small primary dot marks applied filters while the bar is collapsed. */ +/* Chat-list filter toggle — lives inside the header search field because + search and filters both narrow the visible conversations. A small primary + dot marks applied filters while the pill bar is collapsed. */ function ChatListFilterToggle({ open, hasActiveFilters, @@ -775,13 +782,11 @@ function ChatListFilterToggle({ aria-label={ariaLabel} aria-pressed={open} className={cn( - 'relative inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full border', - 'border-border/50 bg-muted text-muted-foreground', - 'dark:border-white/12 dark:bg-white/10', + 'relative -mr-2 inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground', 'transition-colors active:scale-[0.97]', - 'hover:bg-muted/80 hover:text-foreground dark:hover:bg-white/14', + 'hover:bg-background/60 hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/30', - open && 'border-primary/40 bg-primary/15 text-primary' + open && 'bg-primary/15 text-primary' )} >