diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f3d1be..0bedd62c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.2] - 2026-06-06 + +### Fixed + +- 📱 **Improved mobile keyboard handling.** The terminal and chat now resize correctly when the on-screen keyboard opens on iOS and Android. +- 📱 **Fixed page bouncing on mobile.** Eliminated unwanted page scrolling and bounce effects on touch devices. + +### Changed + +- 🔄 **Model selection syncs across devices.** Your selected chat model now persists across browsers and devices instead of being saved locally. + ## [0.1.1] - 2026-06-06 ### Fixed diff --git a/cptr/frontend/package.json b/cptr/frontend/package.json index 13dedd29..0dcf7428 100644 --- a/cptr/frontend/package.json +++ b/cptr/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.1.0", + "version": "0.1.2", "type": "module", "scripts": { "dev": "vite dev", diff --git a/cptr/frontend/src/app.css b/cptr/frontend/src/app.css index 807c3bd4..31bdf3e7 100644 --- a/cptr/frontend/src/app.css +++ b/cptr/frontend/src/app.css @@ -31,10 +31,24 @@ border-color: var(--color-gray-200, currentColor); } + html, + body { + height: 100dvh; + margin: 0; + overflow: hidden; + overscroll-behavior: none; + scrollbar-width: none; + -ms-overflow-style: none; + } + + html::-webkit-scrollbar, + body::-webkit-scrollbar { + display: none; + } + html { color-scheme: dark light; touch-action: manipulation; - overscroll-behavior: none; } button { diff --git a/cptr/frontend/src/app.html b/cptr/frontend/src/app.html index ac0a430a..06594b23 100644 --- a/cptr/frontend/src/app.html +++ b/cptr/frontend/src/app.html @@ -4,7 +4,7 @@ diff --git a/cptr/frontend/src/lib/components/Terminal.svelte b/cptr/frontend/src/lib/components/Terminal.svelte index 286ea694..bfe7322c 100644 --- a/cptr/frontend/src/lib/components/Terminal.svelte +++ b/cptr/frontend/src/lib/components/Terminal.svelte @@ -62,9 +62,6 @@ if (rect.width === 0 || rect.height === 0) return; try { fitAddon.fit(); - // Do NOT scroll after fit. Rich CLI apps (htop, vim, Claude Code) - // manage their own cursor/viewport after SIGWINCH. Forcing scroll - // interferes with their rendering. } catch { // FitAddon can throw if terminal not properly attached } @@ -192,6 +189,18 @@ fitAddon = new FitAddon(); term.loadAddon(fitAddon); term.open(containerEl); + // // iOS: Move xterm's textarea from inside .xterm-helpers (position:absolute, + // // offscreen) to a flex sibling BELOW the terminal. + // if ('ontouchstart' in window && term.textarea) { + // containerEl.after(term.textarea); + // term.focus = () => term.textarea?.focus(); + // + // // xterm's mousedown calls ev.preventDefault() which blocks iOS + // // viewport adjustment. Focus on touchstart (fires before mousedown). + // containerEl.addEventListener('touchstart', () => { + // term.textarea?.focus(); + // }, { passive: true }); + // } // GPU-accelerated rendering, 2-5x faster than canvas 2D. // Falls back to canvas if WebGL is unavailable. @@ -374,7 +383,10 @@ }); -
+
+
+ +
diff --git a/cptr/frontend/src/lib/components/chat/ChatPanel.svelte b/cptr/frontend/src/lib/components/chat/ChatPanel.svelte index 47490acd..c157c791 100644 --- a/cptr/frontend/src/lib/components/chat/ChatPanel.svelte +++ b/cptr/frontend/src/lib/components/chat/ChatPanel.svelte @@ -24,7 +24,7 @@ import { socketStore } from '$lib/stores/socket.svelte'; import { onMount, onDestroy, tick } from 'svelte'; import { get } from 'svelte/store'; - import { currentWorkspace, toolApprovalMode, streamingBehavior } from '$lib/stores'; + import { currentWorkspace, toolApprovalMode, streamingBehavior, selectedModelId } from '$lib/stores'; import ChatInput from './ChatInput.svelte'; import UserMessage from './UserMessage.svelte'; @@ -355,11 +355,9 @@ if (chatId) loadChat(chatId); } - const MODEL_STORAGE_KEY = 'cptr:chat:lastModel'; - onMount(() => { const models = get(chatModels); - const saved = localStorage.getItem(MODEL_STORAGE_KEY); + const saved = get(selectedModelId); const dm = get(defaultModel); if (saved && models.some((m) => m.id === saved)) selectedModel = saved; else if (dm) selectedModel = dm; @@ -397,7 +395,7 @@ // ── Persist model selection ───────────────────────────────── $effect(() => { - if (selectedModel) localStorage.setItem(MODEL_STORAGE_KEY, selectedModel); + if (selectedModel) selectedModelId.set(selectedModel); }); // ── Sync streaming state to shared store for tab icon ──── diff --git a/cptr/frontend/src/lib/stores.ts b/cptr/frontend/src/lib/stores.ts index 6a02af6e..1bae2c9f 100644 --- a/cptr/frontend/src/lib/stores.ts +++ b/cptr/frontend/src/lib/stores.ts @@ -74,6 +74,7 @@ export interface UserPreferences { workspaceOrder?: string[]; // ordered paths for sidebar drag-reorder keybindings?: Record; // user-customised keyboard shortcuts version?: string; // last seen app version for changelog + selectedModelId?: string; // last selected chat model, synced across browsers } export type Theme = 'dark' | 'light' | 'system'; @@ -138,6 +139,7 @@ export const gitReviewOpen = writable(false); export const isGitRepo = writable(false); export type StreamingBehavior = 'queue' | 'interrupt'; export const streamingBehavior = writable('queue'); +export const selectedModelId = writable(''); /** Saved workspace path order for sidebar drag-reorder. */ export const workspaceOrder = writable([]); @@ -269,7 +271,8 @@ function persistPreferences(): void { locale: i18next.language, workspaceOrder: get(workspaceOrder), keybindings: get(keybindings), - version: get(lastSeenVersion) + version: get(lastSeenVersion), + selectedModelId: get(selectedModelId) || undefined }; savePreferences(prefs as unknown as Record).catch(() => {}); }, 300); @@ -303,6 +306,9 @@ function subscribeForPersistence() { lastSeenVersion.subscribe(() => { if (get(stateLoaded)) persistPreferences(); }); + selectedModelId.subscribe(() => { + if (get(stateLoaded)) persistPreferences(); + }); i18next.on('languageChanged', () => { if (get(stateLoaded)) persistPreferences(); }); @@ -327,6 +333,7 @@ export async function loadPreferences(): Promise { if (Array.isArray(prefs.workspaceOrder)) workspaceOrder.set(prefs.workspaceOrder as string[]); if (prefs.keybindings) loadKeybindings(prefs.keybindings as Record); if (prefs.version) lastSeenVersion.set(prefs.version as string); + if (prefs.selectedModelId) selectedModelId.set(prefs.selectedModelId as string); } catch { // First run, no preferences yet } diff --git a/cptr/frontend/src/routes/+layout.svelte b/cptr/frontend/src/routes/+layout.svelte index f28d6cb2..3248c0d0 100644 --- a/cptr/frontend/src/routes/+layout.svelte +++ b/cptr/frontend/src/routes/+layout.svelte @@ -38,7 +38,6 @@ let { children } = $props(); let showQuickOpen = $state(false); let showSettings = $state(false); - let viewportHeight = $state('100dvh'); // Auth state type AuthState = 'checking' | 'needs_setup' | 'needs_login' | 'authenticated'; @@ -65,28 +64,28 @@ }, 30 * 60 * 1000 ); - - // iOS/Android: visualViewport gives accurate height when keyboard is open. - // Debounce to avoid firing dozens of resizes during the keyboard animation - // (~300ms). We only commit the final height after things settle, which - // prevents TUI apps from receiving multiple SIGWINCH and re-rendering. + // iOS: Termius-style keyboard handling. visualViewport.height + // gives us the area above the keyboard. Set max-height on the + // main content column so the terminal shrinks to fit. + // The layout container's overflow:hidden clips the gap. const vv = window.visualViewport; - let vpTimer: ReturnType; if (vv) { - const update = () => { - clearTimeout(vpTimer); - vpTimer = setTimeout(() => { - viewportHeight = `${vv.height}px`; - }, 350); + const syncHeight = () => { + const col = document.getElementById('main-col'); + if (!col) return; + const kbHeight = window.innerHeight - vv.height; + console.log('[viewport]', { innerHeight: window.innerHeight, vvHeight: vv.height, kbHeight }); + col.style.maxHeight = kbHeight > 100 ? `${vv.height}px` : ''; }; - vv.addEventListener('resize', update); + // iOS may fire 'scroll' instead of 'resize' when keyboard opens + vv.addEventListener('resize', syncHeight); + vv.addEventListener('scroll', syncHeight); return () => { - clearTimeout(vpTimer); clearInterval(healthCheck); - vv.removeEventListener('resize', update); + vv.removeEventListener('resize', syncHeight); + vv.removeEventListener('scroll', syncHeight); }; } - return () => clearInterval(healthCheck); }); @@ -249,12 +248,11 @@ /> {:else if $stateLoaded}
-
+
{#if !$currentWorkspace} {/if} diff --git a/pyproject.toml b/pyproject.toml index fd5ca13f..747aa8ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cptr" -version = "0.1.1" +version = "0.1.2" description = "Your computer, from anywhere. Code, manage, and control your machine from the web." license = {file = "LICENSE"} readme = "README.md"