fix(mobile): navigation back behavior, scroll button position

- New Chat → Chat uses replaceState so back skips new chat screen
- Active tab persisted to sessionStorage, restored on back navigation
- Scroll-to-bottom button positioned relative to footer (not hardcoded)
- overscroll-behavior: none to prevent iOS rubber-band

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kuannnn
2026-03-29 09:50:38 +08:00
parent 10c38ad2e4
commit e2bf3512c9
4 changed files with 28 additions and 14 deletions
+6 -2
View File
@@ -36,7 +36,7 @@ function persistView(view: View) {
sessionStorage.setItem('currentView', JSON.stringify(view)); sessionStorage.setItem('currentView', JSON.stringify(view));
} }
function navigateTo(view: View) { function navigateTo(view: View, replace = false) {
persistView(view); persistView(view);
let url = '/'; let url = '/';
if (view.name === 'chat' && view.sessionId) { if (view.name === 'chat' && view.sessionId) {
@@ -45,8 +45,12 @@ function navigateTo(view: View) {
} else if (view.name === 'settings') { } else if (view.name === 'settings') {
url = '/?view=settings'; url = '/?view=settings';
} }
if (replace) {
window.history.replaceState({ view }, '', url);
} else {
window.history.pushState({ view }, '', url); window.history.pushState({ view }, '', url);
} }
}
export function App() { export function App() {
const [authed, setAuthed] = useState(isAuthenticated()); const [authed, setAuthed] = useState(isAuthenticated());
@@ -225,7 +229,7 @@ export function App() {
// Navigate to chat view with cwd — ChatView will pick up globals and send the prompt // Navigate to chat view with cwd — ChatView will pick up globals and send the prompt
const chatCwd = view.name === 'newchat' ? view.cwd : undefined; const chatCwd = view.name === 'newchat' ? view.cwd : undefined;
const v: View = { name: 'chat', cwd: chatCwd, initialPrompt: options.prompt, adapter: options.adapter }; const v: View = { name: 'chat', cwd: chatCwd, initialPrompt: options.prompt, adapter: options.adapter };
navigateTo(v); navigateTo(v, true); // replace newchat → chat so back goes to session list
setView(v); setView(v);
}, [view]); }, [view]);
+4 -2
View File
@@ -253,16 +253,18 @@ export function ChatBody({
)} )}
</div> </div>
{/* Scroll-to-bottom button */} {/* Scroll-to-bottom button — sits just above the footer area */}
<div className="relative shrink-0">
{userScrolled && ( {userScrolled && (
<button <button
onClick={() => { setUserScrolled(false); scrollToBottom(true); }} onClick={() => { setUserScrolled(false); scrollToBottom(true); }}
className="absolute bottom-20 right-4 w-8 h-8 rounded-full bg-surface border border-border flex items-center justify-center shadow-lg hover:bg-surface-light transition-colors z-10" className="absolute -top-10 right-4 w-8 h-8 rounded-full bg-surface border border-border flex items-center justify-center shadow-lg hover:bg-surface-light transition-colors z-10"
aria-label="Scroll to bottom" aria-label="Scroll to bottom"
> >
<ArrowDownToLine className="w-4 h-4 text-text-secondary" /> <ArrowDownToLine className="w-4 h-4 text-text-secondary" />
</button> </button>
)} )}
</div>
{renderAboveInput?.()} {renderAboveInput?.()}
+8 -1
View File
@@ -8,7 +8,14 @@ export function useSessions() {
() => localStorage.getItem(STORAGE.PROJECT_DIR) () => localStorage.getItem(STORAGE.PROJECT_DIR)
); );
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<'projects' | 'active'>('projects'); const [activeTab, _setActiveTab] = useState<'projects' | 'active'>(() => {
const stored = sessionStorage.getItem(STORAGE.SESSIONS_TAB);
return stored === 'active' ? 'active' : 'projects';
});
const setActiveTab = useCallback((tab: 'projects' | 'active') => {
_setActiveTab(tab);
sessionStorage.setItem(STORAGE.SESSIONS_TAB, tab);
}, []);
const [activeSessions, setActiveSessions] = useState<any[]>([]); const [activeSessions, setActiveSessions] = useState<any[]>([]);
const fetchAll = useCallback(async () => { const fetchAll = useCallback(async () => {
+1
View File
@@ -5,6 +5,7 @@ export const STORAGE = {
PROJECT_DIR: 'clawtap:projectDir', PROJECT_DIR: 'clawtap:projectDir',
DRAFT: 'clawtap:draft', DRAFT: 'clawtap:draft',
INSTALL_DISMISSED: 'clawtap:install-dismissed', INSTALL_DISMISSED: 'clawtap:install-dismissed',
SESSIONS_TAB: 'clawtap:sessionsTab',
adapterPrefs: (id: string) => `clawtap:adapterPrefs:${id}` as const, adapterPrefs: (id: string) => `clawtap:adapterPrefs:${id}` as const,
} as const; } as const;