* fix(number-input): stepper drift on rapid clicks (closes#2053)
The shared NumberInput stepper buttons (-/+) computed the next value from
`baseValue`, a useMemo of the controlled `value` prop. When the user
pressed - and + in rapid succession, both inline closures read the same
pre-update `baseValue` because the prop round-trip (click ->
onValueChange -> store/persistence update -> re-render) had not landed.
Net result: rapid alternation drifted or oscillated instead of returning
to the start value.
Route the stepper math through a new `committedValueRef` updated
synchronously inside `commitValue`, and re-sync the ref from `baseValue`
via a useEffect so external mutations (the reset button next to each
stepper, undo, multi-instance sync) keep the ref aligned. Keep
`baseValue` for the `disabled` predicate so the prop still gates the
buttons at the bounds.
For the same invariant, route `handleBlur`'s finite-parse branch
through `commitValue` so a typed value followed by a stepper click does
not compute from a stale ref. The empty-draft `onClear` early-return
relies on the baseValue useEffect to re-sync.
Cover the path with a new bun:test suite that drives the real onClick
closures through createRoot with a minimal document/window stub (no new
deps). Tests assert rapid --+ and +-- sequences net to the start value,
a sustained 6-click alternation does not drift, sequential clicks with a
re-render between them settle correctly, and a typed-then-stepper
sequence uses the typed base.
* test(number-input): restore DOM globals and guard empty recorded arrays
Follow-up to the stepper-drift fix on the same PR.
- installDomStub now captures the previous values of
document/window/navigator/IS_REACT_ACT_ENVIRONMENT before overwriting
and exposes a restore() function. withHandle calls stub.restore() in
finally after unmount(), so the test process no longer leaks a fake
DOM across tests.
- Replace the four 'recorded[length-1]!' non-null assertions with a
lastCommit(handle) helper that throws a clear error if the parent
never produced a commit. A regression that drops the first commit
fails loudly instead of silently coercing to undefined.
- Add a short comment on the useEffect re-sync documenting the
controlled-parent assumption (ref can briefly lead the prop if a
parent ever rejects or debounces onValueChange; no production caller
does today).
---------
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
* feat(command-palette): add projects to existing fuzzy search
Adds projects to the existing command palette search — same single-input
fuzzy search that already covers sessions, files, settings, and commands.
Projects are scored alongside everything else by scoreByFuzzyQuery, and
the best-matching result appears first regardless of type.
Selecting a project opens a new session draft with the project pre-selected.
Closes#976
* fix(command-palette): keep file search tied to debounced query
---------
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
* feat(settings): add editor font size setting for chat input and code editor
Adds an 'Editor font size' control in Settings > Appearance that sets an
absolute px font size for the chat input textarea and the in-app
CodeMirror editor. Mirrors the existing terminalFontSize lifecycle.
- New store field editorFontSize (default 13, clamp 9-32, step 1) in
useUIStore with narrow selectors at each consumer.
- Persistence wired through appearanceAutoSave, desktop + runtime API
types, and persistence.ts read/normalize.
- Settings UI row (NumberInput) with reset to 13, VisibleSetting union
entry, OpenChamberPage registration, and search index entry
appearance.editor-font-size.
- Applied as a post-zoom absolute override on the chat input textarea
and on the CodeMirror theme's content rule, leaving gutter/line-number
chrome at its existing hardcoded sizes (matches terminal scope).
- All 10 locales translated (en, es, fr, ja, ko, pl, pt-BR, uk, zh-CN,
zh-TW); no English placeholders in non-English dictionaries.
Refs #1325
* fix(codemirror): use unitless lineHeight so it scales with editor font size
The & rule in the CodeMirror theme set lineHeight to 1.5rem (~24px),
which does not scale when editorFontSize is increased (e.g., 28-32px).
This causes overlapping lines at larger font sizes.
Change to unitless 1.5, which scales proportionally with whatever fontSize
resolves to (dynamic prop or --text-code fallback). Matches browser best
practice for proportional leading.
Review comment: https://github.com/openchamber/openchamber/pull/2065
---------
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
React error #31 (Objects are not valid as a React child) was thrown
intermittently when a task/subagent tool returned structured data
(e.g. { TODO: '...' }) in a field that the OpenCode SDK types as a
plain string. Pathological payloads would propagate into JSX children
without runtime validation, white-screening the chat until refresh.
This change adds a single `coerceToText` helper in toolRenderers.tsx
and applies it at every vulnerable JSX expression:
- ToolPart.tsx:1807,1975 {state.error} (typed string, can be object)
- ToolPart.tsx:1825 {q.question} (QuestionCard input cast)
- ToolPart.tsx:1830 {opt.label} (QuestionCard input cast)
- ToolPart.tsx:1848 task tool markdown output
- ToolPart.tsx:1898 ToolScrollableTextOutput entry
- toolRenderers.tsx {todo.content} x4 in renderTodoOutput
renderTodoOutput now also validates the parsed array at the boundary
(JSON.parse result is filtered to objects whose content and status are
runtime strings), so a single bad row no longer poisons the whole
tool output.
Tests: 12 new unit tests in
packages/ui/src/components/chat/message/parts/__tests__/issue-2011-react-error-31.test.ts
covering the {TODO}-key object path, circular references, and
non-string content/status on parsed todos.
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
* fix(sync): commit first message page before expansion loop (#2084)
loadMessages committed the store only after the full expansion sequence
(50→100→150), so the hydrating skeleton stayed for 3 sequential HTTP
round-trips when a session tail had no user message boundary.
Move the store write (materialize + setState) to happen after the first
fetch. The expansion loop now commits each expanded page incrementally
instead of overwriting a page variable and committing once at the end.
The first commit is gated on hasUserMessage(page.session) || page.complete:
if the tail is assistant-only, deferring to the expansion loop keeps the
skeleton (loading state) instead of rendering an empty chat that looks
like a fresh session. Sessions with a user boundary in the first 50
messages get content after a single round-trip.
* fix(sync): address review nits for #2084
- deferred init uses page.session instead of [] so limit reflects the
real fetched count if the expansion loop is ever a no-op
- both stale branches in commitMessagesToStore return messages: [] for
consistency
- add isStale guard between expansion fetch and commit for defense-in-depth
* fix(sync): always commit prepend-mode pages to store (#2084)
The deferred init path (assistant-only tail) skipped commitMessagesToStore
entirely when options.before was set — prepend mode. The fetched older
messages were never written to the store, silently dropping them.
Gate the deferral on !options.before: prepend mode always commits because
messages are already rendered (no skeleton to protect) and skipping the
store write would lose the fetched page.
---------
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
* fix(chat): preserve chronological message order during history pagination
The baseDisplayMessages dedup loop iterated from tail to head (newest
to oldest), keeping the newer occurrence of each message ID. During
history pagination (prepend mode), the server returns older messages
that may overlap with the current view at the boundary. The tail-first
iteration discarded the older (prepended) duplicate in favor of the
newer (existing) one, breaking chronological ordering.
Change the loop to iterate head to tail (oldest to newest) so the
first occurrence of each time-sortable message ID is preserved. Remove
the now-unnecessary .reverse() call.
Fixes#2088
* test(chat): add dedup logic coverage for baseDisplayMessages
Covers message ID deduplication in baseDisplayMessages useMemo:
- First-occurrence preservation during dedup
- Input order maintenance
- Empty input, single-element, all-same-ID edge cases
- History pagination prepend scenario with overlapping IDs
---------
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Fixes GitHub PR/issue context endpoints returning 404 when working
from a fork because they resolved the repo from origin remote only.
- Pass sourceRepo: status?.repo ?? null to all prContext() calls in
PullRequestSection.tsx (5 call sites)
- Pass sourceRepo: args.pr.sourceRepo ?? null / args.issue.sourceRepo
?? null to NewWorktreeDialog.tsx (3 call sites: prContext, issueGet,
issueComments)
- Add status?.repo to dependency arrays to prevent stale closures
The prStatus endpoint already resolves the correct repo through the
fork network; this change wires it through to the downstream API calls.
Closes#2090
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Replace the destructive refreshRoot() with an incremental refresh that
re-fetches the root and every expanded directory while preserving
childrenByDir. This keeps the file tree expanded instead of collapsing
it to the root on every manual refresh.
Closes#2036
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Adds a surface=desktop URL param to embedded session chat links
Uses the surface override to classify embedded chat as desktop
Covers the new URL parameter in tests
Prevents narrow embedded session chat panels from being misdetected as mobile
Uses the ocPanel=session-chat query parameter to पहचान desktop-like layout
Keeps device detection aligned with other desktop shell runtimes
Android 15 enforces edge-to-edge and ignores the StatusBar overlay:false
inset the app relied on, while every --oc-safe-area-* CSS definition was
gated behind iOS-only conditions. Read the Capacitor-injected
--safe-area-inset-* vars (with env() fallback) on the Android native
shell so the header, top toasts, and connect screen clear the status
bar; both sources report 0 where the native inset still applies.
- --relay links now carry both routes: direct LAN plus relay fallback,
matching the UI's Anywhere pairing; devices prefer the direct route
- pairing sessions created by the CLI are marked with usesRelay, and the
server reconciles relay demand on a timer, so a headless instance
brings the relay up on its own after connect-url --relay
- warn with LAN_UNREACHABLE when the link's direct route points at
loopback and other devices cannot use it
- document the --relay flow and the --lan binding caveat in Connect a
Device and Remote Instances across all locales
The "Add to Context" command and the active-editor pin-selection suggestion both create selection attachments but used the basename only (e.g. assist.ts:47). OpenCode synthesizes its Read call from that filename, so the directory was lost and the model could read or edit the wrong file when names collide.
Use the workspace-relative path (asRelativePath(uri, false)) in both paths so the filename carries the directory and the two paths produce identical filenames, restoring attachment dedup.
Fixes#1914
- new Connect a Device page: one-time QR pairing, transport choices, device management
- new Private Relay page: E2EE guarantees, demand-driven lifecycle, relay vs tunnel
- rewrite mobile page around the native iOS/Android apps (TestFlight + APK)
- update remote-instances, security, tunnels, and remote-access troubleshooting to point at the new pairing flow
- translate everything across all 8 locales and update the sidebar
Tool JSON output now starts with a compact navigable summary view.
Expandable tool output includes quick open-file and diff actions for changed files.
Reasoning headers strip stray HTML comments, and navigation tools stay compact.
Changelog leads with the private relay and the native mobile apps (TestFlight
beta + Android APK links), followed by pairing v2 and the device management,
desktop multi-transport, and chat items; VS Code changelog gets the shared
chat-render entries.
Add Windows launch-at-login with background startup support and extend the
native tray integration to Windows.
Add a Windows-only setting to minimize or close the main window to the
system tray, persist it through desktop settings, and expose it in Settings
search and all locale dictionaries.
Keep tray state synchronized with live sessions on both macOS and Windows,
while preserving the existing macOS behavior.
- A saved host now keeps every transport its pairing link carried: direct URL
plus the relay descriptor, with one token for both (the mobile connection
model). Switching tries the direct leg and falls back to the E2EE tunnel;
list probes report Connected · Relay when only the tunnel reaches the host;
relaunch restore picks direct first
- Host switching trusts the dropdown's fresh probe instead of re-probing on
click (no doubled latency, no transient Unreachable flashes); statuses are
written once with the final outcome, survive the dropdown closing via a
last-known cache, and an unprobed host reads Checking — never Unknown
- Open-in-new-window works for relay hosts: a new IPC command boots the local
UI with the host id injected and the renderer picks the transport; the app
render holds on the relay restore so the splash shows instead of a transient
auth screen (10s safety valve)
- Relay host control socket gained protocol-level keepalive: a missed pong
window terminates and reconnects, so the relay can no longer hold a ghost
registration that leaves every client tunnel hanging; the desktop relay
probe also hard-times-out at 8s instead of hanging status flows
- Services dropdown restyled with mobile-style cards: per-provider usage
cards, per-host instance cards with a selected highlight and a toned
status line, MCP servers grouped in a card
- Each saved server row shows live reachability (Connected · Nms ping /
Unreachable / Auth required) with a status dot, probed once per list change
through the shared HTTP/relay probe (relay probing moved to desktopHosts as
probeRelayDesktopHost, reused by the host switcher)
- Section header: one short description, Import Link promoted to the primary
action; the token-storage note moved into the Add Server dialog next to the
token field it describes, and the dialog got its own description
- Probe relay hosts through a throwaway E2EE tunnel in the host switcher
instead of an HTTP probe against the relay:// pseudo-URL, which always
reported Unreachable
- Show 'via OpenChamber Relay' for relay hosts in the switcher and the
servers list instead of the raw relay:// pseudo-URL; hide the URL-centric
edit action for relay hosts (saving it would drop the tunnel descriptor)
- Pairing LAN candidate prefers the address the requesting client actually
reached the server on; interface scanning could pick an unroutable virtual
bridge (docker0), producing links whose LAN leg silently failed and forced
devices onto the relay
The tanstack rows sit in a wrapper offset with transform: translateY(), and a
transformed ancestor becomes the sticky containing block — turn headers stuck
to the wrapper's overscan-dependent top edge instead of the scroll container,
floating over the previous turn. Offset the wrapper with padding-top instead:
identical geometry, sticky computes against the scroll container again, and
the padding only changes when the virtual window shifts, not per scroll frame.
The compile fix replaced the direct topEdgeEffect API with KVC casting the
effect to UIView — but UIScrollEdgeEffect is not a UIView, so the cast
silently returned nil and the system's dark edge band stayed visible behind
the status bar. Keep the KVC (compiles with pre-26 SDKs) but toggle the
ObjC 'hidden' key on the effect as a plain NSObject, guarded by respondsTo.
The pairing session is single-use, so it leaving the pending list (polled
every 5s) means it was redeemed — close the dialog and toast success. Armed
only after the pairing has been seen in the pending list, so the stale list
at result-phase open can't blink the dialog shut; expired/cancelled sessions
close it silently. Pending-list polling now preserves the previous list on a
transient fetch failure instead of blanking it (which would also have faked
the redeem signal).
- Connect screen leads with Scan QR code plus a plain-words hint of where the
code lives; manual URL entry is collapsed behind Connect by address (expanded
automatically on web where scanning is unavailable); saved connections show a
per-row connecting spinner
- Instances sheet is list-first: the active instance shows a live status dot
and transport (Connected - Local network / Private relay), rows connect on
tap with an inline spinner, and the add/edit form hides behind Scan QR code /
Add by address
- Deleting the last instance returns to the connect screen: without a runtime
endpoint the native app no longer bootstraps against the webview's own origin
(which faked a successful connection), and the connect screen renders
regardless of a stale isConnected flag
Reworks how devices connect to an OpenChamber server, end to end.
Pairing v2:
- One-time pairing links/QR codes (openchamber://connect?v=2) carrying a set of transport candidates (LAN/tunnel/relay) and a single-use secret redeemed server-side; no tokens embedded in links
- Add-a-device dialog written for first-time users: intent-based transport choice (Anywhere / Home network only / This computer only) with plain-language descriptions, transparent fallback checkboxes, server-authoritative LAN detection, high-res QR dialog
- Private relay folded into pairing as a transport candidate with a demand-driven lifecycle (enables when a relay device is paired, disables when none remain)
Multi-transport devices:
- A saved device holds all its transports and one token; mobile re-probes on connect, resume, and network change and hot-switches LAN<->relay seamlessly (no re-pairing, no remount, session preserved)
- Desktop can import relay pairing links, switch to relay hosts through the E2EE tunnel, and restore a relay default host after relaunch
Device management:
- Device list (web + desktop) shows live per-device connectivity with the active transport (Connected - Local network / Relay) and platform badges (iOS/Android/macOS/Windows/Linux)
- One physical device = one record: stable per-install dedupe keys across pairing and password re-login; typed pairing label names the device, paired devices name the connection by the issuing server hostname
- Trusted desktop-local client manages all devices (list, revoke, clear revoked); relay host reaps dead client sockets after 3 missed keepalives
Android:
- LAN transport unblocked (cleartext + mixed content, mirroring iOS ATS exceptions); resume re-probe retries through network flux and silently auto-reconnects from a disconnected state
* feat(chat): migrate history list to @tanstack/react-virtual with deterministic mobile history loading
- Replace virtua with @tanstack/react-virtual for chat history on all
surfaces: bottom anchoring (anchorTo: end), key-stable prepend
preservation, and native iOS touch/momentum deferral live in the core
- Patch virtual-core to clamp the render range to real scroll bounds
during transient adjustments (OpenCode upstream parity)
- Rows render in normal flow inside a translated wrapper so sticky user
headers keep working; measurement snapshots cached per session
- Pre-write container height in scrollToFn so the browser cannot clamp
anchor corrections to the stale height; hold the prepend anchor for up
to 180 frames on mobile while fresh rows settle (cancelled by user
input; desktop relies on core anchoring alone)
- Adaptive row-size estimate from per-session measured averages; disable
reveal fade-in for virtualized history rows
- Mobile loads older history only through an explicit localized top
button: no scroll-position trigger and no post-mount background
prepend, so every insert happens from a resting state; a quiet-window
hold defers any stray prepend commit while a touch gesture is active
- Desktop/VS Code keep the seamless scroll-up trigger and progressive
background prepend
* Share project edit form between settings and sidebar dialog
Extract ProjectIdentityFields and useProjectIdentityForm so the projects
settings page and sidebar Edit dialog share the same layout and behavior.
Rename the project menu action from Rename to Edit, and add per-project
default model selection for new chats with persistence and draft-session
resolution ahead of global defaults.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* Unify project edit UI with shared ProjectIdentityEditor shell
Wrap header, fields, and inline Save changes button in one editor
component used identically by settings projects page and sidebar
dialog. Remove dialog-specific footer, title, and padding so both
surfaces render the same layout.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* Include Actions and Worktree sections in project Edit dialog
Extract ProjectSettingsPanel with the full settings=projects content
(identity, actions, worktree) and render it from both the settings page
and sidebar Edit dialog. Keep the dialog open after identity save so
users can configure actions and worktrees without reopening.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* Narrow project Edit dialog to modal-appropriate width
Use max-w-2xl instead of max-w-4xl so the popup does not inherit the
full settings page width.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* Unify project settings subsections and auto-save all fields
- Add shared ProjectSettingsSubsection with consistent titles and dividers
- Auto-save identity, actions, and worktree setup commands (debounced)
- Remove Save changes and Save Actions buttons
- Split worktree into Worktree and Existing worktrees subsections
- Align controls to shared max width across all subsections
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* Harden project settings auto-save error handling
- Only update worktree setup snapshot after successful save; toast on failure
- Toast when actions auto-save is blocked by validation for >1s
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* Show toast when project identity auto-save fails
Wrap onSave in try/catch and surface settings.projects.page.toast.saveFailed
so rejected parent callbacks are not silently swallowed.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* Fix clearing project default model from settings
Send null instead of undefined when no default model is selected so
updateProjectMeta enters the defaultModel branch and deletes the field.
Apply consistently in prepareSaveData, ProjectsPage, and SessionSidebar.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
PR #1673 added sanitizeHeadersForBrowser in the fetch bridge to handle
non-ISO-8859-1 directory paths by encoding the value and attaching a
x-opencode-directory-encoding: uri header. The server-side decoder was
already in place (08b86613). However, the CORS Access-Control-Allow-Headers
list on the Express server was not updated to include this new header.
When a user opens a directory with CJK or other non-Latin-1 characters
(e.g. D:\文件), the browser sends a CORS preflight OPTIONS request with
x-opencode-directory-encoding in the Access-Control-Request-Headers.
The preflight fails because the server does not list it as allowed,
blocking all subsequent API requests with 'Failed to fetch'.
Add X-OpenCode-Directory-Encoding to the Access-Control-Allow-Headers
response header for openchamber-ui://app packaged client origin.
* fix: open mobile model/agent panels on tablet-width Capacitor shells and keep composer taps from dismissing the keyboard
* feat: add iPadOS-style split layout to the Capacitor app
- classify the Capacitor shell as mobile in device detection so shared
surfaces (draft starters, panels) stop falling into tablet branches
- add isIPadApp() and useOrientation() helpers
- iPad: persistent full-height sessions sidebar (mobile sessions surface
inline), Changes/Files in a right sidebar with header shortcut toggles
- animate sidebar open/close like the desktop sidebars and add
finger-sized drag-resize with persisted widths
- anchor the overflow menu and the usage/metadata popover next to their
header buttons regardless of open sidebars
* fix: re-anchor metadata popover on layout shifts and untangle sidebar toggle updates
- recompute the iPad metadata popover anchor via a ResizeObserver on its
wrapper so sidebar toggles/resizes while it is open cannot leave it
misplaced
- move the portrait right-panel close out of the setIpadSidebarOpen
updater into plain sequential state updates
Avoids syncing code line numbers while markdown is still streaming
Keeps code block wrapping and line numbers stable after render
Updates code block layout to support deferred gutter insertion
Normalizes bare ---/+++ headers and paths before rendering
Repairs loosely formatted hunk bodies for patch display
Recounts hunk ranges so diff headers stay accurate
Adds line-number gutters for markdown code blocks
Keeps gutter heights in sync when wrapping or resizing changes
Applies wrap styles directly to pre and code for better overflow handling
Adds a chat code block wrap toggle in markdown code block headers
Persists and restores the setting across desktop/web settings
Adds localized labels and OpenChamber search entry for the new option
Adds OpenChamber Relay — an opt-in way to reach an instance from a phone,
browser, or another desktop from anywhere, with no open inbound ports, no
tunnel, and no shared LAN. The instance dials outbound to a relay; all app
traffic (HTTP, the event stream, terminal, dictation) is multiplexed and
encrypted through a single connection per client, so the relay only ever
forwards opaque ciphertext.
Transport
- End-to-end-encrypted channel over WebCrypto (ECDH P-256 -> HKDF ->
AES-256-GCM) with a capability-negotiated handshake and a small
HTTP/SSE/WebSocket multiplexing protocol. A byte-compatible JS host mirror
is cross-checked by tests.
- Host: outbound connection manager, per-client tunnel dispatcher to the local
server over loopback, reuse of the existing instance identity key, and
management routes. Disabled by default; explicit opt-in.
- Client: plugs into the existing runtime layer (runtime-fetch/-url/-switch/
-auth, event pipeline, terminal, dictation) so features work over the relay
unchanged; direct-URL and Electron realtime-proxy paths are untouched.
Pairing & UX
- Relay section in Settings -> Remote Instances (live status, QR/link pairing,
revocation via the existing client-token list) and the mobile connect flow.
- Frame batching and idle-gated keepalive keep tunnel message volume low
without affecting streaming smoothness.
Security
- The tunnel is transport only; the server authenticates every tunneled
request exactly as for a direct remote client.
fragments only. The relay stores no keys, tokens, or payloads.
Operability
- The endpoint can be pinned to a self-hosted rel
paired clients inherit it from the offer automatically.
- Relay module DOCUMENTATION.md and a relay-trans
invariants that future WebSocket/streaming changes must follow.
The relay transport is complete and tested; the UI for enabling and pairing
is gated behind openchamber_relay_gate and stays
Keep the browser pane loaded URL separate from the in-frame current URL so SPA navigation updates the address bar and history without remounting the iframe or resetting the Electron webview src.
Preserve parsed ?session= route params during initial URL normalization and pass a directory hint when applying deep links, preventing embedded OpenChamber sessions from collapsing back to / while bootstrap catches up.
Adds a Last turn scope to DiffView that renders OpenCode snapshot diffs from the latest user message summary without re-fetching git contents. The view hides Review in that mode and carries the selected diff scope through main and context-panel navigation.
Connects latest-turn changed-file chips in chat to the snapshot diff view on desktop and mobile, while keeping older turn chips static/read-only to avoid misleading affordances and extra subscriptions. Updates localized labels and empty states plus changelog.
Validation: bun run type-check (packages/ui); bun run lint (packages/ui).
Moves the hidden file input out of the attachment controls so it stays mounted.
Prevents file selections from being lost when the composer variant changes.
Restores reliable local attachment uploads after opening the OS picker.
Keeps mobile composer controls from missing taps during keyboard blur/reflow
Applies the deferred blur behavior to mobile browsers and installed PWAs
Leaves Capacitor behavior unchanged