feat(mobile): list managed Chats in the sessions sheet
Chat sessions without a project were dropped from the mobile session tree because no registered project owned their directory. The sheet now partitions sessions like the desktop sidebar and shows Chats as a collapsible section above the project tree, with the same rows, swipe actions and paging; search results label them "Chats" instead of the raw directory name. Claude-Session: https://claude.ai/code/session_017TK5JAYDfT3Fotc23UEg98
This commit is contained in:
@@ -41,6 +41,8 @@ import { ScrollShadow } from '@/components/ui/ScrollShadow';
|
||||
import { toast } from '@/components/ui';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { getProjectLabel, normalizePath } from './mobilePaths';
|
||||
import { CHAT_DRAFT_PROJECT_ID, isChatDirectoryPath } from '@/lib/chatDirectories';
|
||||
import { partitionSidebarSessions } from '@/components/session/sidebar/list/sessionCollection';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { matchesRankQuery, rankByQuery } from '@/lib/search/fuzzySearch';
|
||||
@@ -1022,6 +1024,27 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
return merged.filter((session) => !session.time?.archived);
|
||||
}, [globalActiveSessions, liveSessions]);
|
||||
|
||||
// Managed Chats (sessions under ~/.config/openchamber/chats) are not owned
|
||||
// by any registered project; they get their own section above the project
|
||||
// tree, the same split the desktop sidebar makes. Temporary /btw forks are
|
||||
// dropped here as well.
|
||||
const { projectSessions, chatSessions } = React.useMemo(
|
||||
() => partitionSidebarSessions(sessions, false),
|
||||
[sessions],
|
||||
);
|
||||
const chatsBucket = React.useMemo<WorktreeBucket>(() => ({
|
||||
key: CHAT_DRAFT_PROJECT_ID,
|
||||
label: '',
|
||||
path: '',
|
||||
worktree: null,
|
||||
sessions: orderSessionsByLifecycleScopes(chatSessions, pinnedSessionIds, sessionOrderRanks),
|
||||
}), [chatSessions, pinnedSessionIds, sessionOrderRanks]);
|
||||
const chatsBucketKey = `${CHAT_DRAFT_PROJECT_ID}::${CHAT_DRAFT_PROJECT_ID}`;
|
||||
const chatRootCount = React.useMemo(
|
||||
() => chatSessions.filter((session) => !getParentId(session)).length,
|
||||
[chatSessions],
|
||||
);
|
||||
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
|
||||
// On open, bring the current session (or at least its project) into view —
|
||||
@@ -1070,7 +1093,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
for (const worktree of node.project.worktrees) ensureBucket(node, worktree.path, worktree);
|
||||
}
|
||||
|
||||
for (const session of sessions) {
|
||||
for (const session of projectSessions) {
|
||||
const directory = getSessionDirectory(session);
|
||||
if (!directory) continue;
|
||||
const normalizedDirectory = normalizePath(directory);
|
||||
@@ -1093,7 +1116,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}, [activeProjectId, pinnedSessionIds, projectsMeta, sessionOrderRanks, sessions]);
|
||||
}, [activeProjectId, pinnedSessionIds, projectSessions, projectsMeta, sessionOrderRanks]);
|
||||
|
||||
const normalizedDirectory = normalizePath(currentDirectory);
|
||||
|
||||
@@ -1149,8 +1172,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
// Paginated, tree-aware list of a bucket's sessions: top-level sessions paginate,
|
||||
// and a parent with subsessions can be expanded to reveal its children (nested,
|
||||
// recursively). Pagination counts only top-level sessions.
|
||||
const renderBucketSessions = (node: ProjectNode, bucket: WorktreeBucket, indent: number) => {
|
||||
const bucketKey = `${node.project.id}::${bucket.key}`;
|
||||
const renderBucketSessions = (bucketKey: string, bucket: WorktreeBucket, indent: number) => {
|
||||
|
||||
// Group children by parent within this bucket, and treat sessions whose parent
|
||||
// is not in this bucket as top-level so nothing is hidden.
|
||||
@@ -1336,13 +1358,14 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
const buildSessionContextLabel = React.useCallback(
|
||||
(session: Session): string => {
|
||||
const directory = getSessionDirectory(session);
|
||||
if (isChatDirectoryPath(directory)) return t('mobile.sessions.section.chats');
|
||||
const project = findExactProjectMatch(projectsMeta, directory);
|
||||
if (!project) return getProjectLabel(directory) || directory;
|
||||
const matchedWorktree = findExactWorktreeMatch(project, normalizePath(directory));
|
||||
if (matchedWorktree?.branch) return `${project.label} · ${matchedWorktree.branch}`;
|
||||
return project.label;
|
||||
},
|
||||
[projectsMeta],
|
||||
[projectsMeta, t],
|
||||
);
|
||||
|
||||
const handleSelectProject = (project: ProjectMeta) => {
|
||||
@@ -1481,7 +1504,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{projectsMeta.length === 0 ? (
|
||||
{projectsMeta.length === 0 && chatSessions.length === 0 ? (
|
||||
<MobileSessionsEmpty
|
||||
title={t('mobile.sessions.empty.noProjectsTitle')}
|
||||
description={t('mobile.sessions.empty.noProjectsDescription')}
|
||||
@@ -1601,7 +1624,56 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col">
|
||||
{orderedNodes.map((node, nodeIndex) => {
|
||||
{(() => {
|
||||
const chatsExpanded = projectExpandedMap[CHAT_DRAFT_PROJECT_ID] ?? true;
|
||||
const chatsLabel = t('mobile.sessions.section.chats');
|
||||
return (
|
||||
<section>
|
||||
<div className="flex min-h-12 w-full items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-h-12 min-w-0 flex-1 items-center gap-2 px-3 py-1.5 text-left transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset"
|
||||
onClick={() => {
|
||||
if (revealedRowId) {
|
||||
handleRowKeyRevealedChange(revealedRowId, false);
|
||||
return;
|
||||
}
|
||||
toggleProject(CHAT_DRAFT_PROJECT_ID, chatsExpanded);
|
||||
}}
|
||||
aria-expanded={chatsExpanded}
|
||||
aria-label={
|
||||
chatsExpanded
|
||||
? t('sessions.sidebar.group.collapseAria', { label: chatsLabel })
|
||||
: t('sessions.sidebar.group.expandAria', { label: chatsLabel })
|
||||
}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-[var(--surface-muted)] text-muted-foreground">
|
||||
<Icon name="chat-4" className="size-4" />
|
||||
</span>
|
||||
<span className="block min-w-0 flex-1 truncate typography-ui-label font-semibold text-foreground">
|
||||
{chatsLabel}
|
||||
</span>
|
||||
<span className="shrink-0 typography-micro text-muted-foreground tabular-nums">
|
||||
{chatRootCount}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{chatsExpanded ? (
|
||||
<div className="pb-2">
|
||||
{chatsBucket.sessions.length > 0 ? (
|
||||
renderBucketSessions(chatsBucketKey, chatsBucket, PROJECT_SESSION_INDENT)
|
||||
) : (
|
||||
<p className="px-3 pb-1 typography-micro text-muted-foreground" style={{ paddingLeft: PROJECT_SESSION_INDENT }}>
|
||||
{t('sessions.sidebar.activity.chatsEmpty')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
})()}
|
||||
{orderedNodes.map((node) => {
|
||||
const projectExpanded = isProjectExpanded(node);
|
||||
const buckets = normalizedQuery
|
||||
? node.buckets.filter((bucket) =>
|
||||
@@ -1614,7 +1686,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
return (
|
||||
<section
|
||||
key={node.project.id}
|
||||
className={cn(nodeIndex > 0 && 'border-t border-border/70')}
|
||||
className="border-t border-border/70"
|
||||
>
|
||||
<MobileSwipeActionsRow
|
||||
actionsWidth={96}
|
||||
@@ -1712,7 +1784,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
return (
|
||||
<>
|
||||
{rootBucket && rootBucket.sessions.length > 0
|
||||
? renderBucketSessions(node, rootBucket, PROJECT_SESSION_INDENT)
|
||||
? renderBucketSessions(`${node.project.id}::${rootBucket.key}`, rootBucket, PROJECT_SESSION_INDENT)
|
||||
: null}
|
||||
{worktreeBuckets.map((bucket) => {
|
||||
const worktreeExpanded = isWorktreeExpanded(node, bucket);
|
||||
@@ -1787,7 +1859,7 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
</button>
|
||||
</MobileSwipeActionsRow>
|
||||
{worktreeExpanded
|
||||
? renderBucketSessions(node, bucket, PROJECT_SESSION_INDENT)
|
||||
? renderBucketSessions(`${node.project.id}::${bucket.key}`, bucket, PROJECT_SESSION_INDENT)
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -42,9 +42,12 @@ existing data; it is never treated as an authoritative empty list.
|
||||
|
||||
Web and desktop show managed Chats before optional Recent activity. Chats use
|
||||
their shared managed root for folders and never expose worktree actions. Project
|
||||
display can be all projects or one selected project. VS Code excludes worktrees
|
||||
and managed Chats, while retaining its workspace-scoped grouped list and inline
|
||||
archived buckets.
|
||||
display can be all projects or one selected project. The mobile sessions sheet
|
||||
(`apps/MobileSessionsSheet.tsx`) partitions the same way through
|
||||
`partitionSidebarSessions` and lists Chats as a collapsible section above the
|
||||
project tree, with no Recent projection. VS Code excludes worktrees and managed
|
||||
Chats, while retaining its workspace-scoped grouped list and inline archived
|
||||
buckets.
|
||||
|
||||
Directory demand always includes known project roots and worktrees. Visibility
|
||||
only changes priority. Row mounts must not start bootstrap work. Selection and
|
||||
|
||||
@@ -103,6 +103,7 @@ export const dict = {
|
||||
'mobile.sessions.section.worktrees': 'Worktrees',
|
||||
'mobile.sessions.section.otherProjects': 'Projekt wechseln',
|
||||
'mobile.sessions.section.projects': 'Projekte',
|
||||
'mobile.sessions.section.chats': 'Chats',
|
||||
'mobile.sessions.empty.noProjectsTitle': 'Noch keine Projekte',
|
||||
'mobile.sessions.empty.noProjectsDescription': 'Füge ein Projekt hinzu, um mit deinem Code zu chatten.',
|
||||
'mobile.sessions.empty.noSessionsTitle': 'Noch keine Sitzungen',
|
||||
|
||||
@@ -130,6 +130,7 @@ export const dict = {
|
||||
'mobile.sessions.section.worktrees': 'Worktrees',
|
||||
'mobile.sessions.section.otherProjects': 'Switch project',
|
||||
'mobile.sessions.section.projects': 'Projects',
|
||||
'mobile.sessions.section.chats': 'Chats',
|
||||
'mobile.sessions.empty.noProjectsTitle': 'No projects yet',
|
||||
'mobile.sessions.empty.noProjectsDescription': 'Add a project to start chatting with your code.',
|
||||
'mobile.sessions.empty.noSessionsTitle': 'No sessions yet',
|
||||
|
||||
@@ -131,6 +131,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"mobile.sessions.section.worktrees": "Worktrees",
|
||||
"mobile.sessions.section.otherProjects": "Cambiar de proyecto",
|
||||
"mobile.sessions.section.projects": "Proyectos",
|
||||
"mobile.sessions.section.chats": "Chats",
|
||||
"mobile.sessions.empty.noProjectsTitle": "Sin proyectos",
|
||||
"mobile.sessions.empty.noProjectsDescription": "Agrega un proyecto para empezar a chatear con tu código.",
|
||||
"mobile.sessions.empty.noSessionsTitle": "Sin sesiones",
|
||||
|
||||
@@ -2918,6 +2918,7 @@ export const dict = {
|
||||
'mobile.sessions.section.worktrees': 'Worktrees',
|
||||
'mobile.sessions.section.otherProjects': 'Changer de projet',
|
||||
'mobile.sessions.section.projects': 'Projets',
|
||||
'mobile.sessions.section.chats': 'Discussions',
|
||||
'mobile.sessions.empty.noProjectsTitle': 'Aucun projet pour le moment',
|
||||
'mobile.sessions.empty.noProjectsDescription': 'Ajoutez un projet pour commencer à discuter avec votre code.',
|
||||
'mobile.sessions.empty.noSessionsTitle': 'Aucune session pour le moment',
|
||||
|
||||
@@ -131,6 +131,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'mobile.sessions.section.worktrees': 'ワークツリー',
|
||||
'mobile.sessions.section.otherProjects': 'プロジェクトを切り替え',
|
||||
'mobile.sessions.section.projects': 'プロジェクト',
|
||||
'mobile.sessions.section.chats': 'チャット',
|
||||
'mobile.sessions.empty.noProjectsTitle': 'まだプロジェクトがありません',
|
||||
'mobile.sessions.empty.noProjectsDescription': 'プロジェクトを追加してコードとチャットを始めましょう。',
|
||||
'mobile.sessions.empty.noSessionsTitle': 'まだセッションがありません',
|
||||
|
||||
@@ -131,6 +131,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'mobile.sessions.section.worktrees': '워크트리',
|
||||
'mobile.sessions.section.otherProjects': '프로젝트 전환',
|
||||
'mobile.sessions.section.projects': '프로젝트',
|
||||
'mobile.sessions.section.chats': '채팅',
|
||||
'mobile.sessions.empty.noProjectsTitle': '프로젝트 없음',
|
||||
'mobile.sessions.empty.noProjectsDescription': '코드와 채팅을 시작하려면 프로젝트를 추가하세요.',
|
||||
'mobile.sessions.empty.noSessionsTitle': '세션 없음',
|
||||
|
||||
@@ -132,6 +132,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'mobile.sessions.section.worktrees': 'Worktrees',
|
||||
'mobile.sessions.section.otherProjects': 'Zmień projekt',
|
||||
'mobile.sessions.section.projects': 'Projekty',
|
||||
'mobile.sessions.section.chats': 'Czaty',
|
||||
'mobile.sessions.empty.noProjectsTitle': 'Brak projektów',
|
||||
'mobile.sessions.empty.noProjectsDescription': 'Dodaj projekt, aby zacząć rozmawiać ze swoim kodem.',
|
||||
'mobile.sessions.empty.noSessionsTitle': 'Brak sesji',
|
||||
|
||||
@@ -131,6 +131,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"mobile.sessions.section.worktrees": "Worktrees",
|
||||
"mobile.sessions.section.otherProjects": "Trocar de projeto",
|
||||
"mobile.sessions.section.projects": "Projetos",
|
||||
"mobile.sessions.section.chats": "Conversas",
|
||||
"mobile.sessions.empty.noProjectsTitle": "Sem projetos",
|
||||
"mobile.sessions.empty.noProjectsDescription": "Adicione um projeto para começar a conversar com seu código.",
|
||||
"mobile.sessions.empty.noSessionsTitle": "Sem sessões",
|
||||
|
||||
@@ -117,6 +117,7 @@ export const dict = {
|
||||
'mobile.sessions.section.worktrees': 'Worktree\'ler',
|
||||
'mobile.sessions.section.otherProjects': 'Proje değiştir',
|
||||
'mobile.sessions.section.projects': 'Projeler',
|
||||
'mobile.sessions.section.chats': 'Sohbetler',
|
||||
'mobile.sessions.empty.noProjectsTitle': 'Henüz proje yok',
|
||||
'mobile.sessions.empty.noProjectsDescription': 'Kodunuzla sohbet etmeye başlamak için bir proje ekleyin.',
|
||||
'mobile.sessions.empty.noSessionsTitle': 'Henüz session yok',
|
||||
|
||||
@@ -131,6 +131,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
"mobile.sessions.section.worktrees": "Worktrees",
|
||||
"mobile.sessions.section.otherProjects": "Інші проєкти",
|
||||
"mobile.sessions.section.projects": "Проєкти",
|
||||
"mobile.sessions.section.chats": "Чати",
|
||||
"mobile.sessions.empty.noProjectsTitle": "Ще немає проєктів",
|
||||
"mobile.sessions.empty.noProjectsDescription": "Додай проєкт, щоб почати спілкування з кодом.",
|
||||
"mobile.sessions.empty.noSessionsTitle": "Ще немає сесій",
|
||||
|
||||
@@ -131,6 +131,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'mobile.sessions.section.worktrees': '工作树',
|
||||
'mobile.sessions.section.otherProjects': '切换项目',
|
||||
'mobile.sessions.section.projects': '项目',
|
||||
'mobile.sessions.section.chats': '聊天',
|
||||
'mobile.sessions.empty.noProjectsTitle': '暂无项目',
|
||||
'mobile.sessions.empty.noProjectsDescription': '添加项目以开始与代码对话。',
|
||||
'mobile.sessions.empty.noSessionsTitle': '暂无会话',
|
||||
|
||||
@@ -131,6 +131,7 @@ export const dict: Record<I18nKey, string> = {
|
||||
'mobile.sessions.section.worktrees': '工作樹',
|
||||
'mobile.sessions.section.otherProjects': '切換專案',
|
||||
'mobile.sessions.section.projects': '專案',
|
||||
'mobile.sessions.section.chats': '聊天',
|
||||
'mobile.sessions.empty.noProjectsTitle': '尚無專案',
|
||||
'mobile.sessions.empty.noProjectsDescription': '新增專案即可開始與程式碼聊天。',
|
||||
'mobile.sessions.empty.noSessionsTitle': '尚無會話',
|
||||
|
||||
Reference in New Issue
Block a user