Commit Graph
192 Commits
Author SHA1 Message Date
Bohdan Triapitsyn 2e49e44205 feat(ui): let users hide context rail surfaces
A trailing configure button on the rail — outside the sortable list and the
digit shortcuts — opens a dialog that toggles each surface. The choice is
stored as the hidden set so newly added surfaces appear for everyone, and
the rail and the mod+alt+digit switcher share the same visibility filter, so
badges and shortcuts always agree. Hidden surfaces keep their data and stay
reachable from the command palette.
2026-08-26 16:18:15 +03:00
Bohdan Triapitsyn f2ec9b1003 feat(ui): add session history navigation, permission keys, and review shortcuts
mod+alt+arrows step through this window's session-open history (or between
neighbouring tabs when session tabs are on), mod+k r renames the current
session inline, and mod+k a toggles permission auto-accept. Pending
permission cards respond to alt+enter / alt+shift+enter / alt+backspace with
the keys printed on the buttons. The commit message box commits on
mod+enter, alt+arrows step the diff review between changed files, and the
command palette gains search-only commands for rare actions so the initial
list stays short.
2026-08-26 15:46:44 +03:00
Bohdan Triapitsyn 7977842e1e feat(ui): redesign the default shortcut layout around a mod+k leader
Single chords stay for everyday actions; open/go actions move to two-step
mod+k sequences; held mod+digit switches header session tabs and held
mod+alt+digit switches context panel surfaces. Rare actions leave the
shortcut schema for the command palette, every remaining action ships with
a default binding, and stored overrides from the old layout reset once.
Key matching now follows the physical key on non-Latin layouts and for
Option-modified digits on macOS, including in the recording dialog.
2026-08-26 14:52:31 +03:00
Bohdan Triapitsyn f1c3870909 feat(ui): land the centralized shortcuts core from #2532 with review fixes
The schema/config/bindings/registry/dispatcher module, useKeybind hooks,
recording dialog, reworked shortcuts settings page, help dialog, and the
localized action labels — re-based onto current main rather than merged
(the branch predates 440+ commits including the session-tabs shortcuts).

Review fixes applied on top of the original:
- close_session_tab (alt+w) joins the schema with labels in every locale;
  it shipped on main after the PR's base and would otherwise silently die.
- switch_context_surface's special-case in conflict resolution is now a
  declared prefixStyle config property instead of a magic id string.
- Duplicate handler registration warns in dev builds.
- The risky-browser-shortcut warning inspects every chord and covers
  mod+q/d/h/j/o/u plus mod+shift+w/q.
- The dispatcher remembers which target armed a two-chord prefix so the
  window-level completion handler can distinguish a deliberate sequence
  from typing in an editable field (guard lands with the dispatch hook).
- Schema tests: unique normalized default bindings enforced, and the
  flat-file-era override format proven to keep resolving.
2026-08-26 10:47:44 +03:00
Bohdan Triapitsyn a3813f57e9 feat(header): session tabs — a horizontal working set of open sessions
Web/desktop header replaces the single session title with a strip of
soft pill tabs, one per session the user has opened (sidebar, palette
or deep link — opening anywhere adds a tab once). The active tab is the
familiar title block — rename, meta row and the full session menu —
inside a gently selected pill; a brand-new draft shows as a transient
pill until its session exists. Inactive tabs show the title with a
hover-revealed "..." menu (close tab, close other tabs, copy id) that
nudges the text like sidebar rows, and close by middle-click too.

Tabs drag to reorder, scroll behind the right-side header buttons with
soft fade edges, respect the reserved window-controls inset, and
persist across reloads. Closing the active tab activates its neighbour
(or opens a new draft when it was the last). Tab ids whose session is
not in the loaded list stay stored but hidden, so a partial session
list never destroys the working set. VS Code keeps the plain title;
mobile is untouched.
2026-08-24 17:07:09 +03:00
Bohdan Triapitsyn eb03ffaa80 feat(files): comment on selection in read-only markdown previews
Selecting text in a rendered markdown preview shows a Comment pill;
attaching stores a file-quote context draft carrying the file path, the
selected fragment (not whole lines), the user's comment, and a
best-effort source line range resolved by anchoring the fragment's
first and last lines in the raw content — a partially located fragment
gets no range rather than a misleading one. The fragment stays
highlighted while the comment input is open, using the selection
overlay rects shared with chat quote comments, and the preview's
native selection color now matches chat messages. file-quote flows
through the same context contract: composer chip previews, the message
context card, and the metadata round-trip.
2026-08-24 01:21:35 +03:00
Bohdan Triapitsyn 538b532fb1 feat(composer): context chip previews with inline comment editing
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.
2026-08-24 00:16:26 +03:00
Bohdan Triapitsyn 1ae3aeff5f feat(chat): structured context attachments with metadata round-trip
Every user-attached context item (diff/file/plan comments, terminal
selections, browser annotations, PR comments and failed checks, linked
issues/PRs, and new chat-quote comments from the selection menu) is now
sent as its own synthetic text part carrying an openchamberContext
metadata payload. The model-facing text keeps the previous wording; the
timeline reads the metadata back and renders each item as a context card
instead of raw prompt text. Legacy messages still render via the old
text sniffing.

The selection menu gains a Comment option with an inline multiline
input, the quoted fragment stays highlighted while commenting, and on
mobile the input overlays the composer pill by rendering inside the
composer form. Add to chat is renamed Add to input; the menu is
restyled and the mobile Copy tile removed. Terminal drafts move their
terminal id out of the language field (persisted-draft migration v3),
and the dead preview-console source is deleted.
2026-08-23 23:40:32 +03:00
ChangeHowandBohdan Triapitsyn 3a78d86248 fix(ui): open app deep links from chat after confirmation (#2932)
* fix(ui): open app deep links from chat after confirmation

DOMPurify's default URI policy stripped href from anchors with custom
application schemes (obsidian://, vscode://, ...), so every app link
rendered in chat was dead across web, desktop, VS Code, and mobile.

- Classify safe app-link schemes in lib/url.ts (browser-handled,
  scriptable, webview-internal, network, and self-deep-link schemes
  stay excluded) and let openExternalUrl accept them
- Keep app-link hrefs through the markdown sanitize hook
- Intercept app-link clicks in the markdown renderer and route them
  through a confirmation dialog (Trust and open / Open once, dismiss
  to cancel) mounted in the desktop/web app root and the mobile shell
- Persist per-device trusted schemes in a zustand store; trusted
  schemes open without asking again

* feat(settings): manage trusted app link schemes in General

Add an App links section to Settings > General listing the application
schemes trusted on this device with a delete action; removing a scheme
restores the confirmation dialog for it. Register the section in
settings search.

* fix(ui): enforce app link confirmation

* fix(ui): handle app links by runtime

* fix(vscode): keep app links unsupported

* fix(settings): clarify trusted app links

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-08-23 01:53:21 +03:00
Jay GuptaandBohdan Triapitsyn 46426e8495 feat(chat): /btw — side questions in a temporary forked session (#2796)
* 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>
2026-08-23 00:40:06 +03:00
Bohdan Triapitsyn 735a9463ca fix(chats): use a dedicated empty message
Show a localized Chats-specific empty state instead of reusing the project workspace session message.
2026-08-22 11:41:59 +03:00
Bohdan Triapitsyn 0b01f5ae2d feat(diff): add branch scope to context panel diff view
Show every change on the current branch relative to its base in the
Changed/Staged/Last turn dropdown. The base comes from the branch's
reflog record or an explicit per-branch user choice (persisted), never
a main/master guess; when git has no record the user picks a base once
from a searchable branch list.

- server: GET /api/git/branch-base (reflog-derived base),
  GET /api/git/range-files (name-status -z with rename/copy
  destination paths and -C copy detection)
- shared UI: optional getBranchBase/getGitRangeFiles runtime APIs
  with boundary parsing; persisted per-branch overrides keyed by
  runtime+directory+branch
- DiffView: branch scope with confirmed-unavailability coercion of
  persisted tabs (detached HEAD, default-branch checkout, metadata
  settled without a default), range-invalidated diff cache guarded
  against stale completions, bounded branch-metadata retry, read-only
  diff actions in branch scope; hidden in VS Code
- helper module branchDiffScope.ts with tests for coercion,
  availability, race conditions, and retry exhaustion
2026-08-22 01:10:19 +03:00
Bohdan Triapitsyn b879cf323f feat(sidebar): add single-project display mode 2026-08-21 16:25:51 +03:00
Bohdan Triapitsyn 9e87d7fdb9 feat(chats): add managed projectless chat sessions
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.
2026-08-21 12:12:40 +03:00
Bohdan Triapitsyn 3613127e2d fix(quota): support z.ai credit limits 2026-08-20 19:09:13 +03:00
Serhii Dziupin 14d7a0ca9b fix: settle busy sessions after managed OpenCode restart (#3002)
* fix: reconcile busy sessions after managed OpenCode restart

Forced health-check restarts previously rebound the event stream without
settling in-flight turns, so sessions stayed busy with no terminal state.
Interrupt those sessions, classify health failures, and retain bounded
process diagnostics for post-restart diagnosis.

Fixes #2943

Co-authored-by: serkraser <serkraser@gmail.com>

* fix: surface interrupted chats after OpenCode restart

Complete unfinished assistant turns as aborted once the session is
authoritatively idle, and show a persistent toast so users can continue
instead of remaining silently stranded.

Fixes #2943

Co-authored-by: serkraser <serkraser@gmail.com>

* fix: redact Basic auth credentials in restart diagnostics

The key/value sanitizer stopped at whitespace, so Authorization: Basic
credentials survived in stderr tails and health snapshots. Redact the
scheme token before that rule runs.

Co-authored-by: serkraser <serkraser@gmail.com>
2026-08-19 11:53:30 +03:00
Yun Feng 36ce73f753 feat(ui): show session cost in VS Code context usage readout (#2991)
Share the desktop session-card currency formatter (lib/money.ts) and surface
the current session's cost in the extension chat-header context usage tooltip.
2026-08-19 00:34:23 +03:00
Bohdan Triapitsyn 423f5b9652 feat(files): upload files with drag and drop 2026-08-18 21:24:53 +03:00
Bohdan Triapitsyn 17d08c0395 feat(quota): add Command Code usage tracking 2026-08-18 16:24:02 +03:00
Bohdan Triapitsyn 34e8a24b20 feat(knowledge): rebuild the project notes panel as Project knowledge (#2973)
The panel stored notes, todos and plans inside one shared JSON file that
six unrelated domains also wrote to, synchronised itself through window
CustomEvents, and could only read plans. It is now Project knowledge:
server-owned storage with explicit routes, a store with rollback, a
section sidebar, plans that open and edit in place, and search across
all of it.

Notes and plans the user pins travel with every message sent in that
project. Pinning is project state, not an attachment to one message, so
it holds until unpinned and the work status panel names what is riding
along and can detach it.

Agent memory is added alongside, in two scopes: what is true about the
user, and what is true about this codebase. The split is not cosmetic —
a wrong project fact costs one project and is noticed, while a wrong
global fact quietly shapes every session everywhere and the user has no
code to check it against. It stays separate from notes so an agent
mistake cannot land in what the user wrote. Sessions receive an index of
titles only; bodies are read on demand, because an index carrying full
text grows until it crowds out the conversation.

Deciding what a session must be told, and whether it has been told, now
lives on the server. The client owned it before, which meant sessions
started without a UI — scheduled tasks, sessions the agent dispatches —
received nothing at all, and a tab's record of what it had sent outlived
the conversation: after compaction the agent no longer held the block
while the tab went on believing it did. What was delivered is recorded
in the session's own metadata, and compaction restores it through the
runtime that already restores pinned messages, in the same turn.

Agent memory ships dark behind OPENCHAMBER_MEMORY_ENABLE: unset, there
is no tool, no routes, no session index, no settings row and no panel
tab. Absent rather than switched off, so nothing invites turning on a
feature that has not been announced. Pinned notes and plans are
unaffected and ship as normal.
2026-08-18 02:59:04 +03:00
Bohdan Triapitsyn 268f9ea9f2 fix(github): keep merged PRs as branch history instead of hiding them
Branch status resolves an open PR across the whole fork network first, so a
merged fork PR can never hide an open upstream PR for the same head. Only when
no target has an open PR does the branch's newest closed/merged PR come back,
as history.

The panel shows that history as a compact note and offers creating the next PR
below it, instead of either sticking on a terminal PR or going blank after a
merge. Terminal associations stay persisted for reload continuity but are never
treated as authority: they revalidate on the discovery cadence and on focus.

History is looked up only for the branch's own remote and name, and remembered
per repo+branch, so the extra lookup cannot exhaust the route's resolve budget.
The checks summary and merge-permission lookup are skipped for a closed or
merged PR, where neither is actionable.
2026-08-15 17:56:55 +03:00
Bohdan Triapitsyn b77a30cd88 feat(quota): read Claude plan limits from the Claude Code login
Claude quota only worked when the user had signed into Anthropic through
OpenCode. Credentials are now discovered from Claude Code itself first: the
macOS Keychain entry, then the Linux/WSL credentials file (honouring
CLAUDE_CONFIG_DIR), then OpenCode auth.json, then CLAUDE_CODE_OAUTH_TOKEN.

All sources stay read-only and the OAuth token is never refreshed: Anthropic
allows one live refresh token per client_id, so refreshing here would sign the
user out of Claude Code. Credentials are re-read per request instead, and an
expired token reports that Claude Code needs a sign-in rather than a bare 401.

Usage is now read from the limits[] array, so model-scoped weekly limits work
again after Anthropic stopped populating seven_day_sonnet/seven_day_opus, and
new limit kinds no longer need a code change. Adds extra-usage spend and the
plan name, and holds the last good values through Anthropic's 429s with a
cooldown and an account-keyed cache.
2026-08-14 20:49:59 +03:00
Bohdan Triapitsyn a5aa32446d feat(browser): replace the preview proxy with a real browser panel and an agent web tool (#2883)
The preview panel worked by proxying a dev server through OpenChamber's own
origin and rewriting the HTML that came back. Anything the rewriter did not
anticipate broke, and pages that refuse to be embedded never loaded at all.
This deletes the proxy (-1604 lines and its tests) and merges the preview and
browser panels into one surface backed by a real Chromium view.

What the panel is now

- A `<webview>` in its own session partition: logins and cookies persist, hot
  reload works because nothing is rewritten, DevTools are one click away.
- Annotation: pick one element, drag a region, or draw freehand, write a note,
  and it reaches chat with a screenshot of the visible page with the marks on it.
- Toolbar: hard reload, page zoom, device sizes, a light/dark switch that
  applies to the page rather than the app, and cookie/cache clearing scoped to
  the panel alone.
- Several pages at once, each tab showing the page's own favicon, and an address
  bar that suggests pages already visited in this project.
- Dev servers are listed from what is actually listening on the machine, checked
  against what a project announced, so a server is offered no matter how it was
  started. One that is still starting is waited for instead of failing.

Remote dev servers

The desktop app binds a local port and pipes raw bytes to the OpenChamber host
over the existing authenticated connection, so the page keeps its own origin at
the root of its own host. The reachable set is exactly what discovery reports
and is re-checked per connection, so an authenticated client cannot dial
arbitrary local services on the host. Links and redirects to another loopback
port stay on the machine that served the page. A tunnel that cannot be opened is
reported; it is never replaced by the plain loopback URL, which would answer
from the user's own machine under a remote address.

Agent control

Browser actions are a separate `openchamber_web` tool: open, snapshot, click,
type, scroll, inspect computed styles, resize between mobile/tablet/desktop, and
capture a screenshot into `.openchamber/screenshots/` in the project. The
existing `openchamber` tool keeps sessions, worktrees and scheduled tasks. Each
has its own setting in the new Settings -> General -> OpenChamber Tools section,
and the plugin is not injected at all when both are off.

Capability belongs to the connected client, not to configuration: a client
declares on its event stream that it can drive a page, which only a Chromium
host does. Exactly one client performs each request — it claims the request
before acting, and the first claim wins — because deciding by whose result
arrives first would be too late for a click that already happened. No client
listening is answered immediately with an explanation rather than a timeout.

Runtime boundaries

Web tabs get a plain iframe that can display a page but not inspect one. The
VS Code extension no longer offers the surface at all, since nothing that makes
the panel worth having works there. Mobile is unaffected.

Native boundary

Camera, microphone, location and device-picker requests from panel pages are
denied — Electron grants them by default when no handler is set, and the panel
loads whatever address the user types. Page capture, appearance emulation and
storage clearing verify that their target belongs to the panel's own session
instead of trusting a web-contents id from the renderer.

Persisted state

Stored `preview` tabs migrate to `browser` (v13 -> v14). Context panel tab
limits are now per surface, so filling one surface no longer evicts another's
tabs. Address history is stored per project and per runtime.

Documentation

`preview.mdx` and `desktop-browser.mdx` rewritten across all locales, the agent
tool settings path corrected, new `DOCUMENTATION.md` for the browser-control
broker and the dev tunnel, and the `ui-api-decoupling` skill updated where it
still described the deleted proxy.
2026-08-13 22:44:13 +03:00
Serhii DziupinandSerhii Dziupin e99f6560be fix(scheduled-tasks): prevent dual-server double dispatch of daily tasks (#2713)
* fix(scheduled-tasks): claim schedule occurrences across server instances

Two OpenChamber servers sharing project config each armed timers and both
dispatched the same daily/weekly/cron/once slot (#2710). Claim the occurrence
in shared config under a cross-process write lock before creating a session.

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

* fix(scheduled-tasks): harden occurrence claim failure and lock ownership

Address PR review blockers: release running-slot bookkeeping when claim
throws, avoid silently dropping an armed occurrence after a due-slack sync,
verify lock-file ownership on release, and cover real on-disk lock behavior.

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

* fix(scheduled-tasks): always release running slot on state-write failures

Wrap runTask bookkeeping in finally so claim, manual-start, and completion
lock timeouts cannot stuck-run a task; drop the diskNext claim guard that
suppressed later occurrences; recover unparseable locks via mtime age.

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

* fix(scheduled-tasks): stop re-arming past nextRunAt and clear stuck running

Only schedule future nextRunAt values so once-task losers and claim-failed
paths cannot spin delay-0 retries. Clear past once nextRunAt on claim, and on
completion-write failure retry terminal status so manual runNow still returns
the session instead of a hard 500 with lastStatus stuck running.

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

* fix(scheduled-tasks): release write chain on lock acquire timeout

withProjectWriteLock left the in-process promise chain pending when
acquireProjectFileLock timed out, wedging every later project write and
stranding runTask before finally. Always release the chain; surface
persistError on run; record once claim failures in task state.

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

---------

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-13 16:14:42 +03:00
Bohdan Triapitsyn dc9c5f8e3e feat(mobile): hidden connection log panel for device-only diagnostics
Connection lifecycle events (probes, transport failures, resume decisions)
are mirrored into an in-memory trail that resets on every launch. A long
press on the connect-screen logo or the instances list opens a panel that
renders the trail with one-tap copy, so release builds can report the exact
probe sequence without a tethered debugger. Details reuse the already-masked
log payloads — no tokens or secrets are captured.
2026-08-13 00:06:51 +03:00
Bohdan Triapitsyn a20b87e984 fix(work-status): label project details clearly 2026-08-11 17:00:23 +03:00
Aaron Hogue 02872ff9d3 fix: keep work-status panel reachable when all sections are hidden (#2805)
* fix: keep work-status panel reachable when all sections are hidden

When every section was unchecked in the Panel Sections dialog, the panel
went transparent and inert, making the settings gear icon unreachable.
The only recovery was knowing to toggle the panel off and on from the
header, which still rendered the same empty/inert state.

Changes:
- Panel stays interactive (not inert) when visible, even with zero
  rendered sections. This matches how other panels in the app use inert
  only for visually-collapsed (width/height = 0) states.
- Empty state shows 'No sections selected' with a link to reopen the
  sections dialog, matching the centered text-muted-foreground pattern
  used by the file tree, review panel, and home page empty states.
- Sections dialog gains a 'Show all' link (visible whenever any section
  is hidden) and a warning when all sections are unchecked, matching the
  keybinds settings 'Reset to defaults' pattern.
- Added i18n keys to all 10 locale files (English fallback).

Fixes #2804

* Round 1: fix interactive guard for fresh-mount; translate i18n keys

Address openchamber-bot review findings:

1. (blocker) Replace English fallback strings in all 10 non-English
   locale files with real translations per locale-ui-patterns guidance.

2. (non-blocker) Restore the renderedSections > 0 guard for the
   transient no-data-on-mount state so the panel doesn't flash a bare
   bordered card. The interactive condition is now:
     visible && (renderedSections > 0 || allSectionsHidden)
   Empty-state rendering is gated on allSectionsHidden alone (not
   renderedSections === 0) so it works correctly on fresh mount when
   all sections were already hidden in persisted settings.

Validation:
  tsc --noEmit: 0 errors
  bun test work-status: 34 pass, 0 fail

* Round 2: use .every() guard, dedup chooseLabel key, add tests

1. Replace >= length check with areAllWorkStatusSectionsHidden() helper
   that uses .every() — stale section ids left in persisted settings
   from a future removal can no longer inflate the count.

2. Remove duplicate chooseLabel i18n key from all 11 locales — the
   empty-state link now reuses the existing sections.open key.

3. Add 6 focused tests for areAllWorkStatusSectionsHidden covering
   empty, null/undefined, partial, full, stale-id, and stale+full.

Validation:
  tsc --noEmit: 0 errors
  bun test work-status: 40 pass (6 new), 0 fail
2026-08-10 23:28:50 +03:00
Bohdan Triapitsyn e13b4526c6 feat(tasks): manage markdown loops from scheduled tasks 2026-08-09 21:43:16 +03:00
Bohdan Triapitsyn f4743ea060 feat(chat): work-status panel, and MCP auth and settings fixes (#2776)
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.
2026-08-09 19:30:25 +03:00
deatheros d8518bf053 fix(desktop): recover from macOS directory permission failures 2026-08-07 01:49:44 +03:00
Serhii Dziupin 99a6dcd052 Merge branch 'main' into feat/gh-2634-pending-question 2026-08-06 13:04:16 +03:00
Serhii Dziupin 5964a7d8ba Merge pull request #2698 from makeittech/feat/gh-2583-markdown-loops
feat(tasks): support markdown scheduled-task loops in .agents/loops
2026-08-06 09:57:49 +03:00
makeittech 59a6c1b70d fix(tasks): guard loop name length and surface loop ownership in the UI
Review follow-up:

- Reject loop files whose frontmatter name exceeds MAX_TASK_NAME_LENGTH
  (80): task names are clamped at storage time, so a raw name longer than
  the limit could never match the stored task identity. The file is treated
  as malformed (definition: null) instead of creating an unreachable
  definition; MAX_TASK_NAME_LENGTH is now exported from project-config.js
  and shared with loops.js.
- Surface loop-sourced tasks in the scheduled-tasks dialog: tasks carrying
  loopFile show a 'Managed by loop file <path>' note, and the enable
  toggle / edit / delete actions are disabled with an explanatory tooltip,
  since the file remains authoritative and would revert any such change.
  run-now stays available. New locale keys added to all 11 message files
  (i18n parity test enforces exact key sets).
- ScheduledTask type gains an optional loopFile field (additive, unknown
  to older clients).
2026-08-06 09:38:18 +03:00
Serhii DziupinandSerhii Dziupin 341b4b45c8 fix(ui): stop painting git activity dots on non-git rail surfaces
The badge PR inverted showActivityDot to !== 'git', so editor/terminal/diff
picked up the blue activity dot whenever git had changes. Git already shows
a numeric badge; other surfaces should stay quiet. Also split the count
aria/tooltip strings into singular/plural keys.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-05 13:26:46 +00:00
Serhii Dziupin c1ba631964 feat(sessions): show a pending-question indicator on session rows
Adds a per-session pending-question badge to sidebar rows, driven by the
live directory-store question state through a dedicated per-session
subscription channel so unrelated streaming never re-renders rows.

Collapsed parent rows roll up pending questions of hidden descendants
from their owning directory stores without bootstrapping them. Question
state is cloned on session delete/archive so badges clear when sessions
disappear. Adds the questionChangeCallbacks sync performance counter,
i18n keys for all locales, and unit tests for the subscription channel
and scope selection.

Fixes #2634
2026-08-05 13:41:48 +03:00
Serhii Dziupin 9b7c032524 feat(ui): show the changed-files count badge on the Git rail surface
Replaces the plain activity dot on the context panel rail's Git button with
a numeric badge of the changed-files count from the git store status, so the
count is visible at a glance without opening the Git surface. Large counts
cap at 99+ to keep the pill within the 36px button. The badge is reflected
in the button's accessible label and the hover tooltip.

Fixes #2364
2026-08-05 13:37:46 +03:00
Bohdan Triapitsyn 094f728777 perf(ui): swap the session activity spinner for a dot and a turn timer
The spinner ran a CSS animation on every active row for the whole turn,
repainting a composited layer at frame rate. Rows now carry a static dot —
primary while running, info while unread — and the metadata slot on the right
shows how long the turn has been going, updating once per second in the dot's
colour. The counter is the motion the spinner used to provide, at 1 fps.
Collapsed groups, folders and projects take the dot only, since one counter
cannot speak for several running turns.

Elapsed time is measured client-side because SessionStatus carries no
timestamps, and starts are persisted so a reload resumes the same count. Two
rules keep that honest. Only a liveness stamp — refreshed while a session is
observed active, stamped as the page hides, and compared against the page's
navigation start so a slow bootstrap is not charged to the absence — and a 90s
adoption window may expire a record; a snapshot that cannot yet see a session
is not evidence its turn ended. And a busy event is never read as a turn
boundary, because OpenCode republishes busy at every step of the agent loop, so
after a reload one of those repeats normally beats the first status snapshot.
Idle and error events do end a turn, and retire the record with it.

Snapshot reconciliation walks the running turns and asks whether the snapshot
covers each one, rather than being handed everything it covers: only a live
start can settle, so the pass scales with timing work instead of with the
directory's session list, and allocates nothing per poll.

Also applied to the mobile sessions sheet and session switcher. The shared
duration ticker moves to hooks/ now that it has a second consumer.
2026-08-05 03:06:35 +03:00
Bohdan Triapitsyn f3dd894209 feat(ui): numbered context-panel surface switching with configurable prefix
- Add switch_context_surface shortcut (default Cmd/Ctrl + 1..9, 0 for the 10th
  surface) that opens/closes/switches context panel rail surfaces by their
  visible order, configurable and persisted in Settings -> Shortcuts.
- Show order-number badges on rail icons while the modifier is held >500ms;
  dismiss on release, blur, or a number press until the next press-and-hold.
- Remove the legacy mod+2/3/4 (diff/terminal/git) and switch_tab_1..9 bindings
  so numbered surface switching goes only through the new mechanism.
- Replace the help-dialog 'Switch Project' row with the surface-switch row and
  update the shortcuts footer/header icons to the command icon.
2026-08-04 20:39:45 +03:00
Bohdan Triapitsyn 8c37061886 fix(walkthrough): name an outdated server instead of failing to parse its HTML
A server without these routes does not answer 404 with JSON. The unmatched
/api path reaches the OpenCode proxy, and OpenCode serves its embedded web UI
for anything it does not recognise — HTML, status 200 — so a client newer than
its server parsed a web page as JSON and put "Unexpected token '<', "<!doctype"
in the panel, naming neither the cause nor the remedy.

The client now checks the content type before parsing. A non-JSON answer on 2xx
or 404 blocks with "this server is older than the app, update it and refresh".
A non-JSON 5xx keeps its own failure: a server that answered badly is not a
server missing the feature, and sending that user to upgrade chases the wrong
thing.
2026-08-04 19:06:19 +03:00
Bohdan Triapitsyn bcae0fcfc3 fix(walkthrough): stop the importance tag reading as a review finding
The "Critical" pill was painted in the status-error colour, so a stop marked
because it drives the change read as a severity reported against the code —
the one thing this feature never does. It is now "Key change", carries its
emphasis with weight and an outline rather than a status colour, and both tags
state their meaning in a tooltip. The panel links the guide from its header,
and the guide gained a section on what the tags mean and what they do not.

Also corrects two German strings that translated the noun "stop" as the verb.
2026-08-04 19:06:19 +03:00
Serhii Dziupin f47110c66f Merge pull request #2616 from makeittech/feat/restore-archived-sessions-2346
feat: add restore/unarchive for archived sessions
2026-08-04 13:41:00 +03: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
Serhii Dziupin 397b9840b7 Merge pull request #2595 from openchamber/feat/ctrl-l-add-selection-to-chat-58c2
feat(ui): Ctrl/Cmd+L adds selected text to chat
2026-08-04 11:33:51 +03:00
Serhii DziupinandClaude Sonnet 5 f02969548a fix(files): hide desktop-only reveal action and label download in browser clients
Reveal-in-file-manager was always offered whenever the server exposed
revealPath, including in a plain browser tab where there is no local
file manager to reveal into. Gate it behind a new isBrowserClientRuntime
check (web platform, no Electron shell) and relabel the save action to
"Download" for that case, since it triggers a browser-style file
download rather than an in-place save.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:11:41 +03:00
Cursor AgentandSerhii Dziupin 3ad3f21024 feat(ui): add Ctrl/Cmd+L to send selected text to chat
Bind mod+l to append the current selection into the chat composer
(Cursor-style), and move session sidebar toggle to mod+alt+l so desktop
menus stay in sync. Closes #208.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
2026-08-03 12:04:35 +00:00
Bohdan Triapitsyn 82c540b9a3 feat(i18n): complete German localization 2026-08-03 02:26:52 +03:00
Bohdan Triapitsyn 1d17cb87b3 feat(walkthrough): write walkthroughs in the reader's language
A guided explanation is only useful in a language the reader reads, so the
panel header gets a language picker alongside the model one, defaulting to
the interface language. Like the model, it is request state rather than a
setting: the language travels with the read and the generation, and the one
a walkthrough was written in is stored with it, so reopening a review
describes what is there instead of what a fresh one would be.

Only prose is translated. Hunk aliases resolve back to hunk ids and
icon/importance are validated against fixed English values, so a translated
one would be dropped by the normalizer — silently losing an anchor or a
style. Identifiers and paths stay as they appear in the code.

The language is part of the cache key, and a read now asks the cache for the
exact request it was given before falling back to the pointer. Without that
the panel answered a request to switch languages with the text it already
had, leaving the other language unused in the cache.

Alongside it:

- The answer budget is derived from the resolved model instead of a flat 24k.
  That number was the same for a 64k-context model and for one that admits to
  384k output tokens, and on the latter it was the only reason generation
  failed: the model spent the whole allowance reasoning and returned nothing.
  It is now min(96k, max(24k, a quarter of the context)) capped by the
  catalog's output limit, decided once so the input reserve and the request
  cannot drift apart.
- A read no longer offers Cancel. It is a few hundred milliseconds of git with
  nothing to cancel, and the button flickered on every model or language
  change. When the panel is showing a fallback, a banner names what is on
  screen versus what was asked for — only once the read has settled.
- The header keeps one 32px control height and drops its labels below 680px
  instead of squeezing them to two letters and an ellipsis.

Docs and module documentation updated in every locale.
2026-08-03 01:27:27 +03:00
Bohdan Triapitsyn 89399ec1c2 fix(ui): simplify walkthrough action label 2026-08-02 21:17:39 +03:00
Bohdan Triapitsyn 9d48d5d02b fix(git): clean up in-progress merge/rebase banner
Use the real status-warning tokens (--status-warning-bg did not exist, so
the card rendered without a fill), drop the decorative icons, and fold the
conflict count into the title as a single full-message key per locale.
The operation description now wraps instead of truncating, and both the
conflict and ready-to-continue states share the same action layout.
2026-08-02 20:27:05 +03:00
Bohdan Triapitsyn 96c011a8ef feat(mobile): tablet layout pass and foldable-ready size class (#2569)
The tablet ran the phone layout with a half-finished iPad draft on top: two
custom sidebars, a leftover overflow menu, split Files/Changes header buttons,
and phone-width sheets stretched across a 13" screen. This brings it onto the
phone's navigation model and keeps only the differences a large screen earns.

- Sessions are a persistent resizable left sidebar; the overflow menu is gone
  and its destinations moved into that sidebar's footer (connected instance,
  settings, pending web update) and into the workspace drawer.
- The workspace (Changes / Files / Terminal / Notes / MCP) is the phone's
  drawer everywhere: a resizable right sidebar where the screen can host one
  (up to 900px) and the full-cover drawer otherwise, with its mounted panes —
  an open diff, an edited file, an attached terminal — surviving rotation.
- Header dropdowns are anchored popovers: the recents switcher mirrors the
  usage overlay on the left, and its trigger is sized to the title rather than
  to the free width.
- App-level pages (settings, instances, update, an opened plan) render as
  centered dialogs instead of covering the screen.
- Overlays center on the chat column through published insets, so the model
  and directory pickers no longer sit off-centre; the directory picker also
  stops overriding the shared width clamp.
- Wide chat layout applies to mobile surfaces, where a tablet chat column is
  finally wide enough for the setting to mean anything.

The layout gate is a live size class rather than a device check, so Android
tablets and foldables are covered by the same code:

- `enabled` when the shortest viewport side is at le
  sw600dp). The short side is what makes this a size question instead of a
  device question — a phone reports ~360-430 whichev
  unfolded book foldable ~600+, and folding shut drops back under it. iPads
  also answer on identity, since iPadOS hands out od
- `roomyForPanels` when landscape and at least 1000px wide, which is what it
  takes to host the sidebar, the panel and a readabl
  foldables miss it in BOTH orientations — their long side is barely wider
  than a tablet's short one — so they keep the portr

Every consumer re-decides instead of remembering wha
open sidebar closes if the device folds shut under it. iPad behaviour is
unchanged: its landscape widths all clear the panel
ones do not, exactly as the previous orientation check did.

Hardware keyboards are read natively. iOS reports them through GCKeyboard,
published to the web layer at document start and kep
disconnect and foregrounding; the layer stops inferring once that answers. A
single early publish was not enough — the connect no
already-attached keyboard fires before the page exists, and GameController can
populate late — so the state is re-published across
resume. With a keyboard attached the draft screen keeps its starter chips and
the composer never collapses; tablets skip the colla
Runtimes with no native answer fall back to inferring it from the keyboard
bridge, and only ever conclude "hardware" from silen

Also: sidebar rows no longer sit on a differently ti
footer is no longer clipped by an over-tall content box, the resize handles
moved above the panes' own overlays so they can actu
now-unreachable overflow menu, fullscreen terminal/MCP/notes surfaces and their
locale key are deleted.

Device behaviour is unverified — the tablet layout,
keyboard bridge and the foldable size class have not been exercised on
hardware, and the 600/1000 thresholds are derived fr
rather than measured on a foldable.
2026-08-02 16:25:16 +03:00