feat(mobile): mobile app navigation rework and beta-feedback closeout (#2561)
Navigation model rebuilt around two full-width drawers and a minimal header (sessions / title-switcher / usage ring / workspace): - Left sessions drawer: cross-project tree with live status indicators, swipe actions on sessions (rename/archive/delete) and on group headers (project edit / two-step close, worktree delete), reorder-only edit mode with collapsible project cards and draggable worktrees, app-level footer (connected instance, settings, pending web update). - Right workspace drawer: Changes / Files / Terminal / Notes / MCP as pill tabs (inactive tabs icon-only); panes stay mounted once visited. The full desktop file editor serves the Files tab; read/skill tool taps in chat open the file there at the requested line. - Header session switcher on title tap: 10 cross-project recents with live busy/attention indicators and project · branch metadata; the usage ring opens a metadata overlay with an explicit loading state. - The overflow menu is gone on phones (its destinations moved into the drawers); iPad keeps it until its dedicated layout pass. Correctness and continuity: - /auth/session answers bearer-first, so a stale WebView cookie can no longer mask a revoked device token; cold launches classify failures fast and land on an explicit connect screen. - Authoritative session snapshots raise frozen ordering baselines and stale live ranks — recents stay truthful after the app slept. - Cold launches reopen the last active session per instance (persisted pointer, confirmed against a sessions snapshot; a user-opened draft clears it), with a logo hold instead of a draft flash. Also: collapsed pill composer gains the stop control; chat tool rows share one 36px rhythm; Task subtool rows truncate; larger bottom safe area so the composer clears big-screen corner radii; Capacitor build hides About/Update (store updates apply there); widgets link to the sessions drawer with a list icon; MobileApp split into focused modules; five mobile-surface detectors unified; translucent borders normalized to 70%; all new strings translated across the 10 locales. iPad and foldable layouts are intentionally untouched - separate next version PR.
This commit is contained in:
committed by
GitHub
parent
ea8cc5d7b0
commit
86ef96302d
@@ -8,6 +8,7 @@ import { useGitAllBranches } from '@/stores/useGitStore';
|
||||
import type { SessionNode } from '../types';
|
||||
import { isPathWithinProject } from '../utils';
|
||||
import { compareSessionsByLifecycleOrder, useSessionOrderingStore } from '@/sync/session-ordering';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
|
||||
export type SwitcherItem = {
|
||||
node: SessionNode;
|
||||
@@ -23,6 +24,8 @@ const MAX_PARENT_SESSIONS = 7;
|
||||
|
||||
type SwitcherItemsOptions = {
|
||||
scopeProjectId?: string | null;
|
||||
/** How many parent sessions to return (default 7 — the desktop dropdown). */
|
||||
maxParents?: number;
|
||||
};
|
||||
|
||||
const normalize = (value: string | null | undefined): string | null => {
|
||||
@@ -41,12 +44,30 @@ const formatProjectLabel = (project: { label?: string | null; path: string } | n
|
||||
};
|
||||
|
||||
export const useSwitcherItems = (enabled: boolean, options: SwitcherItemsOptions = {}): SwitcherItem[] => {
|
||||
const { scopeProjectId = null } = options;
|
||||
const { scopeProjectId = null, maxParents = MAX_PARENT_SESSIONS } = options;
|
||||
const activeSessions = useGlobalSessionsStore((state) => state.activeSessions);
|
||||
const projects = useProjectsStore((state) => state.projects);
|
||||
const pinnedSessionIds = useSessionPinnedStore((state) => state.ids);
|
||||
const sessionOrderRanks = useSessionOrderingStore((state) => state.rankById);
|
||||
const branchesByDirectory = useGitAllBranches();
|
||||
const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject);
|
||||
|
||||
// Worktree sessions live OUTSIDE their project's path, so prefix matching
|
||||
// can't resolve their project — and their branch is known from worktree
|
||||
// discovery long before any git status is fetched for that directory.
|
||||
const worktreeInfoByPath = React.useMemo(() => {
|
||||
const map = new Map<string, { projectPath: string; branch: string | null }>();
|
||||
for (const [projectPath, worktrees] of availableWorktreesByProject) {
|
||||
const normalizedProjectPath = normalize(projectPath);
|
||||
if (!normalizedProjectPath) continue;
|
||||
for (const worktree of worktrees) {
|
||||
const worktreePath = normalize(worktree.path);
|
||||
if (!worktreePath) continue;
|
||||
map.set(worktreePath, { projectPath: normalizedProjectPath, branch: worktree.branch?.trim() || null });
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [availableWorktreesByProject]);
|
||||
|
||||
const normalizedProjects = React.useMemo(
|
||||
() => projects
|
||||
@@ -58,12 +79,18 @@ export const useSwitcherItems = (enabled: boolean, options: SwitcherItemsOptions
|
||||
const findProjectForDirectory = React.useCallback(
|
||||
(directory: string | null) => {
|
||||
if (!directory) return null;
|
||||
// Known worktree → its project, regardless of where the worktree lives.
|
||||
const worktreeInfo = worktreeInfoByPath.get(normalize(directory) ?? directory);
|
||||
if (worktreeInfo) {
|
||||
const byPath = normalizedProjects.find((project) => project.normalizedPath === worktreeInfo.projectPath);
|
||||
if (byPath) return byPath;
|
||||
}
|
||||
const matches = normalizedProjects
|
||||
.filter((project) => isPathWithinProject(directory, project.normalizedPath))
|
||||
.sort((a, b) => (b.normalizedPath?.length ?? 0) - (a.normalizedPath?.length ?? 0));
|
||||
return matches[0] ?? null;
|
||||
},
|
||||
[normalizedProjects],
|
||||
[normalizedProjects, worktreeInfoByPath],
|
||||
);
|
||||
|
||||
const items = React.useMemo<SwitcherItem[]>(() => {
|
||||
@@ -94,7 +121,7 @@ export const useSwitcherItems = (enabled: boolean, options: SwitcherItemsOptions
|
||||
return findProjectForDirectory(directory)?.id === scopeProjectId;
|
||||
})
|
||||
.sort((a, b) => compareSessionsByLifecycleOrder(a, b, pinnedSessionIds, sessionOrderRanks))
|
||||
.slice(0, MAX_PARENT_SESSIONS);
|
||||
.slice(0, maxParents);
|
||||
|
||||
const buildNode = (session: Session): SessionNode => {
|
||||
const childSessions = childrenByParent.get(session.id) ?? [];
|
||||
@@ -109,7 +136,11 @@ export const useSwitcherItems = (enabled: boolean, options: SwitcherItemsOptions
|
||||
const directory = resolveGlobalSessionDirectory(session);
|
||||
const matchedProject = findProjectForDirectory(directory);
|
||||
const projectLabel = formatProjectLabel(matchedProject);
|
||||
const branchLabel = directory ? branchesByDirectory.get(directory) ?? null : null;
|
||||
// Live git branch when available; the discovered worktree branch fills
|
||||
// in for directories whose git status hasn't been fetched yet.
|
||||
const worktreeInfo = directory ? worktreeInfoByPath.get(normalize(directory) ?? directory) : null;
|
||||
const liveBranch = directory ? branchesByDirectory.get(directory) : undefined;
|
||||
const branchLabel = liveBranch ?? worktreeInfo?.branch ?? null;
|
||||
return {
|
||||
node: buildNode(session),
|
||||
projectId: matchedProject?.id ?? null,
|
||||
@@ -120,7 +151,7 @@ export const useSwitcherItems = (enabled: boolean, options: SwitcherItemsOptions
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [activeSessions, branchesByDirectory, enabled, findProjectForDirectory, pinnedSessionIds, scopeProjectId, sessionOrderRanks]);
|
||||
}, [activeSessions, branchesByDirectory, enabled, findProjectForDirectory, maxParents, pinnedSessionIds, scopeProjectId, sessionOrderRanks, worktreeInfoByPath]);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user