feat(mobile): start a session in a project from the sessions drawer
Project rows carried an active-project dot and a session count, neither of which answers a question the user has while browsing the drawer, and there was no way to start a session in a project's root directory: the only per-row action was creating a worktree. Drop both indicators and put a "+" beside the worktree button instead. It opens a draft already carrying the project and its directory, the same contract as the desktop sidebar's per-project "+", so the current directory is not switched out from under the session behind the drawer. Project rows in search results lose the count the same way and gain the same button; dropping it there also drops a per-project session scan that ran on every keystroke. Testing: package type-check and lint; drove the mobile surface in a browser against an isolated server (rows render the button, counts gone).
This commit is contained in:
@@ -269,6 +269,31 @@ const NewWorktreeIconButton: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
/** Starts a session draft already pointed at this project — the mobile twin of
|
||||
the desktop sidebar's per-project "+". */
|
||||
const NewSessionIconButton: React.FC<{
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}> = ({ label, onClick, className }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex size-9 shrink-0 items-center justify-center rounded-full text-[var(--surface-mutedForeground)] transition-colors hover:bg-[var(--interactive-hover)] hover:text-[var(--surface-foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--interactive-focus-ring)]',
|
||||
className,
|
||||
)}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onClick();
|
||||
}}
|
||||
style={{ touchAction: 'manipulation' }}
|
||||
>
|
||||
<Icon name="add" className="size-4" />
|
||||
</button>
|
||||
);
|
||||
|
||||
// Width of the swipe-revealed action area (rename + archive + delete buttons).
|
||||
const ROW_ACTIONS_WIDTH = 144;
|
||||
const ROW_SWIPE_SNAP_MS = 180;
|
||||
@@ -1387,6 +1412,15 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
// Same contract as the desktop sidebar's per-project "+": the draft carries
|
||||
// the project and its directory, so the app's current directory is not
|
||||
// switched out from under the session that is still open behind the drawer.
|
||||
const handleNewSessionInProject = (project: ProjectMeta) => {
|
||||
setActiveProjectIdOnly(project.id);
|
||||
openNewSessionDraft({ selectedProjectId: project.id, directoryOverride: project.path });
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
const filteredNodes = React.useMemo(() => {
|
||||
if (!normalizedQuery) return projectNodes;
|
||||
return projectNodes.filter((node) => {
|
||||
@@ -1418,18 +1452,10 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
);
|
||||
}, [normalizedQuery, pinnedSessionIds, projectsMeta, sessionOrderRanks, sessions]);
|
||||
|
||||
const searchProjectMatches = React.useMemo(() => {
|
||||
if (!normalizedQuery) return [] as Array<ProjectMeta & { sessionCount: number }>;
|
||||
return rankByQuery(projectsMeta, normalizedQuery, (project) => [project.label, project.path])
|
||||
.map((project) => ({
|
||||
...project,
|
||||
sessionCount: sessions.filter((session) => {
|
||||
if (getParentId(session)) return false;
|
||||
const directory = normalizePath(getSessionDirectory(session));
|
||||
return projectMatchesExactDirectory(project, directory);
|
||||
}).length,
|
||||
}));
|
||||
}, [normalizedQuery, projectsMeta, sessions]);
|
||||
const searchProjectMatches = React.useMemo<ProjectMeta[]>(() => {
|
||||
if (!normalizedQuery) return [];
|
||||
return rankByQuery(projectsMeta, normalizedQuery, (project) => [project.label, project.path]);
|
||||
}, [normalizedQuery, projectsMeta]);
|
||||
|
||||
const hasNoMatches =
|
||||
normalizedQuery && searchSessionMatches.length === 0 && searchProjectMatches.length === 0;
|
||||
@@ -1592,16 +1618,15 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
<span className="block min-w-0 flex-1 truncate typography-ui-label text-foreground">
|
||||
{project.label}
|
||||
</span>
|
||||
<span className="shrink-0 typography-micro text-muted-foreground tabular-nums">
|
||||
{project.sessionCount}
|
||||
</span>
|
||||
</button>
|
||||
{project.isGitRepo ? (
|
||||
<NewWorktreeIconButton
|
||||
className="mr-2"
|
||||
onClick={() => handleNewWorktree(project.id)}
|
||||
/>
|
||||
<NewWorktreeIconButton onClick={() => handleNewWorktree(project.id)} />
|
||||
) : null}
|
||||
<NewSessionIconButton
|
||||
className="mr-2"
|
||||
label={t('mobile.sessions.newSessionInProjectAria', { label: project.label })}
|
||||
onClick={() => handleNewSessionInProject(project)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1773,17 +1798,15 @@ export const MobileSessionsSheet: React.FC<MobileSessionsSheetProps> = ({ open,
|
||||
<span className="block min-w-0 flex-1 truncate typography-ui-label font-semibold text-foreground">
|
||||
{node.project.label}
|
||||
</span>
|
||||
{node.isActive ? <ActiveDot ariaLabel={t('mobile.sessions.activeProjectAria')} /> : null}
|
||||
<span className="shrink-0 typography-micro text-muted-foreground tabular-nums">
|
||||
{node.totalSessions}
|
||||
</span>
|
||||
</button>
|
||||
{node.project.isGitRepo ? (
|
||||
<NewWorktreeIconButton
|
||||
className="mr-2"
|
||||
onClick={() => handleNewWorktree(node.project.id)}
|
||||
/>
|
||||
<NewWorktreeIconButton onClick={() => handleNewWorktree(node.project.id)} />
|
||||
) : null}
|
||||
<NewSessionIconButton
|
||||
className="mr-2"
|
||||
label={t('mobile.sessions.newSessionInProjectAria', { label: node.project.label })}
|
||||
onClick={() => handleNewSessionInProject(node.project)}
|
||||
/>
|
||||
</div>
|
||||
</MobileSwipeActionsRow>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user