fix: limit PWA input spacing behavior to mobile

- Prevent desktop chat input from inheriting mobile bottom safe-area gap
- Apply chat input safe-area classes only on mobile
- Scope standalone PWA safe-area and blur overlay CSS to mobile runtime
This commit is contained in:
Bohdan Triapitsyn
2026-04-03 12:45:00 +03:00
parent 9d78c48949
commit a36b11d54e
3 changed files with 62 additions and 19 deletions
+44 -1
View File
@@ -1,14 +1,17 @@
# OpenChamber - AI Agent Reference (verified) # OpenChamber - AI Agent Reference (verified)
## Core purpose ## Core purpose
OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an OpenCode server (local auto-start or remote URL). UI uses HTTP + SSE via `@opencode-ai/sdk`. OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an OpenCode server (local auto-start or remote URL). UI uses HTTP + SSE via `@opencode-ai/sdk`.
## Runtime architecture (IMPORTANT) ## Runtime architecture (IMPORTANT)
- `Desktop` is a thin Tauri shell that starts the web server sidecar and loads the web UI from `http://127.0.0.1:<port>`. - `Desktop` is a thin Tauri shell that starts the web server sidecar and loads the web UI from `http://127.0.0.1:<port>`.
- All backend logic lives in `packages/web/server/*` (and `packages/vscode/*` for the VS Code runtime). Desktop Rust is not a feature backend. - All backend logic lives in `packages/web/server/*` (and `packages/vscode/*` for the VS Code runtime). Desktop Rust is not a feature backend.
- Tauri is used only for stable native integrations: menu, dialog (open folder), notifications, updater, deep-links. - Tauri is used only for stable native integrations: menu, dialog (open folder), notifications, updater, deep-links.
## Tech stack (source of truth: `package.json`, resolved: `bun.lock`) ## Tech stack (source of truth: `package.json`, resolved: `bun.lock`)
- Runtime/tooling: Bun (`package.json` `packageManager`), Node >=20 (`package.json` `engines`) - Runtime/tooling: Bun (`package.json` `packageManager`), Node >=20 (`package.json` `engines`)
- UI: React, TypeScript, Vite, Tailwind v4 - UI: React, TypeScript, Vite, Tailwind v4
- State: Zustand (`packages/ui/src/stores/`) - State: Zustand (`packages/ui/src/stores/`)
@@ -18,55 +21,78 @@ OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an O
- VS Code: extension + webview (`packages/vscode/`) - VS Code: extension + webview (`packages/vscode/`)
## Monorepo layout ## Monorepo layout
Workspaces are `packages/*` (see `package.json`). Workspaces are `packages/*` (see `package.json`).
- Shared UI: `packages/ui` - Shared UI: `packages/ui`
- Web app + server + CLI: `packages/web` - Web app + server + CLI: `packages/web`
- Desktop app (Tauri): `packages/desktop` - Desktop app (Tauri): `packages/desktop`
- VS Code extension: `packages/vscode` - VS Code extension: `packages/vscode`
## Documentation map ## Documentation map
Before changing any mapped module, read its module documentation first. Before changing any mapped module, read its module documentation first.
### web ### web
Web runtime and server implementation for OpenChamber. Web runtime and server implementation for OpenChamber.
#### lib #### lib
Server-side integration modules used by API routes and runtime services. Server-side integration modules used by API routes and runtime services.
##### quota ##### quota
Quota provider registry, dispatch, and provider integrations for usage endpoints. Quota provider registry, dispatch, and provider integrations for usage endpoints.
- Module docs: `packages/web/server/lib/quota/DOCUMENTATION.md` - Module docs: `packages/web/server/lib/quota/DOCUMENTATION.md`
##### git ##### git
Git repository operations for the web server runtime. Git repository operations for the web server runtime.
- Module docs: `packages/web/server/lib/git/DOCUMENTATION.md` - Module docs: `packages/web/server/lib/git/DOCUMENTATION.md`
##### github ##### github
GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing. GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing.
- Module docs: `packages/web/server/lib/github/DOCUMENTATION.md` - Module docs: `packages/web/server/lib/github/DOCUMENTATION.md`
##### opencode ##### opencode
OpenCode server integration utilities including config management, provider authentication, and UI authentication. OpenCode server integration utilities including config management, provider authentication, and UI authentication.
- Module docs: `packages/web/server/lib/opencode/DOCUMENTATION.md` - Module docs: `packages/web/server/lib/opencode/DOCUMENTATION.md`
##### notifications ##### notifications
Notification message preparation utilities for system notifications, including text truncation and optional summarization. Notification message preparation utilities for system notifications, including text truncation and optional summarization.
- Module docs: `packages/web/server/lib/notifications/DOCUMENTATION.md` - Module docs: `packages/web/server/lib/notifications/DOCUMENTATION.md`
##### terminal ##### terminal
WebSocket protocol utilities for terminal input handling including message normalization, control frame parsing, and rate limiting. WebSocket protocol utilities for terminal input handling including message normalization, control frame parsing, and rate limiting.
- Module docs: `packages/web/server/lib/terminal/DOCUMENTATION.md` - Module docs: `packages/web/server/lib/terminal/DOCUMENTATION.md`
##### tts ##### tts
Server-side text-to-speech services and summarization helpers for `/api/tts/*` endpoints. Server-side text-to-speech services and summarization helpers for `/api/tts/*` endpoints.
- Module docs: `packages/web/server/lib/tts/DOCUMENTATION.md` - Module docs: `packages/web/server/lib/tts/DOCUMENTATION.md`
##### skills-catalog ##### skills-catalog
Skills catalog management including discovery, installation, and configuration of agent skill packages. Skills catalog management including discovery, installation, and configuration of agent skill packages.
- Module docs: `packages/web/server/lib/skills-catalog/DOCUMENTATION.md` - Module docs: `packages/web/server/lib/skills-catalog/DOCUMENTATION.md`
## Build / dev commands (verified) ## Build / dev commands (verified)
All scripts are in `package.json`. All scripts are in `package.json`.
- Validate: `bun run type-check`, `bun run lint` - Validate: `bun run type-check`, `bun run lint`
- Build all: `bun run build` - Build all: `bun run build`
- Desktop build: `bun run desktop:build` - Desktop build: `bun run desktop:build`
@@ -74,6 +100,7 @@ All scripts are in `package.json`.
- Release smoke build: `bun run release:test` (shell script: `scripts/test-release-build.sh`) - Release smoke build: `bun run release:test` (shell script: `scripts/test-release-build.sh`)
## Runtime entry points ## Runtime entry points
- Web bootstrap: `packages/web/src/main.tsx` - Web bootstrap: `packages/web/src/main.tsx`
- Web server: `packages/web/server/index.js` - Web server: `packages/web/server/index.js`
- Web CLI: `packages/web/bin/cli.js` (package bin: `packages/web/package.json`) - Web CLI: `packages/web/bin/cli.js` (package bin: `packages/web/package.json`)
@@ -83,6 +110,7 @@ All scripts are in `package.json`.
- VS Code webview bootstrap: `packages/vscode/webview/main.tsx` - VS Code webview bootstrap: `packages/vscode/webview/main.tsx`
## OpenCode integration ## OpenCode integration
- UI client wrapper: `packages/ui/src/lib/opencode/client.ts` (imports `@opencode-ai/sdk/v2`) - UI client wrapper: `packages/ui/src/lib/opencode/client.ts` (imports `@opencode-ai/sdk/v2`)
- SSE hookup: `packages/ui/src/hooks/useEventStream.ts` - SSE hookup: `packages/ui/src/hooks/useEventStream.ts`
- Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`) - Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`)
@@ -90,6 +118,7 @@ All scripts are in `package.json`.
- External server support: Set `OPENCODE_HOST` (full base URL, e.g. `http://hostname:4096`) or `OPENCODE_PORT`, plus `OPENCODE_SKIP_START=true`, to connect to existing OpenCode instance - External server support: Set `OPENCODE_HOST` (full base URL, e.g. `http://hostname:4096`) or `OPENCODE_PORT`, plus `OPENCODE_SKIP_START=true`, to connect to existing OpenCode instance
## Key UI patterns (reference files) ## Key UI patterns (reference files)
- Settings shell: `packages/ui/src/components/views/SettingsView.tsx` - Settings shell: `packages/ui/src/components/views/SettingsView.tsx`
- Settings shared primitives: `packages/ui/src/components/sections/shared/` - Settings shared primitives: `packages/ui/src/components/sections/shared/`
- Settings sections: `packages/ui/src/components/sections/` (incl `skills/`) - Settings sections: `packages/ui/src/components/sections/` (incl `skills/`)
@@ -98,16 +127,19 @@ All scripts are in `package.json`.
- Terminal UI: `packages/ui/src/components/terminal/` (uses `ghostty-web`) - Terminal UI: `packages/ui/src/components/terminal/` (uses `ghostty-web`)
## External / system integrations (active) ## External / system integrations (active)
- Git: `packages/ui/src/lib/gitApi.ts`, `packages/web/server/index.js` (`simple-git`) - Git: `packages/ui/src/lib/gitApi.ts`, `packages/web/server/index.js` (`simple-git`)
- Terminal PTY: `packages/web/server/index.js` (`bun-pty`/`node-pty`) - Terminal PTY: `packages/web/server/index.js` (`bun-pty`/`node-pty`)
- Skills catalog: `packages/web/server/lib/skills-catalog/`, UI: `packages/ui/src/components/sections/skills/` - Skills catalog: `packages/web/server/lib/skills-catalog/`, UI: `packages/ui/src/components/sections/skills/`
## Agent constraints ## Agent constraints
- Do not modify `../opencode` (separate repo). - Do not modify `../opencode` (separate repo).
- Do not run git/GitHub commands unless explicitly asked. - Do not run git/GitHub commands unless explicitly asked.
- Keep baseline green (run `bun run type-check`, `bun run lint`, `bun run build` before finalizing changes). - Keep baseline green (run `bun run type-check`, `bun run lint` before finalizing changes).
## Agent code of conduct ## Agent code of conduct
- Prefer the smallest correct change. - Prefer the smallest correct change.
- Preserve working behavior before improving structure. - Preserve working behavior before improving structure.
- Do not add cleverness where a direct implementation is enough. - Do not add cleverness where a direct implementation is enough.
@@ -117,6 +149,7 @@ All scripts are in `package.json`.
- Finish work end-to-end: implementation, verification, and cleanup. - Finish work end-to-end: implementation, verification, and cleanup.
## Development rules ## Development rules
- Keep diffs tight; avoid drive-by refactors. - Keep diffs tight; avoid drive-by refactors.
- Follow local precedent; inspect nearby code before introducing new patterns. - Follow local precedent; inspect nearby code before introducing new patterns.
- Backend changes: keep web, desktop, and VS Code behavior consistent when they share contracts. - Backend changes: keep web, desktop, and VS Code behavior consistent when they share contracts.
@@ -132,28 +165,33 @@ All scripts are in `package.json`.
## Architecture patterns ## Architecture patterns
### Thin entrypoints, focused modules ### Thin entrypoints, focused modules
- Keep orchestration entrypoints thin: `index.js`, bridge files, bootstrap files, provider roots. - Keep orchestration entrypoints thin: `index.js`, bridge files, bootstrap files, provider roots.
- Move route, domain, and runtime logic into focused modules with clear ownership. - Move route, domain, and runtime logic into focused modules with clear ownership.
- Prefer dependency injection over hidden module coupling. - Prefer dependency injection over hidden module coupling.
- Add or update module documentation when ownership changes. - Add or update module documentation when ownership changes.
### Strong source of truth ### Strong source of truth
- Prefer deterministic state over heuristics. - Prefer deterministic state over heuristics.
- Use live server/session state for live activity. Do not let historical anomalies masquerade as current execution. - Use live server/session state for live activity. Do not let historical anomalies masquerade as current execution.
- If a fallback is necessary, scope it narrowly to the active entity and treat it as temporary. - If a fallback is necessary, scope it narrowly to the active entity and treat it as temporary.
- Restore derived UI state from authoritative records. Example: restore model or agent from the latest user message, not assistant-side guesses. - Restore derived UI state from authoritative records. Example: restore model or agent from the latest user message, not assistant-side guesses.
### Live state vs historical state ### Live state vs historical state
- Derive live UI behavior from live state channels, not persisted history. - Derive live UI behavior from live state channels, not persisted history.
- Use historical records to restore context, not to infer that work is still in progress. - Use historical records to restore context, not to infer that work is still in progress.
- If live state is delayed, use the narrowest possible transient fallback and clear it as soon as authoritative state arrives. - If live state is delayed, use the narrowest possible transient fallback and clear it as soon as authoritative state arrives.
### Cross-runtime parity ### Cross-runtime parity
- If web defines a route or payload contract that shared UI depends on, keep VS Code and desktop parity where applicable. - If web defines a route or payload contract that shared UI depends on, keep VS Code and desktop parity where applicable.
- Shared behavior differences must be intentional and visible in code. - Shared behavior differences must be intentional and visible in code.
- Do not ship a web-only assumption into shared UI. - Do not ship a web-only assumption into shared UI.
### Partial-failure-safe flows ### Partial-failure-safe flows
- Cross-directory and multi-entity operations must tolerate partial failure. - Cross-directory and multi-entity operations must tolerate partial failure.
- Prefer per-item results, rollback paths, or resumable cleanup over all-or-nothing assumptions. - Prefer per-item results, rollback paths, or resumable cleanup over all-or-nothing assumptions.
- Never leave optimistic state or local caches stranded after failure. - Never leave optimistic state or local caches stranded after failure.
@@ -196,6 +234,7 @@ are defined in the `clack-cli-patterns` skill and should not be duplicated here.
When working on terminal CLI commands, prompts, or output formatting, agents **MUST** study the Clack CLI skill first. When working on terminal CLI commands, prompts, or output formatting, agents **MUST** study the Clack CLI skill first.
**Before starting terminal CLI work:** **Before starting terminal CLI work:**
``` ```
skill({ name: "clack-cli-patterns" }) skill({ name: "clack-cli-patterns" })
``` ```
@@ -207,6 +246,7 @@ Scope: terminal CLI only (for example `packages/web/bin/*`). Do not apply this r
When working on any UI components, styling, or visual changes, agents **MUST** study the theme system skill first. When working on any UI components, styling, or visual changes, agents **MUST** study the theme system skill first.
**Before starting any UI work:** **Before starting any UI work:**
``` ```
skill({ name: "theme-system" }) skill({ name: "theme-system" })
``` ```
@@ -295,6 +335,7 @@ A single store with N properties means every subscriber re-evaluates on every st
- **Pass directory hints when the source of truth isn't available yet.** Newly created sessions aren't in the sync store until SSE delivers them. Pass the known directory as a parameter instead of relying on lookup. - **Pass directory hints when the source of truth isn't available yet.** Newly created sessions aren't in the sync store until SSE delivers them. Pass the known directory as a parameter instead of relying on lookup.
## Regression-prevention checklist ## Regression-prevention checklist
- When adding fallback logic, ask: can stale persisted data keep this path active forever? - When adding fallback logic, ask: can stale persisted data keep this path active forever?
- When deriving UI state, ask: is this live state, historical state, or inferred state? - When deriving UI state, ask: is this live state, historical state, or inferred state?
- When adding store fields, ask: who reads this, how often does it change, and should it live elsewhere? - When adding store fields, ask: who reads this, how often does it change, and should it live elsewhere?
@@ -304,11 +345,13 @@ A single store with N properties means every subscriber re-evaluates on every st
- When fixing a bug with a heuristic, prefer narrowing the heuristic over widening it. - When fixing a bug with a heuristic, prefer narrowing the heuristic over widening it.
## Validation expectations ## Validation expectations
- Run `bun run type-check`, `bun run lint`, and `bun run build` before finalizing. - Run `bun run type-check`, `bun run lint`, and `bun run build` before finalizing.
- For hot-path changes, verify behavior under streaming or repeated events, not just static render. - For hot-path changes, verify behavior under streaming or repeated events, not just static render.
- For sync or startup changes, verify fresh load, retry/failure, and restart behavior. - For sync or startup changes, verify fresh load, retry/failure, and restart behavior.
- For session changes, verify create, stream, abort, permission, archive/delete, and revisit flows when relevant. - For session changes, verify create, stream, abort, permission, archive/delete, and revisit flows when relevant.
## Recent changes ## Recent changes
- Releases + high-level changes: `CHANGELOG.md` - Releases + high-level changes: `CHANGELOG.md`
- Recent commits: `git log --oneline` (latest tags: `v1.4.6`, `v1.4.5`) - Recent commits: `git log --oneline` (latest tags: `v1.4.6`, `v1.4.5`)
@@ -2990,7 +2990,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
className={cn( className={cn(
"relative pt-0 pb-4", "relative pt-0 pb-4",
isDesktopExpanded && 'flex h-full min-h-0 flex-col pt-4', isDesktopExpanded && 'flex h-full min-h-0 flex-col pt-4',
isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area" isMobile && (isKeyboardOpen ? 'ios-keyboard-safe-area' : 'bottom-safe-area')
)} )}
data-keyboard-avoid="true" data-keyboard-avoid="true"
style={isMobile && inputBarOffset > 0 && !isKeyboardOpen ? { marginBottom: `${inputBarOffset}px` } : undefined} style={isMobile && inputBarOffset > 0 && !isKeyboardOpen ? { marginBottom: `${inputBarOffset}px` } : undefined}
+17 -17
View File
@@ -257,7 +257,7 @@
/* iOS-specific safe area handling with CSS variable support */ /* iOS-specific safe area handling with CSS variable support */
@supports (-webkit-touch-callout: none) { @supports (-webkit-touch-callout: none) {
/* Safe area handling for fixed positioned elements */ /* Safe area handling for fixed positioned elements */
:root { :root.device-mobile:not(.desktop-runtime) {
--oc-safe-area-top: env(safe-area-inset-top, 0); --oc-safe-area-top: env(safe-area-inset-top, 0);
--oc-safe-area-right: env(safe-area-inset-right, 0); --oc-safe-area-right: env(safe-area-inset-right, 0);
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0); --oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
@@ -265,11 +265,11 @@
--oc-safe-area-left: env(safe-area-inset-left, 0); --oc-safe-area-left: env(safe-area-inset-left, 0);
} }
.header-safe-area { :root.device-mobile:not(.desktop-runtime) .header-safe-area {
padding-top: var(--oc-safe-area-top); padding-top: var(--oc-safe-area-top);
} }
.main-content-safe-area { :root.device-mobile:not(.desktop-runtime) .main-content-safe-area {
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
padding-left: var(--oc-safe-area-left); padding-left: var(--oc-safe-area-left);
@@ -277,23 +277,23 @@
} }
/* Safe area for bottom fixed/bottom-0 elements */ /* Safe area for bottom fixed/bottom-0 elements */
.bottom-safe-area { :root.device-mobile:not(.desktop-runtime) .bottom-safe-area {
padding-bottom: var(--oc-safe-area-bottom-visual) !important; padding-bottom: var(--oc-safe-area-bottom-visual) !important;
} }
/* Drawer safe area - top already offset by header height */ /* Drawer safe area - top already offset by header height */
.drawer-safe-area { :root.device-mobile:not(.desktop-runtime) .drawer-safe-area {
padding-top: 0; padding-top: 0;
padding-bottom: var(--oc-safe-area-bottom-visual); padding-bottom: var(--oc-safe-area-bottom-visual);
} }
/* iOS keyboard home indicator safe area - used when keyboard is open */ /* iOS keyboard home indicator safe area - used when keyboard is open */
.ios-keyboard-safe-area { :root.device-mobile:not(.desktop-runtime) .ios-keyboard-safe-area {
padding-bottom: calc(var(--oc-keyboard-home-indicator, 34px) + var(--oc-safe-area-bottom-visual, 0px)) !important; padding-bottom: calc(var(--oc-keyboard-home-indicator, 34px) + var(--oc-safe-area-bottom-visual, 0px)) !important;
} }
/* Prevent keyboard-induced page scroll on iOS PWA */ /* Prevent keyboard-induced page scroll on iOS PWA */
body { :root.device-mobile:not(.desktop-runtime) body {
min-height: 100vh; min-height: 100vh;
min-height: -webkit-fill-available; min-height: -webkit-fill-available;
position: fixed; position: fixed;
@@ -304,24 +304,24 @@
} }
/* Fix iOS viewport issues */ /* Fix iOS viewport issues */
.flex.flex-col.h-screen { :root.device-mobile:not(.desktop-runtime) .flex.flex-col.h-screen {
min-height: 100vh; min-height: 100vh;
min-height: -webkit-fill-available; min-height: -webkit-fill-available;
} }
/* Prevent content overlap in iOS */ /* Prevent content overlap in iOS */
.flex-1.overflow-hidden { :root.device-mobile:not(.desktop-runtime) .flex-1.overflow-hidden {
position: relative; position: relative;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
/* Update header height for iOS */ /* Update header height for iOS */
.header-safe-area { :root.device-mobile:not(.desktop-runtime) .header-safe-area {
--header-height: 4.5rem; --header-height: 4.5rem;
} }
.mobile-sidebar-top { :root.device-mobile:not(.desktop-runtime) .mobile-sidebar-top {
--header-height: 4.5rem; --header-height: 4.5rem;
} }
@@ -358,7 +358,7 @@
/* Fallback for non-iOS standalone mode */ /* Fallback for non-iOS standalone mode */
@supports not (-webkit-touch-callout: none) { @supports not (-webkit-touch-callout: none) {
:root { :root.device-mobile:not(.desktop-runtime) {
--oc-safe-area-top: env(safe-area-inset-top, 0); --oc-safe-area-top: env(safe-area-inset-top, 0);
--oc-safe-area-right: env(safe-area-inset-right, 0); --oc-safe-area-right: env(safe-area-inset-right, 0);
--oc-safe-area-bottom: env(safe-area-inset-bottom, 0); --oc-safe-area-bottom: env(safe-area-inset-bottom, 0);
@@ -366,18 +366,18 @@
--oc-safe-area-left: env(safe-area-inset-left, 0); --oc-safe-area-left: env(safe-area-inset-left, 0);
} }
.header-safe-area { :root.device-mobile:not(.desktop-runtime) .header-safe-area {
padding-top: var(--oc-safe-area-top); padding-top: var(--oc-safe-area-top);
} }
.main-content-safe-area { :root.device-mobile:not(.desktop-runtime) .main-content-safe-area {
padding-top: 0; padding-top: 0;
padding-bottom: 0; padding-bottom: 0;
padding-left: var(--oc-safe-area-left); padding-left: var(--oc-safe-area-left);
padding-right: var(--oc-safe-area-right); padding-right: var(--oc-safe-area-right);
} }
.bottom-safe-area { :root.device-mobile:not(.desktop-runtime) .bottom-safe-area {
padding-bottom: var(--oc-safe-area-bottom-visual) !important; padding-bottom: var(--oc-safe-area-bottom-visual) !important;
} }
} }
@@ -439,7 +439,7 @@
@media (display-mode: standalone) { @media (display-mode: standalone) {
/* Home indicator blur overlay */ /* Home indicator blur overlay */
body::after { :root.device-mobile:not(.desktop-runtime) body::after {
content: ''; content: '';
position: fixed; position: fixed;
bottom: 0; bottom: 0;
@@ -462,7 +462,7 @@
/* Fallback for browsers without rgba(from ...) support */ /* Fallback for browsers without rgba(from ...) support */
@supports not (color: rgba(from white r g b / 0.5)) { @supports not (color: rgba(from white r g b / 0.5)) {
body::after { :root.device-mobile:not(.desktop-runtime) body::after {
background: linear-gradient( background: linear-gradient(
to top, to top,
var(--background) 0%, var(--background) 0%,