Commit Graph
61 Commits
Author SHA1 Message Date
Wsyjq 70226149ce fix(chat): pin existing-session sends to captured target (#2424)
* fix(chat): pin sends to captured session

* fix(chat): handle runtime cancellation consistently
2026-08-07 00:46:21 +03:00
Cursor AgentandSerhii Dziupin 65a1eec782 fix(ui): keep manual model override after delegated subtask completes
Synthetic subagent-completion nudges were treated as the latest user model
choice and rehydrated the agent default, while setAgent preferred the agent
pin over the session override. Skip synthetic prompts for restore, preserve
manual selection-store overrides, and prefer session agent models in setAgent.

Closes openchamber/openchamber#2404

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-04 12:46:48 +00:00
Serhii Dziupin 577aca0437 feat: add restore/unarchive for archived sessions
Archived sessions had no way back to the active list: the only available
action was "Delete permanently". Add restore per session (sidebar context
menu, Archive page row) and in bulk (sidebar selection bar).

The OpenCode server cannot clear time.archived over HTTP — session.update
only applies the field for a finite number, so an omitted key is a no-op
and null is silently ignored (verified against opencode 1.18.12). Restore
therefore writes time.archived = 0: every client-side reader classifies
archive state by truthiness, so 0 reads as active in the UI, the event
reducer, and the OpenCode app/TUI.

The server's time_archived IS NULL list filter still excludes such rows,
so the global session cache no longer issues an archived:false request
for its active list. Full and per-directory loads now fetch once with
the inclusive flag and split client-side via splitGlobalSessionsByArchived,
which also halves per-directory refresh requests. Directory bootstrap
keeps the server filter because live child stores must not hold archived
sessions; a restored session re-enters its live store through the
authoritative session.updated event.

unarchiveSession/unarchiveSessions follow the archiveSession contract:
wait for server confirmation before reconciling stores, runtime-guard
every reconciliation, preserve partial batch results, and fail loudly
when the server keeps the session archived instead of toasting a
successful no-op.

Closes #2346
2026-08-04 13:21:04 +03:00
Bohdan Triapitsyn 3aeca4893e docs(sync): correct the ownership precedence the fix inverted
Review found the owning documentation still describing the behaviour this
branch replaced, in one case stacked directly above the new docstring saying
the opposite. Holding a session proves containment, not ownership, so every
text that called store membership the authoritative mapping was actively
misleading for the module whose wrong answer misroutes every send.

Corrected in the module docstring, the resolution module's precedence
description, the sync-refs helper it points at, and the sync DOCUMENTATION.md
table and rules.

The debug report built its authoritative value membership-first, so for exactly
the scenario this branch fixes it reported the parent directory and could raise
a source-disagreement alert while routing was in fact correct. It now uses the
same record-first order as the resolver.

The CLI timeout comment claimed the wait and provisioning windows were additive
while the code took the larger of the two. The server provisions the worktree
inside session creation, before it waits for the session to go idle, so they do
run in sequence: the windows are now summed and the tests pin both cases.
2026-08-04 01:50:14 +03:00
Bohdan Triapitsyn bf3186c679 fix(sync): read session ownership from the record, not store membership
A session created in a git worktree while the client was already running did
not render: the message list stayed empty while the prompt and the assistant
reply were both present in the session, visible on any fresh load. Reported as
prompting in a worktree sometimes not working.

Ownership was read from which child store holds the session. That is
containment, not ownership. A project's session list includes the sessions of
its worktrees so the sidebar can group them, so the parent repository holds
worktree sessions too, and whichever store bootstrapped first won. Captured
mid-failure, the two signals disagreed outright:

  owningDirectory  /repo                      <- parent, merely holds it
  recordDirectory  /repo/.worktrees/feature   <- the session's own directory

The parent won, so every fetch was addressed to a directory that does not own
the session, the session id resolved to undefined there, and the requests
failed as /api/session/undefined in a retry loop. The session's own record is
now believed; store membership remains the fallback for a record that carries
no directory.

This also explains why the previous commit alone was not enough: settling the
guessed directory adopted this same wrong value and then cleared the guess,
which prevented any later correction.

Verified against the reproduction rather than by reasoning. Before: three of
four runs never rendered. After, on a clean build with the instrumentation
removed: three of three rendered the reply live, each routed to its own
worktree. Tests cover ownership disagreeing with containment, plus both
directions of the guess promotion.
2026-08-04 01:27:18 +03:00
Bohdan Triapitsyn a44d291cb5 fix(sync): settle a guessed session directory once its owner is known
Selecting a session whose directory this client has not indexed yet routes it
through the active directory. That is a deliberate, documented guess: it keeps
routing usable while the owning store bootstraps, and it is excluded from both
the resolver and persistence.

Nothing settled the guess afterwards. `setSessionDirectory` performs exactly
that promotion, but only confirmed destinations call it — a completed move or a
worktree this client created. A session whose directory the client learned about
later, such as one in a worktree created outside this client, kept the guess
forever: every message fetch was addressed to the parent repository, which does
not own the session.

Captured for such a session before this change, with the session already
indexed and its owning store known:

  routedDirectory          .../worktree/feature
  currentSessionDirectory  /repo            <- guess, never settled
  opencodeClientDirectory  /repo
  conflict                 selected -> /repo

and after:

  routedDirectory          .../worktree/feature
  currentSessionDirectory  .../worktree/feature
  opencodeClientDirectory  .../worktree/feature
  conflict                 null

Directory bootstrap completion is the moment the authoritative directory first
becomes readable, so the promotion runs there. It only ever promotes a guess:
a confirmed selection and a selection that has since moved on are both left
alone, and tests cover both directions.

This removes a real routing split-brain. It does not by itself fix the reported
symptom of a session created mid-session never rendering; that remains open.
2026-08-04 00:42:34 +03:00
Bohdan Triapitsyn 2c52240f8e fix(sync): route sessions by server-confirmed directory, unstick queued sends
Session directory resolution had no precedence contract: the selection-time
directory short-circuited every lookup, and a persisted runtime value was
consulted before the authoritative record. A worktree session selected before
its directory store bootstrapped kept the active-directory fallback, and that
guess was persisted, so it survived reloads and restarts.

Directory resolution now lives in one module and orders sources by whether the
server confirmed the path, not by whether the value is local or synced:
authoritative (the child store that holds the session) > server-confirmed
selection > worktree attachment/metadata (the requested path, pre-canonical) >
remembered. A guessed selection is no longer persisted, remembered, or ranked.
Chips read the same resolution the composer used, so queue keys cannot diverge.

Queued auto-send could strand an item indefinitely: backoff, missing send
configuration, and the recent-abort window all returned without scheduling a
wake-up, so the queue only retried when an unrelated status or directory change
re-ran the effect. A retry scheduler now wakes it at the earliest known time.

A rejected send rolls the optimistic message back while the composer stays
silent for transport failures, which makes it indistinguishable from nothing
happening. Failures are now recorded to a bounded in-memory log surfaced in the
About diagnostics report, alongside a directory-resolution breakdown, plus
__opencodeDebug.diagnoseSessionDirectory() and getRecentSendFailures().

Prompted by a report of worktree prompting silently failing. That failure was
not reproduced locally, so the diagnostics are what will identify it.
2026-08-03 12:51:12 +03:00
Alexandre Reyes Martins d19ff96c02 fix(sync): guard delete actions by default
Follow-up to #2574 and f95f1ab18, which guarded the archive path. The
delete path had the same two defects and worse consequences.

`useSessionUIStore.deleteSession`/`deleteSessions` declared an `options`
parameter and discarded it on both paths, so a caller-supplied runtime
key was a silent no-op. `SessionDialogs.tsx:416` already passes options
today and they never reach the action.

The delete path also never rechecked the runtime. Session IDs are not
unique across runtimes, so a response produced by a previous runtime
could commit `finalizeConfirmedSessionDeletion` against the runtime the
user switched to: evicting an unrelated session from the live and global
stores and calling `cleanupPersistedSessionState`, which erases queued
messages, todos, folder membership, inline-comment drafts, chat draft,
and pins. That is user data loss, not stale cache.

`cleanupPersistedSessionState` already rejects an identity whose runtime
is no longer active, but `finalizeConfirmedSessionDeletion` defeated that
check by passing the live `getRuntimeKey()` at commit time, comparing a
value with itself. It now forwards the captured key.

Adopt the default-on shape from f95f1ab18: `expectedRuntimeKey` defaults
to the active runtime in `deleteSession`, `deleteSessionInDirectory` and
the new canonical `deleteSessions` action, and is rechecked before the
request and before every reconciliation. A `404` still means "already
deleted" and commits cleanup, but only while the captured runtime is
active; after a switch it describes the wrong runtime and the action
reports failure instead of committing.

Also documents the throw contract of `patchSessionMetadata`, a
non-blocking nit raised by the review bot on #2574.
2026-08-02 15:13:06 +00:00
Alexandre Reyes Martins 2e8fc6e192 fix(sync): honor expectedRuntimeKey in archive actions
`useSessionUIStore.archiveSessions` declared an `options` parameter and
discarded it, so any caller passing a captured runtime key got a silent
no-op. The archive path also never rechecked the runtime, letting a
response produced by a previous runtime reconcile the live and global
session stores of the runtime the user had switched to.

Move the batch to a canonical `archiveSessions()` action, add an optional
`expectedRuntimeKey` to `archiveSession()`, `patchSessionMetadata()`, and
`cleanupReviewMetadataBeforeDelete()`, and recheck that key before every
store reconciliation. A guarded batch stops at the first observed runtime
change: server-confirmed sessions stay in `archivedIds` and every
unconfirmed ID is returned in `failedIds`, so existing partial-failure
feedback stays truthful. Callers that pass no key keep prior behavior.

Type the store option as `ArchiveSessionsOptions` instead of
`Record<string, unknown>`, since the loose type allowed the drop.
2026-08-02 11:55:02 +00:00
Bohdan Triapitsyn 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.
2026-08-01 21:16:36 +03:00
Bohdan Triapitsyn b0028283fb feat: expand slash-command goals from command templates
Resolves armed slash-command objectives from authoritative templates before dispatch
Applies OpenCode argument expansion for goal metadata in UI and scheduled tasks
Falls back to the raw invocation when command details are unavailable
2026-07-29 00:47:29 +03:00
Bohdan Triapitsyn 85400459e9 perf: overhaul session loading, caching, and runtime isolation (#2360)
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
2026-07-21 20:52:20 +03:00
Bohdan Triapitsyn 95e3e1cf2e fix(chat): preserve prompt when session creation fails
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.
2026-07-17 10:31:56 +03:00
9624d4b6f6 fix(ui,server): normalize Windows drive letter casing for consistent path resolution (#2154)
* fix(ui,server): normalize Windows drive letter casing for consistent path resolution

Provider settings were failing to persist for specific projects on Windows
because path normalization was inconsistent across the codebase. Some
normalizePath functions uppercased the Windows drive letter (c:\ -> C:\)
and others did not, causing:
  - directoryScoped cache misses (different keys for the same directory)
  - broken model selection in the affected project
  - lost conversation history (sessions could not match their project)
  - false cache hits in resolveConfigDirectory on undefined inputs

This change extracts a single shared normalizePath utility and uses it
from the 5 client sites that were missing the drive letter normalization.
The server-side normalizePathForPersistence is updated to uppercase the
drive letter both before and after safeRealpathSync, so the persisted
path is consistent even when realpath returns a symlink/junction with
a lowercase drive letter on some Windows environments.

Fixes #2109

* test(ui,server): add coverage for Windows path normalization

Address review feedback on #2154:
- Add Windows-platform test for normalizePathForPersistence covering
  drive letter uppercase on input and after realpath resolution
- Add dedicated test suite for the shared normalizePath utility
- Defensive fix: normalizePath now returns null for paths that consist
  only of slashes (\\, ///), matching the documented contract

Refs #2154

* fix(server): scope drive casing normalization to Windows

---------

Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-07-13 01:43:35 +03:00
Bohdan Triapitsyn bb45164ae8 feat: session goals - server-driven goal loop with independent small-model audit (#2148)
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.
2026-07-12 01:23:22 +03:00
Bohdan Triapitsyn 72aaa166f7 fix(session): do not block draft creation on auto-accept 2026-07-12 00:54:46 +03:00
Leonidandbashrusakh 6d7ea82d86 perf(worktree): skip unchanged store updates and content-aware persist (#1992)
* perf(worktree): skip unchanged store updates and content-aware persist

- Add content-aware equality check before setState in all three discovery
  loops (SessionSidebar, ElectronMiniChatApp, MobileApp). Compares
  Map size and per-entry length + element references — avoids triggering
  16+ subscriber re-renders when discovery finds the same worktrees.
- Add content-hash guard to persistWorktreeMap subscription with try-catch.
  Avoids redundant localStorage writes when the Map reference changed but
  the content is identical. Serialization errors are caught and skipped.

Contributes to #1990

* perf(worktree): extract shared worktreeMapsEqual, fix comparison, avoid double serialization

- Extract worktreeMapsEqual() into worktreeManager.ts as a shared utility
  comparing worktree maps by path (not reference identity). This replaces
  the inline reference-comparison logic in all three discovery loops
  (SessionSidebar, ElectronMiniChatApp, MobileApp) that was ineffective
  because readStableProjectWorktrees creates new object instances on
  each call after cache expiry, making item !== value[i] always true.
- Pass pre-serialized JSON to persistWorktreeMap to avoid double
  JSON.stringify on every persist. The subscriber already computes the
  serialized string for the content-hash check; pass it through instead
  of re-serializing inside persistWorktreeMap.
- Deduplicate 3 copies of the same comparison logic into the shared util.

* refactor(worktree): make worktreeMapsEqual generic over path-bearing type

The helper's equality contract is element-wise path comparison,
not anything specific to WorktreeMetadata. Generifying on
`T extends { path: string }` documents the contract at the type
level and keeps it reusable for any future map-of-arrays shape
that has a path field. Call sites stay compatible since
WorktreeMetadata has a required `path: string`.

No runtime change.

* refactor(worktree-store): clarify persist hash name and signature

Drop the optional preSerialized parameter from persistWorktreeMap —
its only caller (the subscriber) already builds the serialized
string for the content-compare, so the dual-path body is dead code.
persistWorktreeMap now takes the serialized string directly.

Rename lastPersistedWorktreeHash → lastPersistedWorktreeSerialized
(the variable holds the full JSON string, not a hash) and drop the
try/catch around JSON.stringify: it cannot realistically throw on
Map.entries() of WorktreeMetadata (no circular refs, no BigInt, no
custom toJSON). The try/catch around setItem stays — it can throw
on quota errors.

No behavior change in the success path.

* docs(worktree): trim repeated call-site comments

Replace the 5-line explanation block (copy-pasted in all three
discovery loops) with a one-liner that points at the worktreeMapsEqual
JSDoc. The '16+ subscribers' framing is also dropped — the helper
itself is general-purpose and the precise number was fuzzy.

* fix(worktree): compare branch in worktreeMapsEqual to avoid stale sidebar label

The helper compared entries by path only. An external git checkout
between discoveries changes branch (and the derived label /
headState) while path stays the same, so the helper returned true
and the store update was skipped — leaving a stale branch label in
the sidebar until the next worktree create/remove or project switch,
since there is no periodic worktree-list refresh.

Compare branch in the inner loop alongside path. Tighten the generic
constraint to T extends { path: string; branch: string } so the
contract is documented at the type level.

worktreeStatus is intentionally NOT compared: status transitions go
through setStoredWorktreeStatus, which writes a fresh Map reference
that the persist subscriber picks up directly. Adding worktreeStatus
to the contract would also force the sidebar to detect status changes
that the persist path already handles, and would couple this helper
to a field whose semantics differ from the discovery path.

Fixes the staleness concern raised by openchamber-bot in PR #1992.

* test(worktree): cover worktreeMapsEqual edge cases

Documents the helper's equality contract and guards against
regressions in the path+branch comparison. Eight cases:

- two empty maps
- identical entries (path and branch match in order)
- same path, different branch — the F1 regression case
- different paths at the same index
- per-project array length mismatch
- project-key count mismatch
- positional reorder (helper is order-sensitive)
- non-first-entry branch difference (subset detection)

All 10 tests in the file pass (2 existing + 8 new).

* ci: retrigger checks

* test(worktree): add benchmark for worktreeMapsEqual and persist path

Documents the actual cost of the PR #1992 optimizations on representative
sizes (1-1000 worktrees per project, 1-50 projects), so future contributors
can reproduce the numbers and detect regressions in the equality helper or
the persist subscriber.

Run with: `bun run packages/ui/src/lib/worktrees/worktreeManager.bench.ts`

Measured on V8 (one example run):
- worktreeMapsEqual early-exit (50×20 with first project differing):
  412 ns/op vs 33,034 ns/op full sweep — ~80x speedup when any project
  actually changed.
- F1 path+branch overhead vs path-only (10×50): +2.3 µs (+15.8%) on a
  full sweep; on the early-exit path the F1 cost is irrelevant.
- Stringify dedup in persistWorktreeMap subscriber: 67% saved (552 µs
  per persist on 10×50). This is the main absolute win of the PR.
- Content-compare guard: 19-29 ns/op, free relative to the stringify it
  gates.

Bench file is standalone (import.meta.main guard) — does not run as part
of `bun test`, does not import React, does not touch localStorage.

---------

Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
2026-07-11 16:06:58 +03:00
Leonidandbashrusakh 9bfc5bf0be fix(chat): enable draft auto-accept before first message (#2045)
* fix(chat): enable draft auto-accept before first message

* fix(test): use supported bun assertions

* fix(chat): apply draft auto-accept before session switch

---------

Co-authored-by: bashrusakh <bashrusakh@users.noreply.github.com>
2026-07-11 15:15:02 +03:00
a1aae30e66 Share project edit form; add per-project default model (#2015)
* feat(chat): migrate history list to @tanstack/react-virtual with deterministic mobile history loading

- Replace virtua with @tanstack/react-virtual for chat history on all
  surfaces: bottom anchoring (anchorTo: end), key-stable prepend
  preservation, and native iOS touch/momentum deferral live in the core
- Patch virtual-core to clamp the render range to real scroll bounds
  during transient adjustments (OpenCode upstream parity)
- Rows render in normal flow inside a translated wrapper so sticky user
  headers keep working; measurement snapshots cached per session
- Pre-write container height in scrollToFn so the browser cannot clamp
  anchor corrections to the stale height; hold the prepend anchor for up
  to 180 frames on mobile while fresh rows settle (cancelled by user
  input; desktop relies on core anchoring alone)
- Adaptive row-size estimate from per-session measured averages; disable
  reveal fade-in for virtualized history rows
- Mobile loads older history only through an explicit localized top
  button: no scroll-position trigger and no post-mount background
  prepend, so every insert happens from a resting state; a quiet-window
  hold defers any stray prepend commit while a touch gesture is active
- Desktop/VS Code keep the seamless scroll-up trigger and progressive
  background prepend

* Share project edit form between settings and sidebar dialog

Extract ProjectIdentityFields and useProjectIdentityForm so the projects
settings page and sidebar Edit dialog share the same layout and behavior.
Rename the project menu action from Rename to Edit, and add per-project
default model selection for new chats with persistence and draft-session
resolution ahead of global defaults.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Unify project edit UI with shared ProjectIdentityEditor shell

Wrap header, fields, and inline Save changes button in one editor
component used identically by settings projects page and sidebar
dialog. Remove dialog-specific footer, title, and padding so both
surfaces render the same layout.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Include Actions and Worktree sections in project Edit dialog

Extract ProjectSettingsPanel with the full settings=projects content
(identity, actions, worktree) and render it from both the settings page
and sidebar Edit dialog. Keep the dialog open after identity save so
users can configure actions and worktrees without reopening.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Narrow project Edit dialog to modal-appropriate width

Use max-w-2xl instead of max-w-4xl so the popup does not inherit the
full settings page width.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Unify project settings subsections and auto-save all fields

- Add shared ProjectSettingsSubsection with consistent titles and dividers
- Auto-save identity, actions, and worktree setup commands (debounced)
- Remove Save changes and Save Actions buttons
- Split worktree into Worktree and Existing worktrees subsections
- Align controls to shared max width across all subsections

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Harden project settings auto-save error handling

- Only update worktree setup snapshot after successful save; toast on failure
- Toast when actions auto-save is blocked by validation for >1s

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Show toast when project identity auto-save fails

Wrap onSave in try/catch and surface settings.projects.page.toast.saveFailed
so rejected parent callbacks are not silently swallowed.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* Fix clearing project default model from settings

Send null instead of undefined when no default model is selected so
updateProjectMeta enters the defaultModel branch and deletes the field.
Apply consistently in prepareSaveData, ProjectsPage, and SessionSidebar.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-07-09 13:54:05 +03:00
Bohdan Triapitsyn de1b85ac56 feat(voice): first-class voice input and local TTS across web, desktop, and mobile (#2018)
Complete rebuild of voice input on a server-authoritative streaming
architecture, replacing the legacy Web Speech / whole-blob / WASM engines
and the dead voice-agent layer (~4k lines removed).

Speech-to-text (dictation):
- Client streams 16 kHz mono PCM16 chunks over /api/dictation/ws with
  seq/ack ordering; buffered audio is retained and replayed on reconnect
- Server transcribes and streams live partial transcripts back;
  segments auto-commit every ~15s with silence suppression and adaptive
  finalization timeouts
- Local provider (default, zero config): sherpa-onnx models in a forked
  worker process — auto-download with progress, staged extraction with
  verification, corrupt-model auto-recovery, idle shutdown after 5 min
- Model catalog with settings picker (accuracy/speed ratings, sizes,
  download/delete): Parakeet TDT v2 (English) and v3 (25 European
  languages, auto-detected), Whisper base and tiny (multilingual, light)
- OpenAI-compatible provider for any Whisper endpoint
- Composer overlay with live transcript, volume meter, timer, and
  cancel / insert / insert-and-send actions; failed transcriptions keep
  their audio for retry or accepting the partial text as-is
- Configurable keyboard shortcut (default mod+alt+v) toggles dictation;
  Enter confirms and Escape cancels while recording
- Overlay is pixel-aligned with the composer (measured footer height,
  matching paddings/typography/gaps) — no layout shift when toggling

Text-to-speech:
- Local Kokoro provider (English, 11 voices) synthesized in the same
  worker via /api/dictation/tts/speak, managed by the shared model
  pipeline; sentence-pipelined playback keeps time-to-first-audio at
  ~1 sentence regardless of message length, and stop cancels in-flight
  synthesis
- Sanitizer keeps inline-code content (strips backticks only), reads
  interword slashes aloud, and removes only absolute file paths

Settings:
- Voice page unified: a single read-aloud toggle owns all playback
  options (the confusing "Enable Voice Mode" is gone); a new "Enable
  voice input" toggle (default on, persisted to settings.json) hides
  the composer mic entirely when disabled

Mobile and transport:
- iOS/Android microphone permissions added (dictation was previously
  impossible on mobile)
- Fixed Android WebSocket upgrades: the Capacitor WebView origin
  (https://localhost) was missing from the packaged-client allowlist,
  403-ing every WS connection — root cause of the old mobile SSE lock,
  which is now removed for all transports

Security and conventions:
- All HTTP routes sit behind the global /api auth gate; the WS upgrade
  explicitly validates the UI session and origin, with oc_url_token
  narrowly allowlisted and covered by tests; the dictation socket mints
  a fresh URL token before connecting
- Routes register before the generic OpenCode proxy; the client goes
  through runtimeFetch/getRuntimeUrlResolver, and runtime switches
  reset the dictation socket
- VS Code deliberately reports dictation as unavailable (no server
  process in that runtime)

CI: workflow Node bumped 20 -> 22 to match the repo engines and fix
better-sqlite3 installs broken by node-gyp@latest on Node 20.

New dependency: sherpa-onnx-node (prebuilt N-API; macOS/Linux x64+arm64,
Windows x64 — Windows-on-ARM falls back to the OpenAI-compatible provider)
2026-07-04 02:48:07 +03:00
Tom RochetteandBohdan Triapitsyn 1505274f94 perf(stores): defer safeStorage writes off the interaction path (#1941)
* perf(stores): defer safeStorage writes off the interaction path

Session switches funnel every persisted store slice through safeStorage.setItem,
and doing those large JSON.stringify writes synchronously blocked the main
thread for over a second. Add a write-behind buffer that:

- Defers each setItem/removeItem to a later task via setTimeout(0) so the
  click-to-paint path is not blocked.
- Coalesces repeated writes to the same key into a single backing flush.
- Serves pending values from memory so read-after-write stays consistent
  within the deferral window.
- Flushes synchronously on pagehide/beforeunload/visibilitychange/freeze so
  deferred state survives tab close, reload, and the mobile freeze lifecycle.

Adds a test covering write deferral, coalescing, and pending read serving.

* fix(stores): defer persisted JSON serialization

* fix(stores): defer direct safeStorage writes

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-30 11:47:52 +03:00
f13f6d5540 feat(#1766): support OpenCode steer delivery / follow-up behavior settings (#1781)
* feat: support OpenCode steer delivery / follow-up behavior settings

Implements issue #1766 — steer delivery mode for mid-turn message
insertion, replacing the old boolean queue-mode toggle with a tri-state
follow-up behavior setting (Steer / Queue / Send immediately).

- Plumbing: threaded optional delivery: 'steer' through sendMessage
  -> routeMessage -> opencodeClient.sendMessage -> promptAsync
- Store: messageQueueStore stores followUpBehavior; migration from
  legacy queueModeEnabled persisted state
- Settings: Chat -> Follow-up behavior shows three radio options
  using existing settings UI patterns
- Composer: when session is busy, a floating queue button remains;
  force-sending a queued message (via chip click) uses delivery: 'steer'
  during a busy session; Steer button intentionally omitted — steer is
  available via the two-gesture path (Enter to queue -> chip to steer)
- Keyboard: queue mode = Enter queues, Ctrl+Enter sends; otherwise
  Enter sends, Ctrl+Enter queues
- Persistence: DesktopSettings, web settings payload, and server-side
  sanitizer handle the new key with legacy fallback
- i18n: follow-up behavior section and option labels in all 9 locales
  plus new chat.chatInput.actions.queue label
- Search: settings registry updated from chat.queue-mode to
  chat.follow-up-behavior

Validation: type-check passes (no new errors), lint clean.

* fix(#1766): make steer mode actually steer

The followUpBehavior === 'steer' branch in handlePrimaryAction and the
keyboard handler was a no-op — both fell into the else branch and sent
without the delivery: 'steer' flag, so selecting 'Steer (insert into
the running turn)' in settings produced identical behavior to 'Send
immediately'.

- handlePrimaryAction: when steer mode is selected and the session is
  busy, call handleSubmit({ delivery: 'steer' }) directly
- Keyboard handler: in steer mode, Enter steers and Ctrl+Enter sends
  immediately (consistent with queue mode where Ctrl+Enter bypasses
  the special handling)

Also removes the unused chat.chatInput.actions.queue i18n key from all
9 locales (it was a dead key after the Steer button was removed from
the composer).

Validation: type-check clean, lint clean.

* refactor(#1766): flatten nested ternary in followUpBehavior resolution

Replace nested ternary with explicit if/else chain per project code style
(CONTRIBUTING.md). Import FollowUpBehavior type explicitly for the new
let declaration.

* feat(chat): drop redundant 'immediate' follow-up mode, keep Queue + Steer

'Immediate' was wire-identical to 'Steer' on a busy session: OpenCode only
supports delivery 'steer' | 'queue' and defaults to 'steer', so an immediate
send (no delivery flag) already steered into the running turn. The three-mode
UI therefore exposed two settings that did the same thing.

Collapse to two modes — Queue (unchanged: client-side queue with edit/reorder)
and Steer. Any persisted/legacy 'immediate' (and legacy queueModeEnabled=false)
now maps to 'steer', preserving prior behavior. Removes the immediate option,
its keyboard branch, the i18n label across all locales, and narrows the
followUpBehavior union to 'steer' | 'queue'.

---------

Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-29 01:28:20 +03:00
8c1a24089d fix(worktree): gate sessions on bootstrap readiness (#1762)
Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-26 12:16:42 +03:00
bashrusakhandLeonid Skorobogatyy 03e6f789a4 fix(git): materialize draft session for generate (#1761)
* fix(git): materialize draft session for generate

* fix(sync): remove redundant draft session side effects

---------

Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
2026-06-26 11:52:39 +03:00
Bohdan Triapitsyn a9dfd32347 fix: avoid stale project binding for new sessions
Keeps implicit new sessions tied to the current directory
Prevents unmatched directories from inheriting the active project
Adds regression coverage for draft project selection
2026-06-24 10:56:54 +03:00
Bohdan Triapitsyn 7f8e04d22f fix(session): prefer current directory for implicit drafts 2026-06-24 00:43:19 +03:00
5f3ef320d2 fix(session): bind new sessions to selected project (#1708)
* fix(session): bind new sessions to selected project

Fix #1521: openNewSessionDraft() always used currentDirectory even when
the user selected a different project. Now prefers the selected project's
path when no explicit directory is provided.

* test(session): add unit test for openNewSessionDraft project binding

---------

Co-authored-by: Leonid Skorobogatyy <bash@opencode.itc.local>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-23 22:14:33 +03:00
f1c9776fde fix: invoke skills selected from the slash command menu (#1607)
Selecting a user-installed skill from the slash menu inserted "/name" as a
plain text message instead of running the skill (#1605). routeMessage only
dispatched a "/name" via session.command when the name was found in the synced
command list (hydrated once at bootstrap) or the commands store (which filters
skills out), so skills installed after startup fell through to a plain prompt.

Consult the live skills store when classifying a slash token. OpenCode registers
every skill as a command (source: "skill"), so a known skill is dispatched via
session.command and its content is injected, matching the existing behavior of
skills that happened to be in the bootstrap snapshot.

Signed-off-by: Bohdan Triapitsyn <artmore@protonmail.com>
Co-authored-by: Ibrahim Khan <ibrakhxn@amazon.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-23 11:51:05 +03:00
bashrusakh 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.
2026-06-18 00:43:16 +03:00
Tom RochetteandBohdan Triapitsyn 71bae089a7 fix: pass workspace directory in Files API requests (#1588)
* fix: pass effective workspace directory in Files API requests

The web Files API used useDirectoryStore.currentDirectory as the
workspace root, but the FilesView's effective directory comes from
useEffectiveDirectory() which can differ (e.g. worktree sessions).
When they diverged the server rejected file reads with 'Path is
outside of active workspace'.

Add directory override to FileReadOptions so callers can pass the
effective directory per-call. The FilesView now passes its root
(from useEffectiveDirectory) through readFile, statFile, image/PDF
URLs, and the desktop image fallback. The server receives the
correct workspace root via x-opencode-directory header or directory
query parameter.

Fixes #1456

* fix: cover files workspace directory regressions

* fix: sync directory store on draft session and forward cache options

The content cache wrapper in RuntimeAPIProvider was dropping the
options parameter (including the per-call directory override) when
making internal statFile and readFreshFile calls during cache
validation and misses. This caused the underlying web API to fall
back to getDirectory() which reads useDirectoryStore.currentDirectory.

Additionally, openNewSessionDraft, setNewSessionDraftTarget, and
overrideNewSessionDraftTarget updated the draft's directory without
ever syncing useDirectoryStore. Since the web API's getDirectory()
reads from that store, it returned the stale previous-project
directory during draft sessions, causing 'Path is outside of active
workspace' errors when opening files.

Forward options through all internal calls in the content cache
wrapper, and sync useDirectoryStore via setDirectory() whenever the
draft session directory changes.

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-06-15 11:00:02 +03:00
Bohdan Triapitsyn e372c8d8cb perf: instant startup via cache hydration + decoupled readiness (#1650)
* perf(startup): hydrate providers/agents from cache (stale-while-revalidate)

Persist last-known provider/agent snapshots instead of stripping them, so the
model/agent pickers paint instantly on cold start. Freshness is preserved by the
background refresh in initializeApp() and activateDirectory() (which overwrite on
success) and by the existing provider/agent config-change subscriptions, so the
prior stale-provider regression stays fixed without blanking the UI during fetch.

* perf(startup): cache directory session list for instant sidebar

Persist a capped slice of each directory's session list and seed the child store
from it on creation, so the sidebar paints chats immediately on cold start.
Bootstrap phase-3 loadSessions overwrites with the fresh list; its empty-list
race guard preserves the seeded sessions during OpenCode warmup.

* perf(startup): hold API requests through OpenCode warmup instead of 503

The readiness gate returned 503 the instant OpenCode wasn't ready, pushing the
client into an exponential-backoff retry loop (500ms -> 1s -> ...) that wasted
seconds of cold-start time and could fail bootstrap outright. Now hold the
request and poll readiness up to a bounded window so the first call succeeds as
soon as OpenCode is up (typically sub-second); still 503 fast past the window so
a genuinely-down server doesn't hang. Adds coverage for both paths.

* perf(startup): surface cached providers/agents in pickers (optimistic readiness)

The model/agent pickers gated purely on isInitialized, so they showed
"Loading…" for the entire init round-trip even when provider/agent data was
already hydrated from cache — making the persisted-cache work invisible. Treat
the pickers as ready as soon as cached providers are present (stale-while-
revalidate), so they paint last-known models/agents instantly and refresh in the
background. First-ever launch (no cache) still shows Loading until init.

* perf(startup): don't abort directory bootstrap on transient phase-1 failure

A failed initial path.get OR session.status aborted the whole directory
bootstrap, stranding it in loading and skipping phase 2/3 (session load).
session.status is live data the event pipeline keeps current, and path.get is
tolerable once a project is resolved from global state. Now only a total
failure (or path.get failing with no resolved project) aborts, so the sidebar
and chat keep advancing and loading sessions through warmup hiccups.

* perf(startup): don't bootstrap directories from archived sidebar rows

Each sidebar session row called useDirectoryStore(dir), which defaulted to
bootstrap:true and triggered a full directory bootstrap. Archived sessions point
at dozens of (often deleted) worktrees, so on startup this fired a session-list
fetch + 6x2s empty-retry storm per dead directory (the logs the user saw). The
store ref there is only read on-demand via getState() in export handlers, never
subscribed, so archived rows don't need it bootstrapped. Add a { bootstrap }
option to useDirectoryStore and skip bootstrap for archived rows; active rows
still bootstrap so live cross-directory session/status keeps aggregating.

* perf(startup): stop empty-session bootstrap retry storm on web/desktop

The post-bootstrap retry re-ran the full directory bootstrap 6x2s whenever the
session list came back empty, on the theory that empty meant OpenCode wasn't
ready. But loadSessions already retries transient failures twice over
(listGlobalSessionPages throws on 5xx and retries internally), so on web/desktop
an empty result is authoritative — the directory genuinely has no sessions (e.g.
deleted worktrees referenced only by archived sessions). That produced the
dozens of '[bootstrap] sessions empty ... 6 attempts; giving up' log storms.
Gate the retry to VS Code, where the bridge can return an empty 200 during
warmup that the inner retries can't catch.

* perf(startup): scope provider/agent config to project (worktrees inherit)

Providers/agents/defaults are project-level, but were keyed per directory, so a
worktree fetched and cached its own snapshot — duplicating the parent project's
load (the trace showed initializeApp loading the worktree and activateDirectory
loading the project concurrently, ~8s of redundant background work).

- resolveConfigDirectory() maps a worktree to its owning project; loadProviders
  /loadAgents/activateDirectory now key by it, so a worktree reuses one shared
  project snapshot. activateDirectory resolves up-front so activeDirectoryKey and
  the snapshot key always match (picker stays consistent); the OpenCode working
  directory is unaffected.
- Add a 30s runtime freshness guard so the stale-while-revalidate background
  refresh skips re-fetching config that was just loaded (initializeApp then
  activateDirectory for the same project), and to avoid churn on rapid project
  switches. Config-change invalidation clears the snapshot, which bypasses the
  guard, so freshness never masks a needed refresh.

* fix(sidebar): default archived sessions to hidden to avoid startup flash

useSessionDisplayStore defaulted showArchivedSessions to true, so on startup
archived sessions rendered by default and then vanished once the persisted
preference rehydrated to hidden — a visible flash. Default to hidden so the
pre-hydration state is the quiet one; users who opted into showing archived keep
their persisted true (default change doesn't override persisted state).

* perf(startup): persist worktree->project mapping to kill cold double-load

The worktree->project map (availableWorktreesByProject) is populated by async git
discovery, so it isn't ready when initializeApp runs — a worktree's first config
load couldn't resolve to its project and duplicated the project's provider/agent
load, saturating OpenCode during cold start (the source of the slow first
createSession/send the user observed). Cache resolved worktree->project mappings
to localStorage so resolveConfigDirectory resolves synchronously at init on
subsequent launches; the project is loaded once and activateDirectory hits the
freshness guard. worktree->project is immutable so a cached entry is safe; live
resolution still populates/corrects the cache.

* perf(startup): persist worktree map for instant sidebar + first-launch keying

Worktree discovery is async (git), so availableWorktreesByProject was empty at
startup: the sidebar worktree list appeared late, and useConfigStore couldn't
resolve a worktree to its project on the first launch (causing the cold
worktree+project double-load). Persist the discovered worktree map to
localStorage and seed it synchronously on store init (stale-while-revalidate:
discovery refreshes in the background via the existing setState, which now
write-through persists). The sidebar paints worktrees instantly and
resolveConfigDirectory resolves the project from the very first launch.

* perf(startup): coalesce concurrent duplicate OpenCode reads in runtimeFetch

On cold start the sync bootstrap and the config store independently fire the same
idempotent reads (providers, config, path, agents, project) concurrently with no
shared dedup, saturating the single OpenCode process and delaying work queued
behind it (e.g. createSession). Coalesce genuinely-concurrent identical GETs to
those read endpoints at the transport layer so OpenCode does the work once; each
caller receives an independent response clone. Tightly scoped: GET only,
allowlisted read paths, never event streams, never a signal-bearing request (so
one caller's abort can't cancel the shared fetch). Entries clear on settle, so it
only shares overlapping in-flight requests — never a stale response.

* perf(startup): cache git branches so the draft branch selector paints instantly

The branch selector above the composer was the slowest-loading element: it's
gated behind a cold 'git branch' fetch (useGitStore, not persisted). Cache the
per-directory branch list to localStorage and seed the store on init (with
isGitRepo:true so the selector's gate passes), and write the cache on every
successful fetchBranches. The ChatInput draft-branch effect now refreshes on
staleness (>30s) rather than mere absence, so seeded branches show immediately
and still refresh in the background without a spinner — no stale-forever
regression. Only the branch list is cached; status/log/diff are untouched.
2026-06-15 03:16:34 +03:00
Bohdan Triapitsyn 9111611bdc fix: start draft sessions from default model/agent and honor OpenCode default_agent
A new draft session inherited the previous session's model/agent instead of
resetting to defaults, because opening a draft restored the directory snapshot
without re-applying the startup default cascade. When the prior session ran in
a worktree, defaults were resolved against the worktree directory's provider
list, which omits project/global-scoped providers, so the default agent's model
fell back to opencode/big-pickle.

Resolve the default agent/model via a shared cascade (settings default ->
OpenCode default_agent -> build -> first), resolve the model from the agent's
pinned model/variant or OpenCode's config model, and activate the project's
config (not the worktree's) when opening a draft.
2026-06-14 21:36:05 +03:00
Bohdan Triapitsyn e53c3da223 fix: keep tray session switches in sync
Open tray sessions from the correct directory
Allow remote instances to update the tray
Update the active project when switching sessions
2026-06-10 02:24:47 +03:00
Bohdan Triapitsyn 1f9769a932 feat: add session review handoff flow
Introduce a desktop/web-only /handoff-review flow that generates a handoff from the active implementation session, creates or reuses a separate review session in the same directory, and links the pair through hidden OpenChamber session metadata.

Add review flow orchestration, metadata helpers, magic prompts, localized command/action labels, session metadata create/update support, and assistant message transfer actions for sending reviewer feedback back to the implementer or implementation responses back to the reviewer.

Review sessions are ordinary sessions, not child sessions. The flow avoids exposing session IDs or routing metadata to agents, hides review controls on mobile and VS Code, hides unrelated assistant actions inside review sessions, cleans up stale metadata where possible, and uses the optimistic send path so cross-session sends scroll like normal composer messages.
2026-06-07 01:22:40 +03:00
Bohdan Triapitsyn e0113c637d feat: support fast worktree-backed session flows
Add a directory-created fast path for worktree creation so session and send flows can continue once the target directory exists while Git attachment and bootstrap finish in the background.

Track bootstrap status explicitly in shared UI contracts, including pending, ready, and failed states. Background watchers now surface failures and timeouts, update stored worktree metadata, and keep web and VS Code runtime behavior in parity.

Move GitHub issue/PR worktree sessions and assistant-answer fork sessions onto the unified send path so provider, model, agent, and variant selections are preserved. The assistant-answer fork dialog can optionally create a worktree outside VS Code.

Make worktree deletion dialogs close after linked-session cleanup while removing the worktree in the background, and clean up failed fast-create artifacts safely without recursively deleting user or agent-written files.

Validation: bun test packages/ui/src/lib/worktrees/worktreeBootstrap.test.ts packages/ui/src/lib/worktrees/worktreeManager.test.ts; bun run type-check; bun run lint.
2026-06-06 23:25:39 +03:00
Bohdan Triapitsyn e9d7f913d9 fix: route new prompt sync to target directory
Keeps optimistic prompt state in the session directory
Routes live assistant part updates using upstream event payloads
Adds regressions for startup session switch races
2026-06-04 13:32:16 +03:00
Bohdan Triapitsyn 04c9aa57dd fix: show streaming chat messages after startup
Read chat state from the active session directory
Align status rows with the active session
2026-06-03 22:38:15 +03:00
Bohdan Triapitsyn a649ba8df4 fix: unblock initial new session prompts 2026-06-03 14:51:17 +03:00
Bohdan Triapitsyn 6a88cd09cc feat: add dialog for "Start new session from this answer" (#1501)
* feat: add dialog for "Start new session from this answer"

Replace the one-click fork action on assistant messages with a dialog
(ForkSessionDialog) that lets the user pick model, thinking level, and
agent, plus edit the instructions sent to the new session.

The instructions field is prefilled with the previous fixed fork prompt
and is mandatory. The composed message is now fully visible (no synthetic
preface): the user's instructions sit above a short fixed connective that
opens the assistant content. createSessionFromAssistantMessage takes the
chosen execution params instead of reading from config.

Also fix TodoSendDialog visuals: narrower vertical layout, model trigger
no longer stretches with centered text, and the agent/thinking dropdowns
portal to body so opening them no longer nudges the dialog height. Extract
the shared ThinkingPill into its own component.

* fix: address review feedback on fork session dialog

- Fix "bellow" -> "below" typo in the fork content preface (now user-visible
  since the message is no longer synthetic)
- Reset ForkSessionDialog state only on open transition, reading the config
  store snapshot via getState() so background store refreshes can't discard
  in-progress instruction edits
2026-06-02 12:53:30 +03:00
Bohdan Triapitsyn 2031e3b4a8 Decouple bundled UI from runtime API and add remote instance tooling (#1228)
Add a packaged-client runtime boundary so the shared UI can talk to local,
desktop, remote, and VS Code runtimes through the right transport instead of
assuming one same-origin web server.

Centralize OpenChamber-owned API access behind RuntimeAPIs, runtimeFetch, and
runtime URL helpers, while keeping official OpenCode traffic on the SDK path.
Support runtime switching, remote host selection, desktop client credentials,
and headless connection links for pairing packaged clients with remote
OpenChamber servers.

Harden the new auth model by moving long-lived client tokens out of browser
URLs, introducing short-lived scoped URL tokens for browser-owned transports,
restricting URL-token access to explicit readable/realtime routes, and making
client-token management session-scoped or self-scoped as appropriate.

Update browser-owned assets and preview proxy flows to work with the split
runtime model, including authenticated project icons, preview token propagation,
CSP-safe preview bridge injection, and preview proxy auth that survives
short-lived URL-token expiry.

Tighten Electron security boundaries for packaged clients by gating privileged
preload state to trusted origins and requiring explicit confirmation before
connect deep-links import or switch remote runtimes.

Also refresh agent guidance and project skills so future runtime/API, auth,
preview, UI, CLI, settings, locale, and drag-to-reorder work follows the new
architecture.
2026-06-02 00:43:05 +03:00
Bohdan Triapitsyn f585ca2f60 fix: send multi-run slash commands correctly
Multi-run now uses the normal chat routing path
Slash commands are sent as commands instead of plain text
Type-check and lint pass
2026-05-25 16:00:48 +03:00
Bohdan Triapitsyn 73ab36bc91 fix: send queued messages to the original session
Prevents queued messages from being sent to a newly opened session
Adds explicit session targeting for queued auto-send
Covers the behavior with a unit test
2026-05-25 00:29:52 +03:00
Bohdan Triapitsyn 51c8d52ab5 perf(ui): improve VS Code chat session switching
Improve chat session switching and history pagination, with most of the aggressive limits scoped to the VS Code webview where the freezes were observed.

Session history loading and pagination:

- Reduce the VS Code message page size to 30 records so switching sessions does not immediately hydrate large histories into the webview.

- Keep manual Load older messages in VS Code fixed at 30 records per request instead of growing the request size over time.

- Add a bounded VS Code initial-tail expansion path from 30 to 50, 80, and 120 records only when the initial page has no user-message turn boundary, preventing large final turns from rendering as an empty chat.

- Lower the normal web message page size from 200 to 150 for a mild shared optimization without adopting the aggressive VS Code limits.

- Make session pagination metadata reactive per session so ChatContainer receives cursor updates from materialization and reconnect paths without requiring a switch away and back.

- Write pagination metadata before publishing newly materialized messages so the first render sees the correct has-more state.

- Store cursor information from direct materialization and reconnect message fetches in the shared session prefetch metadata cache.

VS Code cache and memory pressure reductions:

- Use a shared per-directory session recency map so cache eviction is based on app-level recency instead of whichever useSync instance happened to run.

- Limit VS Code warm session cache retention to 4 sessions and evict heavy inactive message caches after switching away from a large session.

- Disable sidebar session prefetch in VS Code because warming extra sessions was increasing webview memory and GC pressure during navigation.

- Remove dropdown background message prefetch so opening the switcher does not start additional session materialization work.

- Drop cached session-message-record snapshots when evicting session data so stale derived records do not remain after the raw session cache is cleared.

- Add bounded LRU caching for session message record snapshots, with much smaller VS Code limits and a VS Code cap that avoids caching snapshots above 30 messages.

- Bound the turn-window model cache in VS Code and avoid caching turn models for sessions above the VS Code message-page size.

Chat render-path reductions:

- Reuse ChatContainer's already-materialized message records in plan detection instead of adding a second active-session message subscription.

- Add a no-op guard when marking session plan availability so repeated detections do not create new Map references and fan out renders.

- Add no-op guards for session switcher and dropdown open state updates to avoid unnecessary store updates and renders.

- Convert several session-specific hooks to useSyncExternalStore with empty-session no-subscribe behavior so empty IDs do not subscribe to broad store updates.

- Remount the chat viewport when the current session changes, isolating per-session viewport and list state.

- Change the virtualized message-list fallback to render only a tail window when the virtualizer has not produced rows yet, instead of rendering an entire large history.

VS Code layout and header improvements:

- Remove the broad useSessions subscription from the VS Code layout header path and subscribe only to the active session title and initial-session existence.

- Unmount the compact VS Code session sidebar when the user is in chat view instead of keeping the hidden session list mounted and subscribed.

- Compute the latest assistant model and latest context-token usage in a single reverse scan of current-session messages instead of scanning the same list twice.

- Remove switcher git-status warmup work so the switcher reads already-loaded branch labels without starting extra background git status requests.

Markdown and file-reference safeguards:

- Skip expensive syntax highlighting for very large code blocks, with a 200-line cap in VS Code and a softer 1200-line cap in web.

- Add an LRU cap to file-reference stat lookups so the cache cannot grow without bound across many rendered messages.

- Limit the number of file references annotated per render to 40 in VS Code and 200 in web to prevent large assistant outputs from spawning too many stat checks.

- Clear file-link annotations when file-reference mode is disabled so stale attributes and handlers do not remain on previously annotated nodes.

Assistant-message action and preview reductions:

- Skip preview URL scanning on VS Code, mobile, and mini-chat surfaces so assistant text and tool output are not scanned where the preview action is unavailable.

- Skip Save-as-Plan project lookup on VS Code, mini-chat, and mobile surfaces.

- Hide Save-as-Plan and Start MultiRun assistant-message actions on VS Code, mini-chat, and mobile surfaces.

- Resolve the current session directory on demand for assistant actions instead of subscribing each assistant message to the full session list.

Tool and task rendering optimizations:

- Prefer finalized task metadata summaries without fetching child-session messages when the summary is already present.

- Avoid polling or final-fetching task child sessions once a final metadata summary is available.

- Use VS Code-specific task child fetch limits of 30 records for initial, active, and idle fetches.

- Parse diff stats by scanning patch text line-by-line instead of splitting large patches into arrays.

- Count write-tool lines by scanning content instead of allocating a split array for large files.

- Avoid trimming large patch strings just to test whether they contain content.

- Memoize diff and write statistics so unchanged tool parts do not recalculate them on every render.

VS Code bridge improvements:

- Return JSON and text proxy responses through the VS Code bridge as bodyText instead of base64 so the webview avoids synchronous base64 decoding for common API responses.

- Keep binary responses on the base64 path while making bodyBase64 optional in the bridge contract.

- Strip content-length, content-encoding, and transfer-encoding headers from proxied responses because the bridge reconstructs the Response body.

Validation:

- bun run type-check

- bun run lint

- bun run vscode:build
2026-05-21 15:45:44 +03:00
Erman HAVUÇandBohdan Triapitsyn 4384d7a8d3 fix(chat): restore file attachments when reverting or forking messages (#1288)
* chore: add .worktrees/ to gitignore for worktree workflow

* fix(chat): restore file attachments when reverting or forking messages

* fix(chat): address review findings in attachment restoration

- Move filePartsToAttachments helper below all imports into its own
  'Attachment helpers' section (was incorrectly placed between imports)
- Compute size from base64 data URL for pasted screenshots instead of
  hardcoding 0; file:// URLs keep size 0 which formatFileSize suppresses
  gracefully
- Capture prevAttachedFiles before optimistic mutation and restore on
  SDK revert failure
- Always use source: 'local' for restored attachments so they are
  visible and removable in the composer regardless of URL scheme

* fix(chat): resolve merge conflicts and restore attachments in fork

- Merge upstream main which already added attachment restoration to
  revertToMessage via addRestoredAttachment
- Add !isSyntheticPart filter to revertToMessage file part collection
  (upstream was missing this)
- Add attachment restoration to forkFromMessage (was not fixed upstream)
- Use upstream's addRestoredAttachment approach for consistency

* fix(chat): clear restored attachments when opening new session draft

Reverted-message attachments (and any other pending attachments in the
global input store) were carrying over to the new session input because
openNewSessionDraft did not clear attachedFiles.

Clear attachedFiles in openNewSessionDraft, which is the navigation-away
event for new sessions (it already sets currentSessionId: null). This
matches the semantics of starting a fresh conversation.

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-17 15:25:59 +03:00
youfchandBohdan Triapitsyn 9c71119835 feat(ui): revert indicator with undo/redo, message list, and attachment restore (#1279)
* feat(ui): revert indicator with undo/redo, message list, and attachment restore

Add bidirectional undo/redo with redo stack navigation and expandable
revert indicator in StatusRow. List reverted messages with inline
revert/fork actions. Restore file attachments on revert.

- Add revert indicator with count, expandable popover, per-button spinner
- Restore file/image attachments when reverting to a message
- Clear previous attachments on revert when target has none
- Restore-all bypasses redo stack for direct unrevert
- Survive popover close/reopen without losing loading state
- Fix flash when sending message after revert
- Fix count disappearing on browser refresh
- Remove dead code (undoStack, getRevertHistory, fork-from-here)
- Fix toast grammar (Undid -> Reverted, Redid -> Redone)
- Add i18n keys for revert popover across all locales

* fix(ui): Greptile review fixes and i18n for revert toasts

- Fix handleSlashUndo toast always showing [No text] (moved getSyncParts before revertToMessage)
- Add inputStore rollback in revertToMessage catch (restore attachments + text on API failure)
- Change portal ID to per-session (prevent multi-session collisions)
- Add i18n keys for undo/redo/restored toasts across all 7 locales
- Use formatMessage in store for localized toast strings

* fix(ui): add missing sessionId to click-outside effect deps

* fix(ui): remove unused sessionActions import in ChatMessage

* fix(ui): derive revert dock from session state

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-16 16:44:37 +03:00
Bohdan Triapitsyn 5313494e5a fix: restore agent mentions and child permission auto-accept
Sends tagged agents as OpenCode agent parts
Auto-accepts pending child-session permissions from parent sessions
2026-05-14 15:30:10 +03:00
Jake b4cbd7f0d6 Fix: archived session bulk delete for VS Code extension (#1256)
* fix/archived sessiongroup bulk delete

* Remove in line comment

Signed-off-by: Jake <101855602+jjdubski@users.noreply.github.com>

* small fix for lint

* indentation fix, restore to original

---------

Signed-off-by: Jake <101855602+jjdubski@users.noreply.github.com>
2026-05-13 11:12:30 +03:00
Bohdan Triapitsyn c827d6b8df fix: restore messages after redo
Refetches session messages before applying redo
Aligns undo/redo navigation with OpenCode behavior
Keeps restored messages visible after unrevert
2026-05-08 14:53:48 +03:00
jwcrystalandBohdan Triapitsyn 03c9065c90 fix: preserve per-session scroll position on session switch (#1083)
* fix: restore scroll position when switching chat sessions

When switching between chat sessions, scroll position now restores to
where the user left off instead of always jumping to the bottom.

- Save pixel-level scrollPosition (scrollTop/scrollHeight/clientHeight)
  in viewport store on every scroll event
- Add restoreSavedScrollPosition to timeline controller for ratio-based
  restoration (handles content size changes between visits)
- Suppress intermediate scroll events during session transition with an
  explicit flag, cleared deterministically after restore completes
- Cancel in-flight animations/follow-loops on session switch
- Preserve scrollPosition when session-ui-store rebuilds SessionMemoryState

* fix: keep streaming sessions pinned on restore

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-01 12:39:04 +03:00
septonbarth 2bc7b9b178 fix(ui): preserve slash command message ids (#1031) 2026-04-27 12:09:56 +03:00