The composer rendered a second StatusRow instance carrying the
pending-changes bar and the todos dropdown, so every restyle of the
floating assistant-status chip (glass, placement, sizing) silently
restyled the composer bar and its dropdown too — for the fourth time.
StatusRow is now only the floating chip above the composer; the
composer's own bar is a new ComposerStatusBar with the pre-glass layout,
its own container name, and its own container-query classes (the
anti-overlap rules that hide the long active todo under 38rem and the
changed-files label under 30rem moved with it — losing them during the
split let the two dropdown triggers overlap on mobile). The two
components share no markup and no CSS hooks anymore.
Sending while the agent is working queues the message without creating a
user row, so the anchor-arming send path had nothing to claim and the
viewport stayed parked mid-history. Queued sends now jump straight to
the live edge (instant, so end following re-latches immediately).
Rail selection also switches from smooth to instant scrolling: a long
smooth scroll through the virtualized timeline gets cancelled by row
remounts and lands mid-way or on the wrong message, while a teleport
always arrives.
Six reported defects, one root theme — our port diverged from the
reference semantics:
- a user gesture no longer collapses the reserved anchored end space
(that snap-to-bottom was the 'anchoring vanishes on a micro-scroll');
it only disarms the follow/anchor machinery, and anchor remeasures
after the user takes over can no longer re-position the viewport
- the anchor arms synchronously BEFORE the send reaches the store, and
the claim compares against the arm-time baseline — arming a frame
after the optimistic row committed is why anchoring 'rarely worked'
and sending from mid-history did nothing
- gestures are selective (wheel up, touch/pointer away from the end,
PageUp/Home/ArrowUp), so scrolling toward the end or clicking a row
at the live edge no longer kills follow
- 'at end' is a tight 40px band against the full content length instead
of the list's half-viewport isNearEnd, so the scroll-to-bottom pill
appears when the viewport actually leaves the end
- reaching the end re-arms follow without knocking an anchored turn out
of its mode, and the overlay scrollbar suppression follows scroll
ownership rather than the anchor's mere existence
- the status/working row moved out of the list footer to a fixed spot
above the composer: inside the list it walked down with every
streamed line, and its unmount was part of the end-of-stream jerk
Hovering (or tapping) a context chip opens a stacked preview of its
pending items above the composer: numbered entries with a muted header
band, the captured selection, and the user's comment, which can be
edited in place (save/cancel) or removed before sending. The chips
component now subscribes to the draft store itself; the per-kind count
plumbing in ChatInput is gone.
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.
* feat(chat): /btw command — side questions in a temporary forked session
/btw <question> forks the current session (full context inheritance) and
opens a compact peek panel docked above the composer. The composer itself
becomes the btw input while the panel is open: sends route to the fork,
the placeholder and a mode chip reflect the target, and the stop button
aborts the fork's turn. Closing the panel (or the chip's ✕) destroys the
fork, leaving the main conversation untouched.
The panel shows only the fork's own tail (messages at/after the fork
creation time) and live permission/question cards scoped to the fork.
- chat/btw/BtwPanel: peek sheet (desktop + mobile), fork-tail view,
auto-close on disappearance, Esc to close
- lib/btw: startBtwSession (fork + rename + routed send), closeBtwPanel
(close = destroy), filterBtwTailMessages
- ChatInput: btw-mode send routing via SendMessageOptions.sessionId,
btw-aware activity (stop/abort), placeholder + mode chip
- useSessionActivity: exported for per-session activity reads
- i18n: btw keys across all 11 locales
* fix(chat): keep btw sends isolated
* refactor(chat): rework /btw into a metadata-scoped peek panel
- Link the active btw fork through the parent session's metadata
(openchamber.btwSessionID) so the panel exists only in the session that
invoked /btw, follows parent navigation, and survives reloads; the fork
carries a kind:'btw' marker with its originalSessionID.
- Replace the wall-clock history boundary with the id of the newest cloned
message (server-generated ascending ids), stored in fork metadata.
- Derive panel identity in useBtwPanelState; useBtwStore shrinks to
transient per-parent UI state (collapsed/creating/destroying).
- Panel UX: dropdown-style glass surface, chat ScrollShadow, single
title+chevron collapse toggle, muted header controls, promote action
(keep as a full session and navigate to it), Esc collapses instead of
destroying, reserved Working indicator row, streaming auto-follow via
ResizeObserver keyed on content readiness.
- Add a 'peek' chat surface mode that suppresses per-message controls and
turn footers inside the panel; user bubbles keep a small gap below.
- Hide btw forks from the sidebar, session switcher, and command palette
until promoted; mark the fork before inserting it into local stores.
- Delete/archive lifecycle: removing the fork unlinks the parent; removing
the parent also removes its temporary fork.
- patchSessionMetadata now mirrors updated sessions into live stores.
- Localize new strings across all 12 dictionaries; add unit tests for
metadata helpers, the btw flow, and the UI store.
* fix(chat): clamp the btw panel below the app header when the keyboard is open
Reuse useMobileAutocompleteMaxHeight (the composer autocomplete precedent)
on the panel's scroll body, reserving the panel header and bottom spacer
height, so the sheet adapts to the visual viewport instead of riding under
the app header on mobile.
* fix(lint): drop unused destructured bindings in sessionBtwMetadata
CI eslint has no underscore ignore pattern; strip metadata keys with typed
copies and delete instead of discard-destructuring.
---------
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Plain Enter now inserts a newline in the expanded composer
Cmd/Ctrl+Enter still sends the message
Helps avoid accidental sends while writing long prompts
Create projectless chat sessions under a managed, date-scoped Chats directory and clean abandoned or deleted session folders.
Add Chats to sidebar state, startup cache, shared context, and Electron Mini Chat while keeping VS Code project-only. Resolve managed chat directories to one server-side memory owner and document the runtime contracts.
Pass initial subagent prompting settings into embedded chat URLs and views
Prevent inactive embedded chats from stealing focus on load
Treat review sessions as independent conversations when resolving prompt read-only state
Snapshot the new-session draft (and keep the existing-session target captured) at
submit time, then use that snapshot for draft materialization and routing instead
of re-reading live selection state after async preparation.
Fixes#2222Fixes#2315
Adds a work-status panel beside the transcript. Context fill, model and
cost, todos, running subagents and the permission requests blocking
them, branch and working-tree state, MCP servers, pinned messages and
context sources were scattered across the header, the composer and the
context panel — a blocked subagent was reported nowhere at all. The
panel reads them from live channels rather than persisted history, and
becomes an overlay where the chat is too narrow to seat a column.
It is on by default, including for existing installs. Because it now
carries these readouts, the desktop header and composer drop the ones it
duplicates: todo and changed-files chips, usage and MCP tabs. VS Code
and mobile keep theirs — neither hosts the panel.
Fixes MCP authorization, which was broken from the panel, invalidated by
a directory switch through a redirect URI that encoded the working
directory, and left the desktop app in the background because browsers
will not follow a custom-protocol link without a user gesture. The
settings page no longer asks the user to understand the MCP spec before
adding a server: one field takes the command or the link, with the kind
inferred and a visible override, and client-registration fields appear
only when a server actually asks for its own credentials.
Also: skills load from the panel instead of only when the composer's
slash autocomplete opens; the header button names the current instance
rather than falling through to the word "Instance" for relay hosts.
Three new optional UI settings keys, all migrated. No change to stored
MCP server configuration.
* fix(ui): update session-switch-resync test to current handleEvent/setSessionTodos signatures
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(build): split Shiki grammars/themes, CodeMirror legacy modes, and @pierre/diffs into on-demand chunks
Merging @shikijs/langs into one manual vendor chunk made the first language
request download every grammar (7.4 MB raw / 1 MB gzip). Letting Rollup split
these packages per dynamically imported module downloads only the languages,
themes, and modes actually used — matching how the worker build already
behaves. @pierre/diffs is split the same way so its pure patch parser (used by
the eager tool renderer) no longer drags the Shiki-importing render stack into
the startup graph.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(ui): restore lazy heavy views and stop eager settings-graph loading
- MainLayout: DiffView/FilesView/GitView/PlanView return to lazyWithChunkRecovery
(they were silently made static in 2031e3b4 while their Suspense wrappers
remained), keeping the CodeMirror and @pierre/diffs stacks out of startup.
- ContextPanel: same lazy treatment for its Diff/Files/Git/Plan/Walkthrough
tabs, with null Suspense fallbacks.
- CommandPalette imported getSettingsNavIcon from SettingsView, statically
pulling the entire settings surface (SkillsPage -> CodeMirrorEditor -> vim
mode, theme registry -> @pierre/diffs) into the eager graph; the helper now
lives in lib/settings/metadata.
- The windowed SettingsWindow mounts only after its first open: rendering the
lazy component closed made React fetch the SettingsView chunk graph at
startup.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(ui): keep @pierre/diffs + Shiki out of the eager chat graph and defer diff worker warmup
- DiffWorkerProvider no longer statically imports @pierre/diffs/worker or the
theme registry, and no longer spawns 3 workers plus a main-thread shared
highlighter during mount. Pools are created on demand through a dynamic
module load, warmed via requestIdleCallback after startup settles, and
useWorkerPool notifies consumers when a pool becomes available.
- ToolPart's rich diff preview moves to lazily loaded ToolPartDiffPreview;
the plain-text patch (PlainDiffFallback) renders while the chunk loads,
mirroring the existing error fallback. Theme registration happens during
render inside the lazy module so PatchDiff never renders unregistered ids.
- ChatInput mounts its lazy ToolOutputDialog only after the first attachment
preview opens instead of fetching the dialog chunk on the draft screen.
- getMarkdownSyntaxVars moves to a pierre-free markdownSyntaxVars module so
eager code-rendering consumers stop importing the registration module.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
* perf(web): load ghostty-web and Nerd Fonts on first terminal use
- ghostty-web (638 KB raw JS + WASM VT) is dynamically imported when a
terminal actually mounts; TerminalView stays eagerly importable for the
bottom dock.
- The ~2 MB of CDN Nerd Fonts are no longer preloaded and force-loaded on
every cold start. index.html exposes an idempotent
__openchamberEnsureNerdFonts hook; TerminalViewport requests it on mount
and waits up to 2s so a cached font is in place before the glyph atlas is
built, while a cold CDN fetch never blocks the terminal. Runtimes without
the hook (VS Code, mobile) resolve immediately, matching their existing
fallback-font behavior.
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
---------
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
A queued message is removed from the queue only after its send resolves,
so between dispatch and resolution it stays visible to every reader — and
a composer submit merges the whole queue into its own send. Over a relay
that window is seconds, long enough to deliver the same message twice.
The queue now tracks which entries are awaiting the server. Dispatchers
skip them, clearQueue retains them so the pending send can still remove
or restore its own entry, and the flag is not persisted because a restart
has no in-flight sends.
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.
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.
Sending a message while a permission prompt is open now denies every
pending permission in the session subtree (optimistically, then formally
via permission.reply reject) and queues the message for next-turn
delivery, mirroring the question-dismiss path from #1740.
- Add dismissOpenPermissionsForSession plus isPermissionRequestNotFoundError
and removePermissionRequestFromChildStores helpers to session-actions
- Extend dismissPermission with not-found cleanup, parallel to rejectQuestion
- Wire handleSubmit to deny permissions and dismiss questions together,
queueing once if either prompt type was open
- Add unit tests mirroring the dismissOpenQuestionsForSession suite
Closes#1958
* feat(ui): add context surface registry and rail switcher
* feat(ui): move git and project notes into context surfaces, embed editor file tree
* feat(ui): replace right sidebar with context surfaces, per-surface panel widths
* refactor(ui): retire legacy main-tab overlays and right-sidebar state
* feat(ui): rail polish, right-docked file tree, terminal surface
* feat(ui): move terminal into context surface, per-surface tab closing, editor empty state
* feat(ui): tune default rail order and activity dot
* fix(ui): keep context panel controls anchored during width animations
* feat(ui): lazy-follow context panel resize with window-level drag tracking
* feat(ui): panel dividers, right-dock tree icon, muted outline folder icons
* feat(ui): restructure git view into changes-first surface with standalone PR surface
- Remove commit/update/pr tabs; git view is always changes + commit
- Promote pull request to its own rail surface with shared repo context
- Move update-branch and re-integrate flows into separate dialogs
- Add PR status chip and repo actions menu to the git header row
- Seed new PR-status entries from resolved sibling remotes to avoid
a false "checking status" state when the PR is already known
- History/graph dialog refresh button, fingerprint global identity icon,
muted outline folder icons follow-ups
* feat(ui): progressive-disclosure PR surface with live checks and pinned chat context
- Segment the PR surface into Overview / Checks / Comments pill tabs with
live badges; merge controls move to the status row
- Live checks segment: progress bar, per-run rows with workflow names,
elapsed timers, expandable failures, auto-refresh while pending
- PR comments and failed checks pin as chat-context drafts (like terminal
selections) instead of sending an immediate message; works on new-session
drafts too
- Shared prContext cache client+server, ETag conditional requests in the
octokit wrapper (304s bypass rate limits), extended checks aggregate
(inProgress/queued/startedAt)
- Resolve gh-CLI auth login for merge-permission checks
- Full-width description editor with matched control heights
* fix(ui): single source of truth for PR checks and status readers
- Derive the checks aggregate from the visible run list and sync it into
the PR-status store so bar, badges, header, and git-view chip agree
- Route PR body hydration through the shared context cache
- Git-view PR chip reads the freshest entry across remote keys
* fix(github): freshness stamps prevent stale cache responses from regressing PR state
- pr/status and pulls/context responses carry a server-side fetchedAt that
survives cache serves
- The status store rejects responses older than the held snapshot (only
clearing the loading flag), and the checks sync adopts the context's
stamp so stale status polls cannot flip fresher derived checks
- Regression test for the stale-response guard
* perf(github): repo-level pull-list cache collapses per-branch PR resolution
- One pulls.list per repo per state per 45s answers every branch (10
worktrees = 1 call, not 10 query fans); in-flight fetches coalesce
- A complete repo list makes a no-PR miss authoritative, skipping the
per-owner head queries AND the Search API fallback (the 30/min killer)
- force refresh bypasses the repo list cache; PR create/merge/ready
invalidate it
* perf(github): back off Search API misses per repo+branch
A branch without a PR re-searched on every poll; with >100 closed PRs the
list miss is never authoritative, so the search fallback still ran and
burned the 30/min search quota. Remember misses for 10 minutes; PR
creation clears remembered misses for the repo.
* fix(github): dedupe re-run check runs to the latest per (app, name)
listForRef returns the superseded completed run alongside its re-run;
GitHub's UI shows only the latest per name. Mirror that in both pr/status
and pulls/context so counts and run lists match github.com.
* fix(ui): address review findings on registry test, surface docs, and PR-context keys
- Rail-order test asserts against the registry itself (was stale after the
'pr' surface landed and failed)
- surfaces DOCUMENTATION.md describes actual behavior: has-content surfaces
hide until content exists; only multi-instance/terminal panes are
keep-alive, singleton surfaces remount and restore from stores
- PR-context cache keys are runtime-scoped JSON tuples; invalidation
compares the directory exactly instead of by string prefix (+ test)
* fix(ui): wrap long unbreakable tokens in check-run details
Annotation messages with long SHAs/URLs overflowed the panel; break-words
on annotation title/message/rawDetails and output summary/text, and the
expanded run body clips instead of widening the panel.
* fix(ui): busy state for context-attach buttons and honest attach labels
- 'Attach failed checks' / 'Attach all to chat' show a spinner and disable
while the context request runs (previously nothing happened for seconds)
- Action labels/tooltips reworded from send-to-agent to attach-to-chat
semantics across all locales
* fix(i18n): Ukrainian attach wording uses 'прикріпити' with proper cases
* fix(ui): runtime-scope PR-view remote caches, correct surfaces doc on preview
- Remote/remote-url caches in PullRequestView are keyed by runtime +
directory so a backend switch never serves another runtime's remotes
- surfaces DOCUMENTATION.md: preview is not keep-alive; preview tabs
remount on switch like singleton surfaces
* fix(ui): rail active color, clearer collapse icon, remove dead bottom-terminal dock
Design-review feedback on the context panel:
- Context rail: icons enlarged 16px -> 18px; the active surface is now
highlighted with the primary color only (no background, no scale
animation), replacing the previous scale-up effect that read as a
resize rather than a selected state.
- Files tree: the icon-only 'collapse all folders' toolbar button now
uses collapse-vertical instead of contract-up-down, which was easily
mistaken for a close button. The labelled 'Collapse all' dropdown item
in the session sidebar keeps its icon since text removes the ambiguity.
- Terminal: removed the leftover bottom-dock expand/close buttons that
rendered in the context-panel terminal but controlled a dock that no
longer exists (nothing toggles it anymore), so the expand button
appeared to do nothing and duplicated the panel-header fullscreen
control. Cleaned up the entire inert layer with it: four useUIStore
fields (isBottomTerminalOpen/Expanded, bottomTerminalHeight,
hasManuallyResizedBottomTerminal), five actions, their persistence,
the MainLayout resize listener that only served the dock height, the
dock-driven refit effect in TerminalView, and the
terminalView.bottomDock.* keys across all 10 locale dictionaries.
Validated: ui type-check and lint clean; messages parity test (2 pass)
and useUIStore contextPanel test (13 pass) green; icon sprite
regenerated via icons:generate.
* refactor: use PR visual state for git header icon
Derives the pull request icon color from a single visual state
Covers merged, closed, draft, blocked, and open PR states
Removes conditional class handling from the git header icon
* 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
Add a shared OpenChamber control service with two thin adapters — a native
`openchamber` tool injected into managed OpenCode, and new CLI commands — so
users can manage parallel sessions, worktrees, and scheduled tasks
conversationally through agents or from the terminal.
Control plane:
- New openchamber-control service owning a fixed action contract:
projects.list, models.list, session list/create/send/fork/status/messages,
and schedule list/create/run/delete/toggle. Session and worktree deletion
and project registration are deliberately not exposed.
- New openchamber-sessions module owning create/worktree/prompt orchestration,
Goal Mode dispatch, wait semantics (initial idle never counts as completion;
timeout and cancellation are failures), and explicit partial-failure results.
- Scheduled-task logic extracted into a service shared by routes, CLI, and the
agent tool.
Agent tool:
- Managed OpenCode gets a materialized plugin registering one typed tool with
a loopback-only callback, per-child ephemeral bearer (timing-safe, never
persisted or logged), and abort propagation into the service.
- The ~1.5k-token schema applies progressive disclosure: short descriptions,
server-side validation returning actionable usage errors, and intent
guardrails — created sessions/tasks are user-facing work (not age
self-delegation); worktree/goal/agent/variant/wait are omit-by-default;
dispatches produce no completion notification, and later result r
to session.messages, which now returns the authoritative sessionStatus.
- session.create without a user-named model picks from favorites/re
send/fork omit the selection and the service reuses the target session's
last user-message model, agent, and variant before falling back t
- An "Agent control tool" setting (default on, Save + Reload to apply)
disables plugin injection entirely.
CLI:
- New `openchamber session`, `schedule`, `projects`, and `models` commands
with automatic instance targeting, --wait/--timeout/--last-assist
worktree flags, and Goal Mode, preserving interactive, non-TTY, --quiet,
and --json contracts. The control HTTP timeout derives from the w
instead of the 4-second default.
UI:
- New built-in "Schedule a Task" starter (/schedule-task) running a
dialogue that defines a task and offers to create it via the tool after
explicit confirmation; Craft a Goal and Feature Planning gain the
handoff offer, and guided starters reserve the question tool for concrete
option choices. Localized in all 10 locales, migrated into custom
starter lists, hidden on VS Code.
- Sidebar shows CLI/agent-created sessions live via the control eve
- openchamber tool calls render with per-action titles and metadata.
Compare normalized attachment MIME types with the selected model's declared input modalities and show a non-blocking warning for incompatible files.
Recheck newly added attachments, restored drafts, async metadata, and existing files after model changes while avoiding warnings when capability metadata is unavailable. Summarize affected filenames and localize the warning across every supported locale.
Add focused modality compatibility coverage, document the composer behavior, and keep model metadata subscriptions stable to prevent startup render loops.
Add picker and MIME support for more source-code, diff, notebook, structured-log, SVG, and Draw.io formats.
Convert Jupyter notebooks into readable text while omitting binary outputs, and sanitize HAR credentials, cookies, sensitive query parameters, and request/response bodies before files enter chat state.
Convert HEIC and HEIF images to JPEG up front, centralize attachment preparation in a focused module, and cover the new validation and transformation behavior with regression tests.
Attachment pickers now share an allowlist for supported file types
Local attachments are normalized to consistent MIME types before upload
VS Code file picker now respects extension filters and larger files are allowed
Improve OpenChamber responsiveness under large session workloads while fixing
cache, synchronization, and persistence correctness across runtimes, projects,
directories, and worktrees.
- prioritize selected and visible sessions during bootstrap and defer
non-critical enrichment work
- reduce redundant message loading, event processing, store publication, and
hidden sidebar work
- prevent stale session and message requests from overwriting newer
authoritative state
- preserve existing data when authoritative fetches fail instead of treating
failures as successful empty responses
- scope session materialization, messages, drafts, queues, todos, pins,
permissions, folders, tabs, Git state, and pull request data by runtime and
directory identity
- harden runtime switching, reconnect, cleanup, mutation reconciliation, and
persisted-state ordering
- preserve live subagent Task linkage when metadata arrives after an older
message request or while streaming parts are suspended
- coalesce overlapping tail refreshes without losing newer refresh demand
- improve cold-session loading by moving deferrable work out of the critical
bootstrap path
- isolate URL authentication, mobile credentials, native secrets, and other
runtime-owned state across endpoint changes
- bound long-lived caches and remove avoidable allocations from event and
rendering hot paths
- limit virtualization to archive collections where it improves rendering
without disrupting active sidebar layout
- stabilize session folders, pin ordering, expanded state, and persisted
sidebar behavior
- open skill files through the same secure editor and outside-workspace grant
flow used by file navigation, including worktree sessions
- expand regression coverage for stale completions, runtime collisions,
reconnect behavior, persistence races, authoritative empty results, and
subagent refresh ordering
- document the updated synchronization, cache ownership, performance, and
runtime-isolation invariants
Replace the legacy terminal flow with a shared authenticated WebSocket
runtime used across web, desktop, relay, and mobile surfaces.
- introduce the v3 terminal protocol with scoped attachments, snapshots,
ordered output, bounded replay history, reconnects, and explicit lifecycle
- harden PTY creation, restart, resize, close, force-kill, idle cleanup,
shell selection, login mode, environment sanitization, and appearance sync
- add runtime-aware terminal APIs with relay authentication and Electron parity
- add a fullscreen mobile terminal workspace with touch scrolling,
long-press selection, safe-area controls, quick keys, and Ctrl/Alt input
- add terminal selection attachments, preview detection, project actions,
shell settings, and localized UI
- harden Ghostty rendering, resize recovery, Unicode handling, block
characters, line height, and stale-row behavior
- remove the obsolete terminal SSE path and update reverse-proxy guidance
- expand terminal runtime, transport, input, selection, and store coverage
- avoid duplicate web builds when preparing mobile assets in root CI builds
Keep the new-session draft open until the backend confirms that the session was created successfully. This prevents a failed request from closing the draft and discarding the user's creation context.
Restore the exact submitted composer text after a new-session send failure and persist it as the draft. Only restore when the composer is still empty or unchanged, so text entered while the request is pending is not overwritten.
Add regression coverage that simulates an offline session creation request and verifies that the draft remains open with its title intact.
The editor font size setting controls code editors and should not also resize the chat composer. Code and conversational input have different readability and layout requirements, so forcing them to share one size is not appropriate.
Commit 5c6546245 removed the editorFontSize reading and inline font-size
styling from ChatInput, calling it 'unused'. However this code was the
core of the editor font size feature (feat/e0229917f) — it applies the
user's chosen font size to the chat input textarea.
Without it, changing Settings > Appearance > Editor font size has no
effect on the chat input textarea. The CodeMirror editor (FilesView,
PlanView) was unaffected because its font size path was not touched.
This reverts the ChatInput portion of 5c6546245, restoring:
- useUIStore selector for editorFontSize
- inline fontSize style on the textarea element
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Adds /craft-goal autocomplete and chat handling for starting a Goal crafting session.
Introduces new Magic Prompts content and localized labels/descriptions for Goal crafting.
Migrates desktop draft starters to include Craft a Goal once and persists the migration marker.
Arm the target button in the composer and the next prompt becomes a goal:
the server keeps the session working toward it (idle tick -> small-model
audit -> continuation) until the objective is verifiably complete, blocked,
or out of budget — even with the UI closed.
Server (packages/web/server/lib/session-goal):
- event-driven loop on the global SSE hub; goal state lives in
session.metadata.openchamber.goal (merge-safe patches, stale-write guard
by goal id), so it survives restarts and syncs to every client for free
- the small-model audit (objective + last assistant turn only, language
pinned to the objective) is the sole termination authority; blocked needs
3 consecutive verdicts, audit outages tolerate one unaudited continuation
then stop the goal as resumable-blocked
- hard stops: optional token budget, auto-continuation cap (Resume grants a
fresh allowance), turn errors; user abort pauses the goal instead of
blocking it, and resuming over an aborted tail nudges immediately
- token accounting as a snapshot of the latest turn (input + cache.read +
output), goal-relative via a creation baseline and segmented across
compactions; a compaction summary skips the audit and continues
- continuations reuse the session's own provider/model/agent/variant
UI:
- three-mode target button (arm / disarm / manage dialog), informational
goal strip with inline pause/resume and an Evaluating indicator, sidebar
state glyph, objective length counter (2000-char server clamp),
read-only completed goals
- goal entry points: composer (sessions and drafts), start-new-session-
from-answer dialog, plan implement dialog (plan content becomes the
objective), scheduled tasks (Run as goal + budget)
- Settings -> Chat -> Goal: feature toggle + default token budget with
three-layer parity (web server, client persistence, VS Code bridge);
VS Code renders goal state but hides the entry points (the loop runs in
the web server only)
Notifications: per-turn "ready" notifications are suppressed while a goal
is active; settling sends one final notification (desktop, web-push, APNs
generic titles with the session name as body) honoring the completion
toggle. Error/question/permission notifications are untouched.
Docs: user guide (session-goals) in all 9 locales + sidebar entry,
scheduled-tasks cross-reference, server module DOCUMENTATION.md.
* feat(settings): add editor font size setting for chat input and code editor
Adds an 'Editor font size' control in Settings > Appearance that sets an
absolute px font size for the chat input textarea and the in-app
CodeMirror editor. Mirrors the existing terminalFontSize lifecycle.
- New store field editorFontSize (default 13, clamp 9-32, step 1) in
useUIStore with narrow selectors at each consumer.
- Persistence wired through appearanceAutoSave, desktop + runtime API
types, and persistence.ts read/normalize.
- Settings UI row (NumberInput) with reset to 13, VisibleSetting union
entry, OpenChamberPage registration, and search index entry
appearance.editor-font-size.
- Applied as a post-zoom absolute override on the chat input textarea
and on the CodeMirror theme's content rule, leaving gutter/line-number
chrome at its existing hardcoded sizes (matches terminal scope).
- All 10 locales translated (en, es, fr, ja, ko, pl, pt-BR, uk, zh-CN,
zh-TW); no English placeholders in non-English dictionaries.
Refs #1325
* fix(codemirror): use unitless lineHeight so it scales with editor font size
The & rule in the CodeMirror theme set lineHeight to 1.5rem (~24px),
which does not scale when editorFontSize is increased (e.g., 28-32px).
This causes overlapping lines at larger font sizes.
Change to unitless 1.5, which scales proportionally with whatever fontSize
resolves to (dynamic prop or --text-code fallback). Matches browser best
practice for proportional leading.
Review comment: https://github.com/openchamber/openchamber/pull/2065
---------
Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
* fix: open mobile model/agent panels on tablet-width Capacitor shells and keep composer taps from dismissing the keyboard
* feat: add iPadOS-style split layout to the Capacitor app
- classify the Capacitor shell as mobile in device detection so shared
surfaces (draft starters, panels) stop falling into tablet branches
- add isIPadApp() and useOrientation() helpers
- iPad: persistent full-height sessions sidebar (mobile sessions surface
inline), Changes/Files in a right sidebar with header shortcut toggles
- animate sidebar open/close like the desktop sidebars and add
finger-sized drag-resize with persisted widths
- anchor the overflow menu and the usage/metadata popover next to their
header buttons regardless of open sidebars
* fix: re-anchor metadata popover on layout shifts and untangle sidebar toggle updates
- recompute the iPad metadata popover anchor via a ResizeObserver on its
wrapper so sidebar toggles/resizes while it is open cannot leave it
misplaced
- move the portrait right-panel close out of the setIpadSidebarOpen
updater into plain sequential state updates
Moves the hidden file input out of the attachment controls so it stays mounted.
Prevents file selections from being lost when the composer variant changes.
Restores reliable local attachment uploads after opening the OS picker.
Keeps mobile composer controls from missing taps during keyboard blur/reflow
Applies the deferred blur behavior to mobile browsers and installed PWAs
Leaves Capacitor behavior unchanged
The dictation overlay is absolutely positioned over the composer, so the
transcript could not expand it — long dictations clipped after two lines.
ComposerDictation now measures the transcript text block (not the flex-1
container, which would feed the composer's own height back and creep a few
px per update) and reports it to ChatInput, which feeds it into the
textarea autosize: same line cap as typing, transcript area scrolls past
it and follows the newest words. Idle/unmount releases the height, and an
idle sibling instance (mobile footer + wrapper engine) can no longer zero
the active one's report.
- 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)