d71bcb5025bde76287c786720c579e196dd70438
64
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1ae3aeff5f |
feat(chat): structured context attachments with metadata round-trip
Every user-attached context item (diff/file/plan comments, terminal selections, browser annotations, PR comments and failed checks, linked issues/PRs, and new chat-quote comments from the selection menu) is now sent as its own synthetic text part carrying an openchamberContext metadata payload. The model-facing text keeps the previous wording; the timeline reads the metadata back and renders each item as a context card instead of raw prompt text. Legacy messages still render via the old text sniffing. The selection menu gains a Comment option with an inline multiline input, the quoted fragment stays highlighted while commenting, and on mobile the input overlays the composer pill by rendering inside the composer form. Add to chat is renamed Add to input; the menu is restyled and the mobile Copy tile removed. Terminal drafts move their terminal id out of the language field (persisted-draft migration v3), and the dead preview-console source is deleted. |
||
|
|
3feee346da |
fix: replace glass dialog styling with solid background
Dialogs and settings window now use the standard background color Removed dialog from the glass utility styles Preserves existing glass effects for other surfaces |
||
|
|
2c30af5807 |
refactor: remove macOS vibrancy support and unify glass surfaces
Drops the macOS vibrancy toggle, IPC, and related settings copy Updates dialogs, popovers, tooltips, and dropdowns to use shared glass styles Adds sticky header fade behavior to model picker and sidebar lists |
||
|
|
5e6d1f8b01 |
fix(ui): restore rail badge background via surface theme tokens (#2790)
* fix(ui): expose surface.* tokens so bg-surface-muted renders * docs: add PR evidence screenshots for rail badge background |
||
|
|
96c011a8ef |
feat(mobile): tablet layout pass and foldable-ready size class (#2569)
The tablet ran the phone layout with a half-finished iPad draft on top: two custom sidebars, a leftover overflow menu, split Files/Changes header buttons, and phone-width sheets stretched across a 13" screen. This brings it onto the phone's navigation model and keeps only the differences a large screen earns. - Sessions are a persistent resizable left sidebar; the overflow menu is gone and its destinations moved into that sidebar's footer (connected instance, settings, pending web update) and into the workspace drawer. - The workspace (Changes / Files / Terminal / Notes / MCP) is the phone's drawer everywhere: a resizable right sidebar where the screen can host one (up to 900px) and the full-cover drawer otherwise, with its mounted panes — an open diff, an edited file, an attached terminal — surviving rotation. - Header dropdowns are anchored popovers: the recents switcher mirrors the usage overlay on the left, and its trigger is sized to the title rather than to the free width. - App-level pages (settings, instances, update, an opened plan) render as centered dialogs instead of covering the screen. - Overlays center on the chat column through published insets, so the model and directory pickers no longer sit off-centre; the directory picker also stops overriding the shared width clamp. - Wide chat layout applies to mobile surfaces, where a tablet chat column is finally wide enough for the setting to mean anything. The layout gate is a live size class rather than a device check, so Android tablets and foldables are covered by the same code: - `enabled` when the shortest viewport side is at le sw600dp). The short side is what makes this a size question instead of a device question — a phone reports ~360-430 whichev unfolded book foldable ~600+, and folding shut drops back under it. iPads also answer on identity, since iPadOS hands out od - `roomyForPanels` when landscape and at least 1000px wide, which is what it takes to host the sidebar, the panel and a readabl foldables miss it in BOTH orientations — their long side is barely wider than a tablet's short one — so they keep the portr Every consumer re-decides instead of remembering wha open sidebar closes if the device folds shut under it. iPad behaviour is unchanged: its landscape widths all clear the panel ones do not, exactly as the previous orientation check did. Hardware keyboards are read natively. iOS reports them through GCKeyboard, published to the web layer at document start and kep disconnect and foregrounding; the layer stops inferring once that answers. A single early publish was not enough — the connect no already-attached keyboard fires before the page exists, and GameController can populate late — so the state is re-published across resume. With a keyboard attached the draft screen keeps its starter chips and the composer never collapses; tablets skip the colla Runtimes with no native answer fall back to inferring it from the keyboard bridge, and only ever conclude "hardware" from silen Also: sidebar rows no longer sit on a differently ti footer is no longer clipped by an over-tall content box, the resize handles moved above the panes' own overlays so they can actu now-unreachable overflow menu, fullscreen terminal/MCP/notes surfaces and their locale key are deleted. Device behaviour is unverified — the tablet layout, keyboard bridge and the foldable size class have not been exercised on hardware, and the 600/1000 thresholds are derived fr rather than measured on a foldable. |
||
|
|
86ef96302d |
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. |
||
|
|
8130381f6c |
fix(ui): smooth desktop sidebar sticky transitions (#2528)
* fix(ui): blend vibrant sidebar transitions * fix(ui): fade sidebar content below header * fix(ui): smooth vibrant sidebar sticky headers * fix(ui): scope sticky fade to desktop * fix(ui): respect sticky header preference |
||
|
|
83a66d0a6d |
feat(theme): add OpenChamber hybrid theme and make it the default
- Add OpenChamber brand theme (dark+light): Vitesse syntax over OpenCode-deep neutrals with Flexoki accents, harmonized in OKLCH - Add scripts/harmonize-theme.mjs: align accent saturation/lightness and boost syntax chroma via OKLCH - Set default theme to OpenChamber (was Flexoki) - Rename fields-of-the-shire off the openchamber-* id; keep it as a normal preset - Remove OpenCode and OC-2 ports and all disabled theme JSON definitions - Make panel/header/sidebar borders opaque and theme-driven; global default border now uses --border instead of hardcoded rgba |
||
|
|
1fd51745c7 | fix(ui): avoid vibrancy backing on unstuck project headers (#2526) | ||
|
|
1291cde5c2 |
feat(ui): sidebar redesign — project zones, grouping modes, full-page surfaces (#2480)
* checkpoint: flatten sidebar core (flat sessions, zones, single mode, folders flat) * checkpoint: sidebar nav + scheduled/archive full-page surfaces, recent zone header * checkpoint: worktrees management surface via project menu * checkpoint: docs, i18n, validation for sidebar redesign * checkpoint: unified row/zone geometry, recent backfill, tooltips everywhere, primary spinner * checkpoint: branch icon marker, date moved to rich tooltip, recent back to pure time window * checkpoint: PR state on branch markers + tooltip, no reserved right space, aligned show-more, instant tooltips * checkpoint: reserve hover-action space so title text is not overlapped * checkpoint: tighten hover-action reserve * checkpoint: color-only unread emphasis to avoid title reflow * checkpoint: drop new-subfolder action, folder actions overlay on hover * checkpoint: fix sticky project headers (sticky on trigger div), stuck elevation * checkpoint: full-bleed semibold zone headers, no top scroll fade * checkpoint: headers without background tint (typography-only emphasis) * checkpoint: recent header flush with scroll top (no pre-stick bump) * checkpoint: shared tooltip provider with grouping (instant handoff between rows) * checkpoint: blur pointer-click focus so hover chrome hides on mouse-leave * checkpoint: tooltip closeDelay bridges inter-row gap * checkpoint: nav above controls, merged view dropdown, project-scoped bulk selection, cross-worktree folders * checkpoint: folder header tooltip with full path name * checkpoint: true page surfaces (hidden chat, header title, close-on-select), multirun page, run-now jump, folders on top, controls row polish * checkpoint: rename mode — dual-instance outside-click fix, no vertical shift * checkpoint: session grouping mode toggle (by-worktree default, flat option) * checkpoint: worktree header — hover padding reserve + delete worktree action * checkpoint: frosted sticky header backing under desktop vibrancy * checkpoint: align worktree sub-header with project header icon column * checkpoint: restore worktree group DnD reorder; dense vibrancy header tint (Chromium mask+backdrop-filter) * checkpoint: vibrancy — drop scroller mask so backdrop-filter samples rows (Chromium backdrop-root limitation) * checkpoint: vibrancy headers use opaque sidebar tone (Electron transparent-window backdrop-filter bug) * checkpoint: nav collapsed to one row (New session + surface icons), mirrors Add project row * checkpoint: raise sticky zone headers above row action layers (z-20) * checkpoint: New session as full-width CTA + single quiet toolbar row * checkpoint: New session row back to quiet text form above the toolbar * checkpoint: align toolbar left icon with New session icon column * checkpoint: hoverless flat header controls (color-only hover states) * checkpoint: sticky project headers toggle in view dropdown (default on) * checkpoint: full-page surfaces adapted — archive directory filter panel, scheduled master-detail, multirun without duplicate title bar * checkpoint: multirun joins mutually exclusive surface set * checkpoint: surfaces leave via navigation only (no close/cancel buttons), robust close-on-new-session, drop dead MultiRunWindow * checkpoint: no scheduled header description, aligned empty-worktree note, collapse/expand covers worktree groups * checkpoint: overlay scrollbar above sticky zone headers * checkpoint: archive delete icons reveal on hover with padding shift * checkpoint: new worktrees surface at top of the worktree list * checkpoint: no grab cursor on worktree headers * checkpoint: worktrees page list-only with inline action, no worktrees in edit dialog, drop menu ellipsis * checkpoint: worktrees page uses full content width * checkpoint: worktrees header — 'in' instead of em dash, no description * checkpoint: drop legacy OpenCode badge from worktree list, tooltip without OpenCode mention * review: bound pr summary cache |
||
|
|
005b2e61b0 |
Composer: CodeMirror editor, unified prompt language, ChatInput decomposition (#2419)
* refactor(ui): unify composer @mention grammar
Extract the composer's @mention rule into composer/language/mentions.ts and
route highlighting, send-time extraction, backspace-deletes-the-mention and
the file-path check through it. The rule previously lived as four separate
regexes in ChatInput.tsx with divergent cleanup, so every new reference type
had to be taught to all four.
Bracket handling is now symmetric: [ and { were accepted before @ but ] and }
were not stripped from the tail, so [@plan] resolved to the name 'plan]'.
Add characterization tests for the markdown tokenizer, which had none, to
pin current behavior ahead of the editor migration.
* refactor(ui): unify composer slash, snippet and trigger grammar
Extract /skill, /command and #snippet scanning into
composer/language/prefixTokens.ts, and the rule deciding which autocomplete a
caret asks for into composer/language/triggers.ts.
Each sigil previously had three separate implementations: one for
highlighting, one for send-time collection, and one for opening the picker.
They disagreed on the valid character set — the send-time skill scanner
accepted only lowercase names, so a /My_Skill token was painted as a command
but never collected. Scanning is now generous and membership in the command,
skill or snippet registry is the authority.
resolveAutocompleteTrigger replaces the 90-line branch chain in
updateAutocompleteState with a pure function, keeping the previous
command > skill > snippet > mention precedence.
* refactor(ui): tokenize the composer in a single pass
Add composer/language/tokenize.ts as the one entry point producing every
highlight range from the text plus what the composer knows about the
workspace. It replaces six independent memos in ChatInput.tsx that each
re-scanned the same string for markdown, fenced code, mentions, slash tokens,
snippet tokens and attachment citations.
Mentions now distinguish the reference span from the raw token: 'see @a/b.ts,'
highlights @a/b.ts and leaves the comma as sentence punctuation, while
backspace still deletes the whole token so a wrapping bracket is not orphaned.
* feat(ui): add the CodeMirror composer editor
Add composer/editor: a CodeMirror view that renders the prompt language as
mark decorations, and a controlled React primitive around it.
The composer previously painted a transparent textarea over a mirror div,
which restricted highlighting to styles that do not change glyph advance
width -- so bold and italic were impossible and the overlay was disabled
outright on mobile, where wrapped text drifted from the caret anyway.
CodeMirror owns the text and the caret together, removing the second layer.
The document stays a plain string, so nothing downstream has to serialize a
rich model back into a prompt.
Split resolveHighlightSegments out of buildHighlightParts so the mirror
overlay and the editor decorations share one priority resolution.
Not yet wired into ChatInput. Editor rendering is unverified: the package has
no DOM test environment, so only the state-level extension is covered.
* refactor(ui): move the composer onto the CodeMirror editor
Replace the transparent-textarea-over-mirror-div composer with ComposerEditor.
The mirror is gone, and with it the constraint that highlighting may only use
styles which do not change glyph advance width, and the mobile carve-out that
disabled highlighting entirely because wrapped text drifted from the caret.
Removals the editor makes unnecessary:
- measureCaretInTextarea, 46 lines of hand-built text mirroring for popup
placement, replaced by the editor reporting caret coordinates
- adjustTextareaHeight and its two layout effects, replaced by the editor
sizing itself; the dictation transcript height is now an explicit floor
- getInsertedTextFromChange, which diffed old against new text to recover what
a paste inserted; the editor reports the change directly
- the highlight mirror element, its scroll-sync and its parts memo
- a _commandMetadata property stashed on the textarea and never read
Caret placement no longer needs a requestAnimationFrame after a text edit:
text and selection travel in one transaction.
Runtime behavior is unverified -- the package has no DOM test environment.
Type-check, lint, the 295 chat tests and a production web build pass.
* refactor(ui): extract composer text and path helpers
Move the composer's text-splicing rules to composer/text.ts and its path
handling to composer/attachments/filePaths.ts, with tests. None of this logic
was covered before, despite handling VS Code drop payloads, percent-encoded
file URIs and Windows drive letters.
normalizeDroppedPath and toProjectRelativeMentionPath were useCallbacks that
closed over nothing but their argument and the search directory; they are now
plain functions taking the root explicitly.
* refactor(ui): make the composer's slash commands a table
Nine of the composer's local commands did the same thing -- render a visible
magic prompt plus synthetic instructions and send them as one message -- and
that shape was written out nine times as an else-if chain. Adding a command
meant copying twenty lines and remembering to change every string.
composer/submit/slashCommands.ts holds the commands as data and the shape as
one executor; ChatInput keeps only the five that manipulate session state or
open UI. The command names, prompts and failure toasts are now typed against
the magic-prompt and i18n key unions, so a mistyped key fails to compile
rather than failing at runtime.
Net: 257 lines of branching become 68.
* refactor(ui): move the composer's footer components out of ChatInput
RevertedMessageDock, ComposerAttachmentControls, PermissionAutoAcceptButton,
FocusModeButton and ComposerActionButtons each own a piece of the composer
chrome and were defined inline above the 4500-line component. They now live
under composer/ui with the imports they actually need.
Pure moves; getRevertedPreview travels with the dock, its only caller.
* refactor(ui): extract composer draft persistence
Move the draft lifecycle -- identity switching, debounced writes, external
deletion, and the flush-on-hide/freeze/pagehide edges -- into
composer/state/useComposerDraft.
It was seven interleaved effects and five refs sharing state through the
component body, which made the ordering constraints between them (skip the
next debounced write while restoring; record the empty signature before a
queued write can resurrect a deleted draft) invisible. They are now stated
where they apply.
* refactor(ui): extract drop payload inspection
hasDraggedFiles, collectDroppedFiles and collectDroppedFileUris were
useCallbacks with empty dependency arrays -- pure DataTransfer readers wearing
React clothing. They move to composer/attachments/dataTransfer.ts with tests
covering the host differences they exist for: browser File lists, VS Code's
proprietary tree types, OpenChamber's own internal-drag marker, and getData
throwing during dragover.
* refactor(ui): extract the mobile composer shell
Move the pill state machine into composer/state/useMobileComposerShell and
the visual-viewport pinning into composer/state/useMobileViewportPin.
Between them they held eight refs, four pieces of state and eleven effects
interleaved with the rest of the composer, which hid what they are: not one
state machine but a state machine plus a set of corrections for specific
platform behaviors -- mobile browsers dismissing the keyboard before a tap's
click lands, iOS refusing programmatic focus outside a gesture, WebKit leaving
the layout viewport panned after the keyboard hides, overlay chains handing
off through a frame where nothing is open.
Verbatim moves: every timeout, flushSync and guard keeps its value and its
reason, because none of them is verifiable outside a real device.
* refactor(ui): extract outgoing message assembly
A single send can carry queued messages, the composer text, inline review
comments, resolved @file attachments, a linked issue or PR, synthetic parts
from conflict resolution, and a skills instruction -- all flattened into
OpenCode's one-primary-plus-parts shape. The flattening rules were spread
through handleSubmit with no coverage, so the ordering they encode (oldest
queued message becomes primary; inline comments attach to the last authored
body, not a new part; PR instructions precede the diff) was trusted rather
than checked.
buildOutgoingMessage is a pure function over injected resolvers, with 25 tests
covering that ordering.
* refactor(ui): extract new-session draft targeting
Move project and worktree selection for the new-session draft into
composer/state/useDraftTarget.
The rule worth naming is that the draft can point at a directory that does not
exist yet -- a worktree still being created. It has to survive not appearing
in the branch list, or the selector snaps back to the project root mid-creation
and the session starts in the wrong place.
* refactor(ui): extract composer context chips and linked reference rows
The chips standing for attached-but-not-typed context (review comments, dev
server logs, preview annotations, terminal selections) were four near-identical
inline blocks; three of them collapse into one CountChip.
The linked issue and linked PR rows were 100 lines of duplicated markup
differing only in their number label, their branch line and which picker they
reopen. They are now one LinkedReferenceRow.
* refactor(ui): extract draft target selectors
Move the project and branch pickers into composer/ui/DraftTargetSelectors:
inline selects for desktop, trigger buttons and bottom sheets for mobile, all
rendering the same options from useDraftTarget.
The project label -- custom icon image, configured icon, or a folder fallback,
with the project name -- was a useCallback rendering JSX and used from four
places; it is now a ProjectLabel component.
* refactor(ui): extract the collapsed mobile pill composer
Move the pill into composer/ui/MobilePillComposer. Both places that ask
dictation to start now go through one toggleDictation callback rather than
dispatching the global event inline.
* refactor(ui): extract the composer footer
Move the footer row into composer/ui/ComposerFooter, which now owns the
desktop and mobile layouts and the components they place. ChatInput keeps only
the handlers it passes in.
* refactor(ui): collapse the composer's four autocomplete states into one
The composer tracked each picker with its own show flag and query string,
which encoded 'exactly one is open' as four booleans that had to be kept
mutually exclusive by hand. It is now one openAutocomplete kind plus one
query, which is what resolveAutocompleteTrigger already returns.
The four popup blocks -- identical apart from their component and caret width
-- become ComposerAutocompletePopups.
* refactor(ui): extract autocomplete positioning and message history
useAutocompletePosition owns caret-relative popup placement, which only
applies in focus mode.
useMessageHistory owns arrow-key recall. Its transitions are pure functions
with tests: entering history stashes the draft exactly once, so walking back
several messages and returning still restores what the user actually typed
rather than the last recalled message.
* docs: document the composer module
Record what each layer owns and the invariants that are not visible from the
code: that the prompt language is the single source of truth for syntax, which
ordering rules in the submit assembly and draft lifecycle are load-bearing,
that the mobile hooks are platform corrections rather than state machines, and
that rendering, focus, keyboard and WKWebView behavior are not covered by
tests and must be verified by hand.
* fix(ui): restore the composer caret colour and click-to-focus
Two regressions from the editor migration.
The caret rendered black in dark themes. CodeMirror's base theme hard-codes it
through '.cm-editor.cm-light .cm-content', one class more specific than the
plain '.cm-content' rule the composer theme used, so the base theme won.
Matching that specificity with '&.cm-editor' fixes both variants.
Clicking the composer's empty space no longer focused it. A textarea filled
its box, so the browser placed the caret for any click inside it; CodeMirror's
content element covers only the text. The content box now stretches to the
full editor height, and clicks landing outside it — in the composer's padding
— are forwarded to the nearest text position.
The theme moves to its own module with a test that installs it. EditorView.theme
compiles selectors at import and throws on scopes it was not given, including
'&light' and '&dark'; neither the build nor the type-check catches that, and
the failure takes the whole composer down at runtime.
* fix(ui): colour the composer caret where it is actually drawn
The previous fix styled caret-color, which drawSelection() overrides with
'transparent !important' at the highest precedence -- it hides the native
caret and draws its own .cm-cursor element, whose base style is a hard-coded
'border-left: 1.2px solid black'. So the caret stayed black on dark themes.
CodeMirror recolours that cursor only for editors that declare themselves
dark. OpenChamber themes are not merely light or dark, so the cursor takes the
surface foreground directly instead.
The theme spec is exported and asserted against: the caret rule must target
.cm-cursor, must not style caret-color, and must carry enough specificity to
beat CodeMirror's own &dark override.
* feat(ui): add emphasis, attention and path highlighting to the composer
The constructs the editor migration was for.
- **bold** and *italic* render as real weight and slant. They are additive
styles: a segment carries one class string, so choosing between weight and
colour would lose one of them -- bold inside a heading now keeps the heading
colour and gains weight.
- '!!! ' marks an attention line. Three marks, so a sentence ending in '!!'
is not swallowed.
- '~path' highlights a path without attaching it, unlike '@path'. Inert by
design: it feeds neither the autocomplete nor the send path.
False positives are excluded positionally rather than by character, since
these delimiters are ordinary prose: '2 * 3' and 'foo_bar' are not emphasis,
'~approximately' and '~1.2 seconds' are not paths.
Also fix the expanded composer, which kept the collapsed composer's eight-line
height cap: the editor scrolled inside an invisible window while the rest of
the surface sat empty.
* fix(ui): style the composer's selection instead of leaving CodeMirror's
Selecting text rendered it in CodeMirror's stock lavender, which buried the
token colours. drawSelection() paints its own layer and CodeMirror styles the
focused case through a six-class selector; the composer's rule was three deep
and lost.
The tint is translucent rather than the flat selection token: an opaque
selection hides the colours the composer exists to show, and selecting text
here is for moving it, not for stopping reading it.
Same failure shape as the caret, so the theme test now covers both.
* fix(ui): mute the composer placeholder
The placeholder rendered at full text brightness. Its colour referenced
--surface-mutedForeground, but the theme emits --surface-muted-foreground:
an unknown custom property makes the declaration invalid rather than falling
back, and since color inherits, the placeholder simply took the editor's text
colour while the source looked correct.
The theme test now rejects camelCased tokens outright, since this failure is
invisible in every check that does not render.
* fix(ui): create the composer editor before the expand gesture ends
The mobile pill expands with flushSync and focuses the editor on the very next
line, still inside the tap's call stack, because that is the only way a mobile
browser raises the keyboard. The EditorView was created in a passive effect,
which flushSync makes no promise about — so at the moment focus() was called
there was no view to focus.
With a textarea the element existed as soon as flushSync returned, which is
why this worked before the migration.
Creating the view in a layout effect restores that ordering.
* perf(ui): keep the composer editor alive across the mobile pill swap
The pill and the full composer are different subtrees, so expanding or
collapsing unmounted and rebuilt the editor. With a textarea that was one DOM
node. A CodeMirror view is extensions, state, document, decorations and a
first measure — all inside the tap's flushSync, before the browser is allowed
to paint the swap. The shape change therefore landed late enough to look
driven by the keyboard rather than by the tap.
The view now lives in a store owned by ChatInput and is detached and
re-attached instead of destroyed and rebuilt. Its extensions read callbacks
through a ref held by the store, so a kept view always calls into the mounted
instance; compartments move to module scope, since per-instance ones would be
unknown to a reused view's configuration.
Also restore the caret hold: WKWebView draws the caret as a native layer that
ignores CSS transforms and visibly flies across the screen during the keyboard
slide. The rule hiding it targeted textarea and input, which the composer is
no longer — and its caret is now a drawn .cm-cursor element rather than the
native one.
* debug(ui): on-screen timeline for the mobile composer swap
TEMPORARY, Capacitor-only. Two plausible fixes for the swap lagging behind
the keyboard changed nothing, so the theory behind them was wrong. This
overlay draws the event timeline straight onto the screen -- the tap, the
committed swap, the first paints after it, the keyboard choreography, and
whether the editor was created or re-attached -- so one screenshot replaces
guessing. It doubles as an asset-freshness check: no overlay means the app
runs a bundle from before this commit.
* fix(ui): put the composer swap on glass before the keyboard moves
The overlay timelines settled it. The swap itself was never slow: commit in
12ms, editor re-attach in 3ms, focus immediate. What lagged was presentation:
WKWebView stops presenting web frames the moment focus starts the keyboard
transition and holds the last presented frame until it ends. Focusing in the
same task as the swap meant the last presented frame still showed the pill —
paint-1 fired at 29ms, the next frame at 190ms, exactly when the keyboard
was already moving.
On expand, Capacitor now waits two frames before focusing, so the swapped
composer is presented first and the keyboard rises under it. The Capacitor
WebView raises the keyboard for a focus() outside the gesture task; mobile
browsers do not, so they keep the synchronous path.
The collapse direction had a genuine race, caught on one screenshot: the
oc:keyboard-intent collapse arrives a few milliseconds after blur on a
setTimeout(0), and React's scheduling of setFocused(false) can lose to it —
busyRef stays stale, the intent handler skips the instant collapse, and the
pill appears via the 250ms fallback, 370ms after the keyboard has gone.
The Capacitor blur branch now commits the state with flushSync.
The diagnostic overlay stays in until this is confirmed on device.
* fix(ui): raise the keyboard from the swap's first frame
Two frames of delay before focusing made expand visibly sequential: swap,
then keyboard. Focusing inside the first frame after the commit puts the
swap's frame into the rendering pipeline before the keyboard transaction
starts, so the keyboard rises from the tap and the composer appears during
the rise rather than after it.
* fix(ui): restructure the draft screen in the same frame as the pill swap
The draft screen centers its title over the space the composer leaves, and
its starter chips leave when the keyboard is up. The chips were keyed on
oc-keyboard-open, which lands with the keyboardWillShow bridge event ~100ms
after the tap — so expanding the composer restructured the page twice: once
at the swap (composer grows, title re-centers) and again mid-keyboard-rise
(chips vanish, title re-centers again). Chat has no centered content, which
is why it was already smooth and the draft screen was not.
A root class now announces the expanded composer from a layout effect, in the
same frame as the swap, and the chips key on it: one restructure, fused with
the pill morph, before the keyboard moves. The keyboard classes remain as
fallbacks for keyboard-up states that do not go through the pill.
* debug(ui): remove the mobile swap timeline overlay
The diagnostic did its job: it identified WKWebView's presentation pause
during keyboard transitions, the React-scheduling race in the collapse path,
and the draft screen's double restructure — all fixed and confirmed on
device.
* feat(ui): grow the mobile composer with content, drop the fullscreen handle
The swipe-up handle promised a fullscreen composer but the normal eight-line
cap already reached within a line of the same height, so the gesture bought
almost nothing and cost a 28px bar above the editor.
The composer now just grows with what is typed: a generous line cap plus a
CSS ceiling of the space the keyboard actually leaves, whichever is smaller
(the editor cap accepts both and takes min()). The handle, its swipe
gestures and the shell's touch plumbing are gone.
* fix(ui): measure the mobile composer ceiling instead of estimating it
The 220px chrome constant guessed at what surrounds the editor. The old
fullscreen handle guessed at nothing — it let flex distribute real space (and
on Capacitor even that silently failed: its h-full resolved against a
shrink-wrap parent, which is why the gesture bought almost nothing).
The ceiling is now measured the way the handle meant to: the screen container
is marked data-composer-bound, and the editor may grow until the composer
fills it — chrome around the editor read live from the DOM, so attachment
chips, the model row and keyboard resizes all shift the cap by themselves.
* fix(ui): keep a 4px gap between the grown composer and the header
On the chat screen the fully grown composer's border landed exactly on the
header's bottom edge. The gap is a visual design choice, not another chrome
estimate: the ceiling itself stays measured.
* fix(ui): show iOS selection handles without giving up typing speed
iOS pins its selection drag handles to the visible native selection and
colours them from the caret, while drawSelection() hides both. Removing
drawSelection(), or leaving the native caret visible while typing, both
make iOS answer every keystroke with severe input lag. Touch devices now
keep drawSelection() and layer a theme over it that re-shows the native
selection, plus an .oc-native-range marker that enables the native caret
only while a range is selected — when there is no caret to lag on.
* feat(ui): make file mentions editable instead of atomic-delete
Deleting a character inside an @file mention edited nothing and erased the
whole token. Mentions now edit like /skill tokens: a deletion changes the
text and the caret position reopens the file picker on its own. findMentionAt
and MentionToken.rawEnd existed only for the atomic delete and are removed.
* fix(ui): composer selection visibility and external-insert caret
Selection was nearly invisible for two reasons: the tint was mixed down from
--interactive-selection, which themes define with its own alpha (often under
10%); and the painted selection layer sits behind the content, so tokens with
their own background (inline code, fences) covered it entirely. The native
selection now shows on every device, not only touch — it paints over token
backgrounds — and its tint comes from --primary at 25%, a full-strength
colour in every theme. drawSelection() and the range-scoped native caret stay
exactly as before, so typing keeps the lag-free path.
External rewrites (add-to-chat, draft restore, history, dictation) also left
the caret at its old position, so the next insertion landed inside the
previous one. They now put the caret at the end, as the old textarea did, and
pin the scroller to the bottom once the layout settles — a transaction-time
scrollIntoView fires before the max-height cap exists and scrolls nothing.
* fix(ui): render ***triple emphasis*** as bold italic
The emphasis tokenizer capped delimiter runs at two characters, so ***x***
parsed as a stray asterisk plus an italic span. Runs of three now emit both a
strong and an emphasis range over the same content; the two are additive
styles, so they compose into bold italic.
* fix: insert mentions through editor dispatch
Places the caret immediately after an inserted mention
Avoids rewriting the whole message and jumping the scroll to the bottom
Falls back to appending inline text when no editor is available
|
||
|
|
b7a5f44b7c | fix: round meta typography line-heights to whole pixels to stop icon jitter | ||
|
|
17f1b24709 |
Standardize Settings layout and save feedback (#2122)
* Group settings navigation menu Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Revert "Group settings navigation menu" This reverts commit 5983a4e82074b8dab1084af1cadd803ba28ea65d. * Standardize settings layout feedback Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Fix settings save status timer typing Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Retain settings save status Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Report color mode save state Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Match Appearance settings to two-column layout Rebuild Appearance into Color mode & Theme, Localization, and Density & type sections with responsive two-column grids, consistent section headers, page description, and green save status. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Migrate settings pages to SettingsPageLayout and SettingsSection Replace ScrollableOverlay/max-w-3xl shells with the shared settings layout primitives across entity and static settings pages, normalize section headers, and add settings.page.behavior.description locales. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Unify remaining settings pages on shared section chrome Wire Appearance, Projects, and Remote Instances through SettingsSection/SettingsPageLayout so every settings surface shares the same header, divider, and page shell treatment. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Align settings UI with OpenChamber design system kit Apply kit geometry and control specs: 840px content width, 32/48 padding, fixed 260/280 sidebars, radius/spacing tokens, settings select height, stepper dimensions, and shared field/link typography. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Migrate OpenChamberVisualSettings to shared settings primitives Replace ad-hoc radio/checkbox/chip/field layouts with SettingsSection shared chrome for Appearance, Density, Navigation, Chat/behavior, and Privacy while preserving handlers and data-settings-item anchors. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Migrate settings pages to shared field/checkbox/radio primitives Unify Defaults, Notifications, Behavior, Git, Session Retention, Passkeys, OpenCode CLI, Commands, About, Keyboard Shortcuts, and Desktop Network on SettingsFieldRow / SettingsCheckboxRow / SettingsRadioGroup / SettingsChipGroup for consistent grid, spacing, and DRY layout. Also remove the GitPage double SettingsSection wrap around GitHubSettings. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Unify remaining settings pages onto shared field primitives Migrate Agents, Snippets, Skills, Usage, MCP identity rows and selects to SettingsFieldRow/CheckboxRow/ChipGroup and SETTINGS_SELECT_*; align page titles; light-touch Voice/Tunnel/Providers/Plugins without rewriting complex OAuth, permissions, or tunnel flows. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Polish settings section dividers and transport helper text Slightly stronger section borders for clearer group separation, and keep message-stream transport description under the chip control. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Fix settings grid alignment, control heights, and Chat section titles Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Tighten settings grid: flat Chat 2x2 and full-width stacked selects Message options use a flat two-column grid so row headers share a baseline. Stacked selects fill their column; field-row selects keep a fixed sm:w-56 width. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Align mixed settings controls to shared FieldRow/CheckboxRow grid Separate FieldRows from CheckboxRows with SettingsInset, move enum radios into ControlGroups, and convert misplaced StackedFields to full-width FieldRows so left edges no longer clash. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Translate Behavior response-style preset labels for es and pl Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Group settings nav into categories and improve icons/order Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Fix settings layout: fewer hrs, shared control widths, chat transport section Remove SettingsInset top borders, align App install/Density controls to full cluster width, give Message Stream Transport its own Chat section, and fold Sessions Small Model into the first section to cut extra dividers. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Align Voice and MCP settings labels with shared heading classes Swap form field labels to SETTINGS_FIELD_LABEL_CLASS and use SettingsGroupTitle for MCP control-group headings (manual auth fallback, request headers). Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Differentiate settings heading levels by context Page titles are larger and quieter than section titles; group and field labels use dedicated shared classes so hierarchy is consistent across settings surfaces without ad-hoc typography mixes. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Use shared settings title classes in SettingsView home Wire home and unavailable headings through the shared L1/L2 class constants so they stay aligned with SettingsPageLayout. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Keep settings number steppers from stretching +/- buttons Density & type NumberInputs no longer flex-grow across the row, and NumberInput locks minus/plus to fixed width so the plus side cannot inflate when the control is placed in a full-width cluster. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Place spacing and input-bar offset on their own row Density & type now lays out as font families, then font sizes, then Spacing Density / Input Bar Offset on the row below. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Regroup settings nav and improve Voice layout Drop Advanced/Usage/Git nav categories: Voice and About under Interface, Usage under OpenCode, Git under Workspace. Voice provider chips and STT model cards use shared settings primitives with roomier spacing and a two-column model grid. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> * Space out chat feature groups in visual settings * Polish settings: save-state wiring, container-query layouts, unified dropdown triggers - Wire shared save indicator into Behavior page saves and git identity CRUD - Convert settings layout primitives and page rows from viewport to container queries so narrow panes stack instead of clipping - Unify custom dropdown triggers (model/agent/provider pickers) and remaining page selects on the settings control size - Bump global radius scale by 1px; align variant input with select size - Migrate stray raw controls (MCP OAuth checkbox, worktree remove button, git identity dialog rows, skills dialog labels) onto shared primitives - Make settings nav items span full sidebar width; trim nav to 240px * Add General settings page, regroup nav, cap control widths, promote chat feature headers * Mobile settings nav: plain background and touch-sized rows * Hide secondary settings descriptions behind clickable info hints * Move quota credentials to Usage, navigation settings to General, rename External Tunnel * Quiet settings save indicator: silent success, delayed spinner, visible errors * Rewrite settings-ui-patterns skill around shared primitives and refactored conventions * Remove settings starter page; open last visited page, defaulting to General * Settings polish: spacing, control sizes, trigger widths, readable model names, device dates * Centralize dropdown trigger chrome, settings nav polish, mobile-only input bar offset * Fix global line-height regression, auto-hide first-section divider, shortcut row spacing * Global line-height 1.45, align scheduled tasks header button with select * Hide editor toolbar and About in VS Code, animate chat render preview outside desktop dialog * Rebuild agent tool permissions on source-of-truth model Edit the agent's own permission map verbatim (inherit vs explicit actions, pattern rules only for pattern-capable keys), save permission-only, drop the server-side non-wildcard re-merge that resurrected deleted rules, and surface session-granted rules as read-only. * Agents model parameters polish: row spacing, variant dropdown, unified widths, dash for unset numbers --------- Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
22d5ad3814 |
fix: mobile goal UX — bottom-sheet dialog, keyboard-safe target button, capacitor safe-top for overlays
- the goal dialog renders as the shared MobileOverlayPanel bottom sheet on mobile instead of a centered dialog - tapping the target button to ARM keeps the soft keyboard open (the next message is the objective; same guard as the attachment/mic buttons), while opening the manage sheet lets the keyboard close as usual - capacitor: bottom-sheet overlays cap their height by the keyboard inset AND the top safe area — with the keyboard raised while typing inside a sheet, 100dvh does not shrink (native resize is off) and the panel could slide under the notch/status bar; applies to every MobileOverlayPanel |
||
|
|
33cbd0b836 |
fix(mobile): pad Android app below the status bar on Android 15
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. |
||
|
|
24c40b4930 |
fix: iOS PWA safe areas, viewport stability, and keyboard/overlay choreography
- Add standalone-only safe-area padding for the composer (bottom floor + fullscreen top inset) and top toast offset; env() reports 0 on iOS 26 standalone so a fixed floor is required - Pin the mobile shell to 100lvh: WebKit leaves 100dvh stuck at the keyboard-shrunk value after dismissal - Clamp visual-viewport pinning to documentElement.clientHeight to guard against stale visualViewport metrics - Defer the composer blur flip (120ms) so taps on composer controls survive the keyboard-resize reflow; transition the bottom padding so the late flip reads as a slide, not a dip - Restore the keyboard after mobile overlays close: MobileOverlayPanel dispatches synchronous open/close events, ChatInput refocuses within the same gesture, holds focus through iOS's tap-settle dismissal, guards the pill collapse via DOM focus, and reveals the composer form above the keyboard (programmatic focus skips iOS's native reveal) |
||
|
|
b09e073e86 |
fix(mobile): browser composer keyboard handling on chat and draft screens
Mobile browsers don't shrink the layout for the keyboard, so the fullscreen composer is now pinned to the visual viewport (fixed at its offset and height, tracked as the browser pans) instead of overflowing underneath it; the draft screen's normal composer gets the same pinning anchored to the visible bottom via a rAF tracker, since Safari's own focused-field reveal proved unreliable there after leaving fullscreen. The draft and empty-session roots drop their transform-gpu (a transform would make them the containing block for the pinned form), the app header hides while the browser fullscreen composer is up (the form can't out-stack it from inside the composer wrapper's stacking context), and leaving fullscreen nudges the still-focused field back into view. Draft starter chips now hide while the keyboard is open in browsers too, via an oc-browser-keyboard-open root class driven by composer focus. |
||
|
|
2c220f3c52 |
perf(mobile): constant-viewport chat scroller across keyboard transitions
Transforms on the chat scroller (content or container) forced WebKit to rebuild its composited scrolling layers — a multi-second stall on long chats. The scroller now gets no transforms at all: it keeps a constant client height by extending below its shrunken region (by keyboard minus the safe inset the shell gives up, so the settle snap is geometry-neutral) and converting the keyboard strip into its own bottom padding, driven by --oc-kb-scroll-inset from the very start of the rise. Nothing resizes for the virtualizer, every reachable row stays mounted, and open/close is a single cheap scrollTop write: the re-pin happens as the keyboard starts rising, and the hide clamp lands behind the still-visible keyboard. The composer and draft title keep sliding with the keyboard, now via inline transforms set by the choreography — WebKit does not reliably start transitions when a transform changes through a CSS custom property, which had parked the composer until the keyboard finished. |
||
|
|
b19f65f9b6 |
fix(mobile): steady overlay entrance while the keyboard dismisses
Opening a bottom-sheet overlay usually dismisses the keyboard in the same moment, so the panel's keyboard-inset bottom anchor was transitioning down underneath the panel's own rise animation — a jerky, offset entrance. The anchor now snaps to its final position for the duration of the enter animation and resumes animating afterwards (so an already-open overlay still lifts smoothly when the keyboard reopens). |
||
|
|
dbcb655e43 |
feat(mobile): synchronized pill composer transitions and draft screen fixes
Drop the animated pill/composer morph in favor of instant swaps that are synchronized with the keyboard choreography: a new oc:keyboard-intent event collapses the composer (flushSync) before the hide compensation is measured, so keyboard travel and composer height change land as a single chat motion on both iOS and Android (Android also gains keyboard signals and deterministic re-pins around its native resize). The WKWebView caret is hidden during the transition so it no longer flies to its new position. Draft screen: starter chips hide instantly while the keyboard is up and the centered title rides the keyboard shift compensation instead of double-jumping; the composer drag handle also works in dictation mode; the highlight mirror is disabled on mobile so the caret matches the text. Fixes: worktree discovery and the GitHub auth probe now wait for the runtime connection (no more empty branch pickers / stale auth on cold start), worktree discovery merges per project instead of clobbering the persisted map, the cross-project session list resets on instance switch (with an in-flight load guard) so no stale sessions linger, and mobile overlay content contains its overscroll instead of bouncing the page. |
||
|
|
fb839b66a9 |
feat(mobile): collapsed pill composer and mobile input redesign
Mobile composer redesign: when the keyboard is closed the input collapses into a narrow pill (sessions, attach, placeholder, mic) with a round new-session button that fades away on the draft screen. Model and agent selectors move into a row above the textarea; the draft project/branch pickers and the attachment menu become searchable bottom sheets reusing MobileOverlayPanel; a drag handle (also available while dictating) swipes the composer into and out of a fullscreen mode. Keyboard-lifecycle hardening: composer controls (agent cycle, dictation and its overlay controls) no longer steal focus and dismiss the keyboard; overlays reopen the keyboard on close via a debounced restore chain that survives menu-to-picker handoffs and skips the native file picker; open overlays and dictation keep the composer expanded. Dictation starts directly from the pill and its overlay content fades in after the shape settles. The keyboard slide compensates the pill-to-full height change in one motion, and the mobile highlight mirror is disabled so the caret always matches the text layout. |
||
|
|
531039b690 |
feat(mobile): transform-based keyboard choreography on iOS
Stop animating the app shell height when the keyboard opens: per-frame reflow plus the scroll-follow chase caused visible micro-jitter on the composer and pinned chat. The shell layout now snaps exactly once per open/close at an invisible choreography point, while the composer and pinned chat content slide via compositor-only transforms in sync with the keyboard. Dismissal starts from the textarea focusout (no bridge latency), runs a shorter 0.2s leg, and the WebKit form accessory bar is disabled. Composer keeps a 12px gap above the open keyboard. |
||
|
|
37a9179656 | chore: remove bundled IBM Plex fonts | ||
|
|
8f685f8db7 |
feat: improve mobile header and toast placement
Move mobile and VS Code toasts to the top Respect safe-area spacing for Capacitor toasts Add dedicated mobile metadata button and update header icons |
||
|
|
61a4a23add |
feat: native iOS & Android mobile apps (Capacitor) (#1954)
* feat(mobile): add Capacitor native shell * docs: add serve-sim workflow guidance * docs(mobile): add implementation handoff * chore(mobile): clean up generated defaults * feat(mobile): add connection onboarding * feat(mobile): manage saved instances * feat(mobile): refine connection management UI * chore(mobile): upgrade Capacitor 8 * fix(mobile): reliable saved-instance auth with secure token storage - store client tokens in the OS secure store (iOS Keychain / Android Keystore) per instance URL via direct native plugin calls; keep only token-less metadata in localStorage. Bound every secure call so a stalled bridge can't hang unlock. - bypass the secure-storage JS wrapper's lazy platform load (which stalled in the webview) by calling internalSetItem/internalGetItem/internalRemoveItem directly. - harden the shared connect/unlock controller (health + session + progressive password) and drop the heavy pre-connect hydration that stalled no-token hosts. - await token persistence before switching runtime endpoints (no fire-and-forget). - sync native iOS/Android projects + Keyboard/StatusBar config for Capacitor 8. * fix(mobile): keep UI stable across connection churn (no transport hardcoding) The "reload every ~10s" was a UX bug, not a transport one: - MobileSurfaceShell received a fresh inline onClose each parent render, so any re-render (e.g. an SSE/WS event) re-ran the focus effect and refocused the first element — stealing focus from the active input and collapsing the keyboard mid-edit. onClose now lives in a ref so the focus/keydown effect depends only on `open`. Fixes all sheets (Instances/Files/Changes/Settings). - Gate the mobile shell on connectionPhase, not the live isConnected flag, so a transient reconnect keeps MobileShell mounted instead of flashing the loader. - Instances form: populate fields imperatively on edit/cancel/save instead of via an effect keyed on the derived connection, so list churn can't wipe input. Transport stays on `auto` (WS-first with SSE fallback) — no hardcoded override, so WS-only Quick Tunnels and SSE-capable proxies both keep working. * feat(mobile): add native QR pairing-code scanner Wire the connection onboarding + Instances scan buttons to a real native scanner via @capacitor-mlkit/barcode-scanning, which registers as the BarcodeScanner plugin the existing mobileQrScan helper already resolves at runtime. Add NSCameraUsageDescription and bump the iOS deployment target to 15.5 (GoogleMLKit 8 requirement). * fix(cli): repair connect-url host resolution Define the missing isWildcardBindHost helper that connect-url called but was never declared, which crashed any link generation that reached host resolution. Also treat a full http(s) --host value as a public server URL so '--host https://example.com' produces a correct link instead of 'http://https://example.com:port'. * fix(mobile): make input follow the keyboard across all surfaces Switch the native Capacitor Keyboard plugin to resize: 'none' and drive the layout from an --oc-keyboard-inset CSS variable set on keyboardWillShow, which fires at the start of the iOS keyboard animation. A transition tuned to the native keyboard curve/duration (0.25s, cubic-bezier(0.38, 0.7, 0.125, 1)) makes the layout rise together with the keyboard instead of snapping into place after the built-in 'native' resize finished (~1.5s lag). The inset is consumed by every surface that can hold a focused input: - chat shell shrinks its height; - portal sheets/overlays raise their bottom edge; - the full-screen connect/login view caps its height so it actually scrolls (and is now generally scrollable for long saved-connection lists). * feat(mobile): rounder chat composer + native bottom safe area Round the mobile chat composer corners a touch more (1rem), and reserve a small app-level bottom safe area for the native shell via the --oc-app-bottom-safe token so controls clear the phone's rounded hardware corners. The reservation folds into the keyboard inset (no gap above the keyboard), and the composer's own bottom padding tightens while the keyboard is open. * fix(mobile): remove iOS 26 dark status-bar band; polish composer The dark band behind the status bar in system Dark Mode was iOS 26's automatic scroll edge effect (Liquid Glass) dimming the WebView's top edge beneath the status bar — appearance-coloured, so it tracked the system theme regardless of the in-app theme. Hide it via UIScrollView.topEdgeEffect/bottomEdgeEffect on the WebView's scroll view (iOS 26+), and make the WebView non-opaque so the themed web background shows under the overlaid status bar. Also: re-assert the status-bar overlay on resume, paint the document canvas with the theme background in the native shell, round the composer corners to 1.5rem, and enlarge the app-level bottom safe area so controls clear the rounded corners. * feat(mobile): logo splash until first paint is final (no FOUT / layout shift) Cold start flashed the fallback font and then reflowed once the real font and persisted appearance prefs landed, and text jumped a frame after mount because the mobile typography classes were applied from a hook effect. Fix it on three fronts: - apply device classes (device-mobile / mobile-pointer) synchronously in renderMobileApp before the first React paint, so mobile --text-* sizes are in effect from the start; - hold a logo splash (useFontsReady) until the UI web font has loaded; - gate that splash on appBootReady too, resolved once async appearance/typography preferences are applied, plus a double rAF so styles commit before reveal. All under a 2.5s safety timeout so a slow/offline CDN can't block startup. * feat(mobile): native local notifications; APNs implemented but frozen The native app now delivers agent ready/error/question/permission events as iOS (and Android) Local Notifications: a native notifications API backed by @capacitor/local-notifications replaces the Web Notifications API (which doesn't display in a WKWebView), driven by the notification SSE stream now subscribed in the mobile app. Tapping a notification opens its session. Also fix the settings toggle, which treated the Capacitor app as a browser and gated 'Enable Notifications' on the absent Web Notification permission, leaving it un-toggleable. Remote APNs push is implemented end-to-end (dependency-free HTTP/2 + ES256 JWT server runtime, token routes, client registration, iOS native config) but kept dormant: config-gated so it never fires, client registration not wired, and the aps-environment entitlement / background mode removed so the app builds with no Apple push setup. It will be reused once OpenChamber ships its own encrypted relay so users don't each configure APNs. See notifications/APNS.md. WKWebView can't use web push (unlike an installed PWA), so true background-when-suspended delivery on native requires APNs via that relay. * feat(mobile): APNs relay-mode background push Deliver native iOS background push through the central relay: the server posts device tokens + generic, model-based text to api.openchamber.dev/v1/push/send (default), which holds the single APNs key and signs+sends; dead tokens (410) are dropped from the per-session store. Direct APNs (HTTP/2 + ES256 JWT) stays as a fallback when OPENCHAMBER_PUSH_RELAY_DISABLED=true. The mobile push payload is generic only (model + scenario) so no session content crosses the relay. Re-enable the client token registration (useNativePushRegistration) and the aps-environment entitlement (alert pushes need no background mode). Wired into the same fanout as web push; focus-suppressed and only when tokens exist. * fix(mobile): APNs-only native notifications, generic templates, no foreground Make APNs the single notification channel for the native app and fix delivery: - Remove local notifications entirely (the @capacitor/local-notifications plugin and the SSE-driven path). A WKWebView can't tell foreground from background (document.hasFocus() is unreliable), so local notifications leaked while the app was open; the in-app dispatch is no-op'd on native. - Stop gating APNs on UI visibility — a backgrounded WebView can't report 'hidden' before iOS suspends it, which dropped background push. Instead always send and let iOS suppress the foreground banner (PushNotifications presentationOptions: []). - Fix a ReferenceError (out-of-scope 'variables') that crashed maybeSendPushForTrigger before any push was sent. - Mobile push text is generic: a scenario title ('Agent response is ready' / 'needs your input' / 'needs permission' / 'hit an error') + the session name, no model or message content. - Hide the focus toggle, templates, and test button in mobile notification settings. * feat(push): sign relay requests + bind tokens per server Each OpenChamber server now auto-generates an ECDSA P-256 keypair (persisted in settings, like the VAPID keys) and uses it to: - bind every newly-seen device token to the server on the relay (POST /v1/push/register-token, signed), and - sign every push send (publicKeyJwk + ts + signature over ts.sortedTokens.title). The relay derives serverId = SHA-256(publicKey), verifies the signature + timestamp, and only delivers to tokens bound to that server. Result: a leaked device token alone can no longer be used to push to a device — the sender also needs the server's private key. Stays zero-config (the keypair generates on first use). Drops the soft PUSH_RELAY_TOKEN bearer. * docs(push): describe relay data-confidentiality model Document that the push payload is not application-encrypted (TLS-in-transit only), what the relay and Apple can see (generic scenario title + session name, plus token/sessionId), that the signature is authentication rather than encryption, and what an end-to-end encrypted payload would require. * fix: invalid skill description * feat(push): app-icon badge for native notifications Send an absolute aps.badge with each native push = the count of distinct collapse-ids (tag) pushed since the app was last foregrounded, mirroring the lock-screen banner stack. Cleared server-side on user engagement (session view, message-sent, visibility beacon) and on-device via sceneDidBecomeActive. * feat(mobile): auto-connect last instance on launch + notification deep-links Cold launch silently reconnects to the most-recent saved instance (when reachable and a token is saved), holding the splash instead of flashing the connect screen; falls back to the connect screen when there's no saved instance, it's unreachable, or it needs a re-login. Notification-tap deep-links are now captured unconditionally (even before connect / on cold launch) and applied once the app is ready, so a tap opens the target session instead of being lost on the login screen. * fix(mobile): resolve theme background before first paint on cold launch The mobile shell entry (mobile.html) had no pre-paint theme step, so a cold launch flashed the WebView's default light canvas, then the baked design-system default (.dark { --background: #151313 }) via body.bg-background, before React's theme system injected the real theme vars. Add a blocking script that resolves dark/light from the persisted theme + system preference and sets --background (plus color-scheme and the element background) inline on the root, so the very first paint matches the resolved theme. Falls back to the default flexoki backgrounds when no theme has been persisted yet. * feat(mobile): openchamber:// deep-link foundation + arm64 simulator build Add a typed deep-link vocabulary (deepLinks.ts: parse/build + DeepLinkIntent) and a single native navigation layer (deepLinkNavigation.ts) that handles both the openchamber:// URL scheme (App.appUrlOpen — widgets, Live Activities, external links) and notification taps, normalising each into an intent. Session and new-session resolve against the store; shell surfaces (sessions/settings/ views/changes) register handlers. Cold-launch intents stash until the app is ready. Replaces the push-only useNativePushDeepLink and keeps backwards compatibility with bare sessionId payloads. Register the openchamber:// scheme in Info.plist. Dev tooling: with-mobile-env now honours xcode-select (-p) instead of hardcoding Xcode.app, so an Xcode beta is used. build:ios:simulator runs a new ios-sim-build script that temporarily drops the MLKit barcode-scanning pod (no arm64-simulator slice) so the app builds an arm64 binary installable on Apple Silicon simulators, then restores the Podfile + Pods for device builds. QR scanning already degrades cleanly when the native plugin is absent. * feat(mobile): iOS home/lock/Control Center widgets + push-driven refresh Add a Widget Extension (OpenChamberWidget) and a Notification Service Extension (OpenChamberNotificationService), wired into the Xcode project, sharing an App Group with the app. Widgets: - Overview (medium): recent sessions with read/unread dots + four quick actions (new, status, instances, settings). - Sessions (large): session list with per-session project label, attention count and a new-session button in the header. - Quick Actions (small): New chat pill + status/instances. - Lock Screen (accessoryCircular x2): brand logo to new session, attention counter. - Control Center control: brand logo (custom SF Symbol) to new session. Data: the app writes a session-overview snapshot (attention count + recent sessions with project labels) to the App Group on scene activate/resign; the NSE refreshes it from each push (aps.badge + sessionId) so widgets update even when the app is closed (needs aps mutable-content, added to the server + relay). Deep links: add openchamber://status (session status panel) and reuse view/instances; all widget taps route through the existing deep-link channel. * feat(mobile): large Sessions widget lists 6 sessions with project labels * feat(mobile): edge-swipe to switch sessions with directional slide+fade * fix(mobile): keep widgets in sync via reload-on-change + periodic refresh Widgets sharing the app's WidgetKit reload budget refreshed unevenly, leaving the large Sessions widget stale (no unread dot / attention count) while medium updated. Drop the per-call updatedAt from the snapshot, only write + reloadAllTimelines when the session overview actually changed (so we don't burn the budget on every scene activate/resign), and give each widget a periodic timeline refresh so a missed reload self-corrects. * feat(mobile): Android support — chrome fixes, SSE lock, icon, QR scan Cosmetics: - Status bar: on Android inset the WebView below the bar (overlay:false) and paint it with the resolved theme background + correct content Style, since Android doesn't feed env(safe-area-inset-top) to CSS. - Keyboard: skip the manual --oc-keyboard-inset on Android (the window resizes natively, so applying it double-counted and floated the composer); declare windowSoftInputMode=adjustResize and disable the shell height transition on Android so the header no longer bounces on keyboard open. Transport: lock Capacitor apps to SSE — native WebSocket streaming is unreliable on Android (events only arrive once a run finishes). Forced in sync-context and the other options are disabled in the Chat settings UI. Push: gate APNs registration to iOS only; on Android @capacitor/push-notifications register() needs Firebase/FCM (not configured) and crashes at launch. QR pairing: declare CAMERA permission + the ML Kit barcode_ui dependency, and install/await the Google barcode scanner module (with a post-install retry) before scanning so the first scan works without a manual retry. Icon: Android adaptive launcher icon generated from the cube logo (full-bleed white background, no edge artifact on One UI). Source assets under mobile/assets. Tooling: adb-based android-device.mjs + android:* scripts for device deploy. * feat(notifications): presence-aware push routing (don't spam the phone) Only push to a device when the notification would otherwise be missed there. A notification is suppressed on devices where the user is already present. - Tag every client's visibility beacon and web-push subscription with a platform ('ios' | 'android' | 'vscode' | 'desktop' | 'web') via getClientPlatform(). - Server tracks visibility per client (keyed by oc_ui_session) with the platform, and exposes isAnyInteractiveClientVisible() = any visible non-mobile client. - Native push (APNs) and mobile PWA web-push are now suppressed when an interactive (desktop/web/vscode) client is visible — it already shows the in-app notification. Gated on the desktop's visibility (reliable), never the phone's own (a backgrounded WKWebView can't report "hidden"). - Desktop/web web-push keeps the any-visible gate (a visible client absorbs it). - Skipping APNs also skips the badge increment so it doesn't drift. Fixes the case where every session on a shared instance pushed to the phone even while the user was actively working on desktop. * feat(mobile): Android FCM push notifications Enable native background push on Android via Firebase Cloud Messaging, in parallel with the existing iOS APNs path. - Add google-services.json + declare POST_NOTIFICATIONS (Android 13+). The Google Services Gradle plugin is applied when the file is present, so register() returns an FCM token instead of crashing. - Un-gate native push registration to iOS OR Android, and tag the registered token with its platform ('ios' | 'android') so the relay routes it to APNs vs FCM. - Server stores the platform per device token and binds it to the relay (platform included in the signed register message). - Notification small icon: monochrome cube silhouette with a mark on the top face, set as the FCM default_notification_icon so the status-bar icon reads as the logo. Relay-side FCM sending ships in openchamber-website. * docs(mobile): refresh HANDOFF with current state, dev/deploy process, and CI gap * chore(mobile): iOS store-review prerequisites (privacy manifest, encryption flag) - Add the app's PrivacyInfo.xcprivacy (no tracking; required-reason UserDefaults for the App Group snapshot shared with the widget + notification service extension) and wire it into the App target's resources — Apple requires an app-level privacy manifest. - Set ITSAppUsesNonExemptEncryption=false to skip the per-build export-compliance prompt. - HANDOFF: add a store-review-readiness checklist (in-repo vs release-time console/infra items). Verified: plist lint, xcodebuild parse, and an iOS simulator build with PrivacyInfo.xcprivacy bundled into App.app. * refactor(mobile): dedupe capacitor detection + make beacon guard explicit Addresses non-blocking PR review notes: - Consolidate the repeated Capacitor-native check (mobileConnections, deepLinkNavigation, usePushVisibilityBeacon each redefined it) onto the single isCapacitorApp() in lib/platform. - usePushVisibilityBeacon now guards on isWebRuntime() OR isCapacitorApp() instead of relying on isWebRuntime() being true for Capacitor, so the beacon can't silently stop if that changes. |
||
|
|
bf1d0d3a75 |
feat(mobile): polish composer model/agent controls and selection overlay
Redesign the mobile composer model and agent buttons as borderless, full-bleed labels that hug their content, truncate with an ellipsis when space is tight, and show the provider logo inline before the model name. Tighten the footer action buttons (sessions / attach / auto-accept) so they sit close together, with a small left inset on the group. In the mobile model selection overlay, make the thinking-variant control text-only with a chevron, vertically center the variant and favorite controls in each row, and place the provider logo inline with the model name. |
||
|
|
c5eab5ac1e |
Revert "fix(ui): actually shrink typography classes on mobile viewports (#1791)"
This reverts commit
|
||
|
|
1ac8fbad70 |
fix(ui): actually shrink typography classes on mobile viewports (#1791)
The mobile stylesheet at packages/ui/src/styles/mobile.css defines a @media (max-width: 1024px) block that overrides the --text-ui-header, --text-ui-label, --text-meta, and --text-micro custom properties to shrink labels on phones. The values it was setting were equal to (or only 1px smaller than) the desktop defaults, so the model selector, agent selector, and any other element using these classes rendered at the same size on a 360px-wide phone as on a 1920px desktop. Fix: tighten the mobile values to 0.875rem (ui-header), 0.75rem (ui-label, meta), and 0.6875rem (micro), so that mobile is genuinely smaller than desktop. The same change is applied to the iOS PWA standalone block lower in the file so both code paths produce the same size. Fixes #504 Signed-off-by: Bohdan Triapitsyn <artmore@protonmail.com> Co-authored-by: James Pinnell <james@example.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
08a851f902 |
fix(markdown): restore paragraph spacing in assistant messages
Wire the unused --markdown-paragraph-spacing token to .markdown-content p so adjacent paragraphs no longer collapse into a single visual line (Tailwind preflight had zeroed the default <p> margins). The renderer wraps each block in a display:contents [data-md-block] element, so the message-level last-child margin nullifiers target the wrapper, not the paragraph. Drop the trailing margin on the last paragraph of the last block directly so messages don't gain extra bottom space. Keep tool-card and reasoning markdown compact. |
||
|
|
59ecd86b4b |
perf: isolate chat streaming renders and reduce sidebar render cost (#1672)
Reworks the chat and session-sidebar render paths to cut render cascades, memory
churn, and UI jank on large sessions and big session trees. Behavior is preserved;
the changes are about *when* and *how much* the UI re-renders.
## Chat streaming
- Freeze the streaming message's parts in the bulk turn projection during streaming,
and re-inject live parts only in an isolated tail leaf, so a ~60/sec delta stream
no longer re-runs the whole-session projection or re-renders unrelated rows.
session with referential reuse of unchanged turns.
- Memoize message rows with field-aware comparators instead of reference equality.
- Replace the manual child-session polling in the task tool with the live SSE
stream + a one-shot load, removing a fetch/settle state machine.
## History loading & scroll
- Load an initial page fast, then prepend one older page in the background so the
scroll container has headroom and "load older on scroll-up" fires before the user
hits the absolute top.
- Compensate scroll synchronously (in a layout effect, before paint) for prepends —
including background prepends that don't originate from a user scroll — so the
viewport stays stable instead of judder-correcting on the next frame.
## Markdown rendering
- Render markdown synchronously *styled* on first paint (paragraphs, lists, code
cards, tables, inline code) instead of raw escaped text; the async pass then only
upgrades syntax-highlight colors. Eliminates the flash of full-width raw text.
- Load KaTeX CSS eagerly with the main bundle instead of inside the lazy markdown
chunk, avoiding a late stylesheet injection on first render.
## Sidebar
- Hoist per-row recursive tree walks out of row comparators into per-group
precomputed sets/keys; batch live-session lookups into a single map; add a
group-level memo boundary.
- Isolate rename drafts so per-keystroke typing doesn't repaint the row tree.
## Sync layer
- Add a staleness guard so a slow message fetch can't repopulate a session the user
navigated away from.
- Throw on fetch failure for authoritative loaders so a transient blip can't read as
an empty server response.
## Cleanup
- Remove dead code (unused hooks, params, duplicated inline types) surfaced while
reworking the above.
## Known issue
- A rare, purely cosmetic first-paint width flash can still appear on large sessions;
it has no behavioral or data impact and is tracked for a follow-up runtime trace.
|
||
|
|
3541bc63f9 |
feat(desktop): macOS vibrancy for the left sidebar with a toggle
Add native macOS vibrancy behind the left sidebar (the only translucent surface; header/chat/right sidebar stay opaque), plus a setting to turn it off. - Window created with vibrancy applied after first show (avoids the cold-launch no-composite quirk); minimize/restore suppress the frost during the genie animation. Renderer frosts the sidebar via --sidebar-vibrancy-overlay once data-oc-vibrancy[-ready] are set; project-actions pill matches when open. - data-oc-vibrancy-ready defaults are set in cssGenerator (DOM guaranteed), not the preload (document-start race left the sidebar un-frosted on launch). - prefers-reduced-transparency falls back to solid surfaces. - Appearance settings (macOS desktop only): a checkbox to enable/disable vibrancy, persisted to settings.json and applied via a Save & restart button (vibrancy is a window-creation option, so it needs a relaunch). |
||
|
|
9b52222ef1 |
Add folder-level revert action in Git changes tree (#1390)
* feat: add folder-level revert for git changes * Fix bot comments. Fix confirmation dialog for mobile view. * fix --------- Co-authored-by: Konstantin Zolin <zolin_ka@vk.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
7adbaca797 |
fix: mobile dialog and overlay panel positioning
Center PWA dialogs again by dropping the stale iOS offset that pushed a flex-centered (position: relative) dialog down by half the viewport. Seat the mobile overlay panel flush to the bottom (drop the float margin), drop its bottom border in favor of a scroll shadow on the content, and raise it above sheets (z-60) so an overlay opened over a surface sheet renders on top instead of behind it. |
||
|
|
90b3d4760e |
fix(ui): keep PWA dialogs visible on Android (#1370)
* fix(ui): keep PWA dialogs visible on Android The PWA dialog adjustments under @media (display-mode: standalone) and (max-width: 768px) were originally written to clear the iOS status bar / notch by adding a top offset and a translateY override. The rule was not scoped to iOS, so Android PWAs matched it too: - The extra top: 50% + (safe-area-top * 0.22) plus --tw-translate-y: -50% + (safe-area-top * 0.6) pushed the dialog below the visible center on Android, where the parent already centers the popup via flex items-center justify-center on Base UI's portal. - max-height was computed against 100vh. Android Chrome's collapsible URL bar makes 100vh larger than the visible viewport, so when the bar is shown the dialog's action buttons can be pushed off-screen (for example the Install button in the Skills install dialog). Fix: - Use 100dvh (with the existing safe-area subtractions) so max-height tracks the dynamic visible viewport on Android. - Move the top / --tw-translate-y override into @supports (-webkit-touch-callout: none) so it only applies on iOS, where it was always intended. Verified by rebuilding packages/web and inspecting the emitted CSS: the common .pwa-dialog-content rule now ships with max-height using 100dvh and no top/translate overrides; the iOS-only block keeps the original offsets. Tested manually on Android Chrome PWA against https://aion.xsim.uk: 'About OpenChamber' and 'Install skill' dialogs now center vertically and their footer buttons stay on-screen as the URL bar collapses/expands. * fix(ui): restore vh fallback for PWA dialog max-height Address review feedback on #1370: the previous patch dropped the original `max-height: calc(100vh - ...)` line entirely and only kept `100dvh`. On browsers that do not understand the `dvh` unit the declaration is invalid and dropped, which would leave the dialog without any `max-height` cap inside this media block — potentially worse than before the fix. Reinstate the canonical progressive-enhancement pattern: ship the `100vh` declaration first so older engines have a usable value, then override with `100dvh` on the next line for browsers that do support it. New comment makes the two-line pattern explicit so it isn't pruned again as accidental duplication. Verified via `packages/web` build: the emitted CSS now contains both declarations in order on the shared `.pwa-dialog-content` rule: max-height: calc(100vh - ...); max-height: calc(100dvh - ...); `type-check` and `lint` in `packages/ui` remain clean. --------- Co-authored-by: lilyzhaun <lilyzhaun@users.noreply.github.com> |
||
|
|
49bf1dbff3 |
feat: widen chat input column with Wide Chat Layout setting (#1318)
The 'Wide Chat Layout' setting toggles a 'wide-chat-layout' class on
the document root that widens '.chat-column' and '.chat-message-column'
from 48rem to 64rem, but '.chat-input-column' (which wraps the
textarea) was missing the matching rule, so the prompt input stayed
narrow while the messages above it widened.
Add the missing '.wide-chat-layout .chat-input-column { width:
min(100%, 64rem) }' selector so the input column tracks the message
column under the same setting.
|
||
|
|
581f7c34dc |
fix: align iPad PWA window controls spacing
Adds tablet standalone PWA detection Aligns header and sidebar action padding Extends safe-area handling for tablet PWAs |
||
|
|
e6c7cc5589 | feat(chat): add wide layout setting | ||
|
|
4b171ee207 |
Prevent page overscroll in chat layout (#1034)
Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com> |
||
|
|
c8eb9a6c5f |
feat: widen chat content area
Chat messages use more horizontal space Chat input keeps its previous max width |
||
|
|
6176ee2b0f |
refactor: simplify mobile keyboard handling
Removed fragile global keyboard viewport manager Restored native mobile viewport behavior Fixed textarea content shifting outside inputs |
||
|
|
87db2ea210 |
fix(ui): stabilize mobile keyboard viewport handling (#1026)
* fix(ui): stabilize mobile keyboard viewport handling * fix(ui): tighten mobile keyboard viewport handling * Avoid redundant keyboard open store writes --------- Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com> |
||
|
|
6a70d51ea7 |
refactor: drop mobile keyboard/viewport hacks, rely on browser
Remove body position:fixed lock, visualViewport listener in MainLayout (keyboard-inset heuristics, scroll-to-zero lock, focusin/focusout RAF), [data-keyboard-avoid-active] translateY rule, and related CSS vars (--oc-keyboard-inset, --oc-keyboard-avoid-offset, --oc-keyboard-home-indicator, --oc-visual-viewport-offset-top). Strip data-keyboard-avoid* attrs and keyboardAvoid props from Dialog/ScrollableOverlay and consumers. Drop isKeyboardOpen from useUIStore. With body unlocked and the layout plain flex-col (h-dvh), the browser shrinks the viewport on keyboard open naturally — composer sits above the keyboard, header stops lagging, input no longer jitters. FilesView.nudgeEditorSelectionAboveKeyboard now derives the occluded bottom locally from visualViewport + documentElement.clientHeight. |
||
|
|
bf110527a5 | feat: implement window controls overlay layout and styles (#1001) | ||
|
|
4506c18f53 |
UI refresh: Base UI migration + flat tinted button language + mobile polish (#960)
* deps: bump @opencode-ai/sdk to 1.4.6 and add @base-ui/react
* build: align vite configs with Base UI and packages update
* feat(ui): migrate primitives to Base UI
Replace Radix-based wrappers (button, checkbox, collapsible, command,
dialog, dropdown-menu, input, radio, scroll-area, select, separator,
sonner, switch, textarea, toggle, tooltip) with @base-ui/react
equivalents and a refreshed visual language: flexoki theme tokens,
typography utilities, and a redesigned CommandPalette. Add new
fancy-button variant set and a local slot helper for asChild composition.
* refactor(ui consumers): adopt Base UI primitives across the app
Update chat, layout, git views, settings, multirun, session and agent
management screens to use the new Base UI-backed primitives (anchor
width token rename, dropdown/select/dialog usage adjustments, new
component APIs).
* fix(ScrollableOverlay): forward `disabled` to inner element
Add `disabled` to the type so it propagates via rest spread to the
underlying element (e.g. textarea in simple mode). Resolves a type
error reported after the main merge.
* refactor(dropdown consumers): migrate preventDefault sites to closeOnClick
Base UI's Menu.Item does not check `event.defaultPrevented` before
closing the menu, so the Radix-era `onSelect={(e) => e.preventDefault()}`
pattern no longer keeps the menu open. Replace those sites with the
Base UI `closeOnClick={false}` prop (VSCodeLayout rate-limit info
rows; AgentGroupDetail worktree actions).
* docs(AGENTS): note Base UI as primary source for UI primitives
* fix(ContextPanel): center expand/close buttons in tab header
Header (h-8) had the action row pinned with items-end + pb-1.5, which
placed the h-7 buttons ~2px above the header top and made them look
cropped. Switch to items-center so they sit on the same baseline as
the tabs.
* style(tabs): drop transitions on default variant, soften active pill
Default (underline) variant now switches instantly — no transform/width
transition on the underline indicator, no color transition on tab
buttons. Active pill bg is color-mixed down to 55% of
--interactive-selection so light-theme selection doesn't scream.
* style(button): pill-style elevation and introduce chip variant
Solid variants (default/destructive/neutral/primary/basic) now use the
active-pill shadow stack — hairline navy outer ring + stacked tight/soft
drops — instead of the pure-black inset/border approach. Outline
variant (and FancyButton basic) gets the same elevation without the
white top highlight (no fill to catch light).
Add a new chip variant: flat `border border-border/60` button without
elevation, intended for one-of-N toggle groups where a raised
button would read as the wrong affordance.
* style(header): apply pill-style shadow to OpenIn and Project Actions
Both split-button wrappers previously used a 1px CSS border for
definition. Replace that with the pill elevation stack so the header
buttons match the new Button variants and feel less hard-edged.
* refactor(toggles): migrate one-of-N segmented groups to chip variant
Color Mode, When-sessions-expire, voice/STT providers, auth method,
agent mode, MCP transport, tunnel type and terminal Ctrl/Cmd modifier
toggles all read as flat chip selectors rather than raised buttons.
Switch them from variant="outline" to the new chip variant so the
raised pill-style elevation doesn't fight the selected-state tint.
* fix(elevation): theme-adaptive pill-style shadow for dark mode
The navy (rgba(14,18,27,*)) outer ring was invisible on dark surfaces,
erasing the button/pill edge. Add dark: variants that use a subtle
white outer ring (rgba(255,255,255,0.10)) and darker drop shadows
(rgba(0,0,0,0.30/0.20)) so the elevation reads on both themes.
Applies to Button solid/outline variants, FancyButton equivalents,
the OpenIn and Project Actions header split-buttons, and the active
pill indicator in SortableTabsStrip.
* style(button): flat tinted variants with squircle corner-shape
Replace elevated pill-shadow design with a flat tinted language: pale tinted
fill + hairline tinted border + saturated tinted text for default/destructive.
Add CSS corner-shape: squircle with @supports gate so Chrome 136+ gets the
softened superellipse corners at 50px, Safari falls back to a slightly larger
round radius.
* style(tabs): replace active pill shadow with tinted border
* style(header): flat border and squircle corners for OpenIn / Actions
Drop pill-style shadow stacks in favor of a plain hairline border and apply
the same squircle corner-shape treatment as Button so header controls match
the new visual language.
* style(checkbox): drop filled background for tinted checkmark
Checked / indeterminate states now keep the transparent box, show a
primary-base check/minus glyph, and use a softened primary-tinted inset
border (50% mix) so the control reads as selected without a solid swatch.
* style(layout): widen sidebar-to-chat corner radius to 10px
* style(button): retune tinted variants and unify outline fill
- Bump primary fill toward noticeable but still soft (light 10/16/22,
dark 16/22/30) and weaken both primary and destructive borders
(12% light / 20% dark) so the tone reads without shouting.
- Further calm destructive across fill and border so revert-style
actions match the muted reference.
- Align the outline variant fill with --surface-elevated so git-view
generate/commit buttons share the header action button surface.
* style(tabs): align pill variant with new button language
Active pill now uses --surface-elevated fill and border-border/60 to
match the outline button. Track and tab/pill geometry adopt the same
squircle treatment as Button: 9-10px fallback with 50px radius under
@supports(corner-shape:squircle). Also tighten track padding (inline
2px, block 2px) and grow active tab height so the indicator fills
the track instead of floating inside it.
* refactor(theme): hardcode radius scale, drop per-theme override
Radius values are now fixed in the design system (flexoki-dark's scale
becomes the app default). Themes can no longer override --radius-* — the
config.radius block is removed from every theme JSON, dropped from the
Theme type, and no longer emitted by the CSS generator.
* chore(ui): lint/type cleanup across Base UI wrappers
* style(tabs): dial pill track background down to near-invisible
* refactor(chip): aria-pressed drives the selected tinted state
The chip variant now carries the tinted primary palette (same as the
default filled button) under aria-pressed, so consumers toggle selection
via a single aria attribute instead of repeating the border/text/bg
override classes. All existing chip consumers migrate to aria-pressed
and drop their hand-rolled selected className.
* chore(defaults): humanize variant label in thinking selector
* fix(diff): render all lines in single-file mode
Single-file mode wrapped PierreDiffViewer in its own layout=fill which
creates a nested virtual-root; combined with the outer CSS containment
on the wrapper, Pierre's virtualizer measured a clamped viewport and
stopped rendering lines past ~150. Mirror the stacked-mode approach:
hoist a ScrollableOverlay as the sole virtual root/content and render
PierreDiffViewer inline, so the shared virtualizer sees the real scroll
viewport and paints every line.
Also drops an unused cn import in SessionRetentionSettings.
* feat(files): collapse floating editor controls behind icon button
The floating file editor toolbar now starts as a single pill icon button
and expands into the full control row on hover or click. Clicking outside
or moving the pointer away collapses it back, keeping the editor surface
unobstructed while the file is being read/edited. Fullscreen overlay
keeps its always-visible toolbar.
* fix(chat): align mobile session status bar radius with chat input
* fix(mobile): keep radio/checkbox/switch at their native size
Mobile CSS forced min 36x36px on every <button>, which swallowed our
Radio and Checkbox primitives (also buttons with role=radio/checkbox)
and ballooned them on touch devices. Exclude role=radio/checkbox/switch
from the touch-target rule and pin the Radio/Checkbox box size with
arbitrary px so padding-scale overrides cannot stretch them either.
* chore(mobile): drop unused cornerRadius prop plumbing
The status bar now hardcodes var(--radius-lg) on both views, so the
cornerRadius prop and the useUIStore selector that sourced it in
ChatInput are dead weight.
|
||
|
|
952132a08d | fix: remove desktop translucency to reduce macOS compositing cost | ||
|
|
a516650f96 |
fix: stabilize chat auto-scroll and bottom resume
- Unify send and session resumes around the latest chat tail - Keep smooth follow active during assistant message growth Remove staged chat rendering from the main scroll path |
||
|
|
dfc61d7671 |
refactor: replace chat markdown renderer
- Replace Streamdown-based chat markdown rendering with the new renderer path - Preserve OpenChamber table, code block, and mermaid presentation - Remove leftover Streamdown dependencies and compatibility hooks |
||
|
|
a36b11d54e |
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 |
||
|
|
f032df2298 |
fix(mobile): close settings drawers and remove extra top spacing (#770)
* fix(mobile): close side drawers when opening settings * fix(mobile): remove duplicate top inset in drawers --------- Co-authored-by: Jovines <jovines@qq.com> |
||
|
|
c38e00bd85 |
perf(desktop): reduce CPU/GPU overhead in Tauri shell
- SSH monitor: adaptive polling 2s→10s after stabilization, cheap TCP probe before expensive SSH subprocess check - SSH setup: exponential backoff in wait_for_master_ready and wait_local_forward_ready (250ms→2s cap) - Health checks: exponential backoff (100ms→1s / 250ms→2s) instead of flat intervals - Startup recovery poll: cap at 15 retries instead of infinite - Remove webview log target in release builds (eliminates IPC overhead) - Set global NO_PROXY env var at startup for all loopback addresses - Remove reqwest::blocking feature; use raw TCP for sidecar shutdown and SSH health checks - Disable pinch-to-zoom on macOS via WKWebView.setAllowsMagnification - Add WebView2 browser args on Windows (proxy bypass + disable unused UI features) - Add Cargo release profile: thin LTO, codegen-units=1, strip - Extract apply_platform_window_config for consistent window setup - Add vibrancy toggle in Appearance settings (macOS desktop only) with solid background fallback when disabled |