Commit Graph
1326 Commits
Author SHA1 Message Date
Bohdan Triapitsyn 6970cb45c0 fix: align markdown link favicons
Fixes favicon vertical alignment in inline markdown links
2026-05-19 17:20:00 +03:00
Isaac Sanchez-HawkinsandIsaac Sanchez 9ed44326f8 fix(ui): update viewport height without visualViewport (#1314)
* fix(ui): update viewport height without visualViewport

* fix(ui): initialize visual viewport state from helper

---------

Co-authored-by: Isaac Sanchez <isanchez-hawkins@arize.com>
2026-05-19 16:58:18 +03:00
Roberto Bertó 46bef9b0c7 Fix/dismissible infinite toasts (#1319)
* fix: make PWA install and OpenCode update toasts dismissible

Both 'Install OpenChamber' and 'OpenCode update available' toasts use
duration: Infinity with no close affordance, so they persist on screen
until the user accepts (install/update) or reloads the tab. For users
who do not want to install the PWA or upgrade right now, this is
intrusive and there is no opt-out.

Add a Dismiss button (sonner cancel action) to both toasts. When the
user dismisses:

- PWA: persist a flag in localStorage so the prompt does not reappear
  on future sessions. Accepting Install still works as before.
- OpenCode update: persist the dismissed version in localStorage. The
  toast will appear again only when a newer version becomes available.

Adds new i18n keys pwa.installPrompt.dismiss and
opencodeUpdate.toast.actions.dismiss across all seven locales (en,
es, ko, pl, pt-BR, uk, zh-CN).

* test: extract toast dedup helpers and cover with 28 unit tests

Lift the dismissal-decision logic out of usePwaInstallPrompt and
OpenCodeUpdateToast into a React-free sibling module so it can be
unit-tested directly. The React surfaces remain sole owners of side
effects (storage writes, toast.info, event listeners); the new module
only answers 'should we show?'.

New module openCodeUpdateDedup.ts exposes four helpers:
- shouldShowPwaInstallToast(input) - three gates: dismissed,
  sessionShown, hasActiveToast.
- shouldShowOpenCodeUpdateToast(input) - empty version, seen set,
  dismissed===version gates; a different dismissed version lets the
  toast resurface for the new release.
- resolveOpenCodeUpdateVersion(detail) - parses CustomEvent payloads
  defensively (null/non-object/non-string -> '').
- resolveOpenCodeUpgradeStatusVersion(status) - parses upgrade status
  payloads (status falsy / available!==true / latestVersion non-string
  -> '').

Consumers now call the helpers and only run the side-effect when the
decision is true. Behaviour is unchanged.

Coverage: 28 tests via bun:test, 33 expects, all pass first try.
2026-05-19 16:57:23 +03:00
Bohdan Triapitsyn bb87111c49 fix: refresh git changes after chat revert
Updates Git and Diff views after reverting a chat turn
Uses the existing git refresh hint path
2026-05-19 16:49:11 +03:00
Bohdan Triapitsyn 04f4b71396 fix: dedupe vscode notifications across panels
Prevents duplicate native notifications when a session is open in both sidebar and editor
Uses an extension-host claim step so only one VS Code webview displays each notification
Keeps notification behavior unchanged for single-panel sessions
2026-05-19 16:41:16 +03:00
Bohdan Triapitsyn c81441ecfc feat: improve VS Code editor chat actions
Targets active editor chat before falling back to sidebar
Adds session open-in-editor action in the VS Code sidebar
Uses attachment badges for VS Code file and selection context
2026-05-19 15:53:54 +03:00
Bohdan Triapitsyn 25037ccab9 fix: open subtasks as sessions on mobile and VS Code
Restores session navigation for subtask links on mobile
Keeps desktop subtask links opening in the context panel
2026-05-19 14:58:09 +03:00
Bohdan Triapitsyn 2245f148d6 fix: limit changelog link hover styling
Underlines only the hovered changelog link
Removes misleading hover state from changelog sections
2026-05-19 14:51:15 +03:00
Bohdan Triapitsyn 3ac697adc7 fix: remove duplicate Zhipu usage provider
Keeps one Zhipu AI Coding Plan entry in usage settings
Preserves existing Zhipu aliases and config lookup
Removes duplicate backend quota registration
2026-05-19 14:49:03 +03:00
Bohdan Triapitsyn 0f015a495b fix: constrain oversized loading spinners (#1326)
Prevents loading icons from rendering too large
Adds explicit sizing to file and diff loading states
2026-05-19 14:33:42 +03:00
Bohdan Triapitsyn fd81c23414 fix: prevent duplicate desktop notifications
Desktop shells no longer subscribe to the browser notification stream
Electron keeps using the native desktop notification path only
Web browser notifications continue to use the notification stream
2026-05-19 14:32:07 +03:00
Bohdan Triapitsyn 237558130e fix: render multi-file tool diffs safely
Prevents chat crashes from multi-file apply_patch diffs
Splits aggregate patches before rendering each file diff
2026-05-19 14:24:05 +03:00
Bohdan Triapitsyn e0a194eed0 fix: improve favicon contrast in chat links
Adds a compact themed badge behind link favicons
Prevents favicon badge borders from clipping
Keeps external link icons readable across themes
2026-05-19 14:01:41 +03:00
Tom Rochette 25b328dcb8 Fix notification button text color in dark mode (#1324) 2026-05-19 13:26:04 +03:00
Bohdan Triapitsyn 87d13cf61b fix: enable web runtime notifications without push (#1322)
Web notifications now listen to the notification stream directly
Native notifications no longer depend on PWA push being enabled
Window focus is respected when deciding whether to send background push notifications
2026-05-19 13:16:47 +03:00
Bohdan Triapitsyn f40110698f fix: preserve markdown when saving notes
Change the chat text-selection Add to notes action to use the existing markdown selection payload, matching Add to chat formatting behavior while still falling back to plain text when markdown extraction is unavailable.

Stop distilling or summarizing selected text before saving it to project notes. The action now appends the selected content directly so code, lists, emphasis, and other markdown survive.

Increase the project notes text limit from 1000 to 3000 characters to account for saving raw selected text instead of a short generated summary.

Update user-facing copy across locales from distilled insight language to selected text language.
2026-05-19 02:07:07 +03:00
Bohdan Triapitsyn 174fa4e96d chore: retire zen-backed summarization
Disable the active Zen summarization flow because the unauthenticated/free Zen provider is no longer available and now returns usage-limit errors for this feature.

Keep /api/text/summarize as an API-compatible stub that returns local sanitized or distilled fallback text with summarized=false, rather than attempting external model calls.

Remove notification and voice playback summary behavior from runtime paths. Notification {last_message} now always uses normalized truncated text, and TTS playback ignores historical summarize request fields.

Hide the notification summary settings and voice summarize-before-playback controls while preserving legacy persisted settings for compatibility. Also disable Zen model startup validation and make Zen model list routes return empty results.

Update module documentation and tests to describe the retired provider behavior and the remaining compatibility stubs.
2026-05-19 02:06:52 +03:00
Bohdan Triapitsyn a57b02a308 fix: restore vscode native notification behavior
Move VS Code/Cursor desktop notifications onto the webview Notification API instead of the extension-host watcher path, which could not reliably produce native OS notifications.

Route OpenCode runtime events from the shared sync pipeline into the VS Code webview so completion, error, question, and permission notifications use the same live event stream as the UI.

Respect the OpenChamber notification settings in VS Code, including template rendering, completion cooldowns, permission auto-accept suppression, and the notify-while-focused mode.

Use VS Code's window focus signal from the extension host instead of document.hasFocus() inside the webview, so hidden-only notifications are suppressed while Cursor or VS Code is focused across platforms.
2026-05-19 02:06:32 +03:00
Bohdan Triapitsyn d928185640 fix: make opencode health checks resilient
Avoids restarting OpenCode after transient health probe failures
Coalesces concurrent health checks and briefly caches probe results
Adds configurable health timeout, retry threshold, interval, and cache settings
2026-05-18 21:17:02 +03:00
Bohdan Triapitsyn eb3c78e224 feat: show favicons for external chat links
Adds DuckDuckGo favicon lookup for external URLs
Shows favicons in markdown and fetch tool link rows
Hides missing icons automatically
2026-05-18 19:25:10 +03:00
Bohdan Triapitsyn 81c834d194 fix: normalize line endings in file diffs (#1306)
Prevents CRLF-only changes from flooding diff view
Keeps text diffs focused on real content changes
2026-05-18 19:21:07 +03:00
Bohdan Triapitsyn 56a2319934 fix: improve slash and mention autocomplete (#1309)
Shows loaded skills in slash autocomplete with clear type badges
Stabilizes keyboard navigation across autocomplete menus
Fixes skill link rendering and skill autocomplete scrolling
2026-05-18 19:11:29 +03:00
Bohdan Triapitsyn aede187244 fix: render markdown skill links in chat messages (#1311) 2026-05-18 18:24:16 +03:00
vhqtvnandvhqtvn 4f25c3009d fix(status): pick latest assistant message in single pass (#1297)
* fix(status): pick latest assistant message in single pass

Sync reconciliation can splice messages out of array order, so the
previous "filter + sort + last" pipeline could pick the wrong record
as the latest assistant message and surface a stale status.

Replace it with a single-pass scan that compares created-time (tiebreak
by id) — same selection, but resilient to non-sorted message arrays.

* refactor: use comparator + running max for latest assistant pick

Address Greptile review: collapse the dual-exit-point single-pass max
finder (separate continue + isNewer branches, repeated null checks)
into a single isLater(a, b) comparator with one running-max comparison
per iteration. Same selection rule, easier to verify.

---------

Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com>
2026-05-18 18:17:18 +03:00
vhqtvnandvhqtvn cbe6335ed9 fix(sessions): align archive-cascade count with executed list (#1298)
* fix(sessions): align archive-cascade count with executed list

When archiving a parent session, the dialog showed the total descendant
count but the actual archive request skipped already-archived
descendants — so the toast/result didn't match what the user was told.

Collect descendants once, then filter per-action: hard-delete cascades
to every descendant; archive skips already-archived ones. Dialog count
and the ID list passed to archiveSessions/deleteSessions now agree.

* fix(sessions): snapshot descendant IDs at dialog open

Address Greptile review: handleDeleteSession populated the dialog
count from a fresh collectDescendants() call, then confirmDeleteSession
re-collected at confirm time — so a background sync that mutated
childrenMap while the dialog was open could re-introduce the same
count-vs-executed mismatch this PR fixed.

Store the computed descendant IDs in deleteSessionConfirm and pass
them through to executeDeleteSession, so the executed list is always
the snapshot the user was shown. The no-dialog direct-execute path
also routes through the snapshot for consistency.

---------

Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com>
2026-05-18 18:13:43 +03:00
vhqtvnandvhqtvn 86f039d3fd fix(.nvmrc): use 'lts/*' so nvm can resolve it (#1300)
The repo's .nvmrc contained the literal string `lts`, which isn't a
valid nvm version specifier — running `nvm use` (or any tool that
respects .nvmrc) fails with "Version 'lts' not found". When that
happens, the shell falls back to whatever Node is on PATH, which
often doesn't meet vite's >= 20.19 / 22.12 requirement and silently
breaks the build.

`lts/*` is the canonical "latest LTS" specifier in nvm and resolves
correctly. Same intent, working syntax.

Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com>
2026-05-18 18:10:00 +03:00
vhqtvnandvhqtvn 871d04e607 perf(sidebar): virtualize archived bucket above 50 rows (#1301)
The archived bucket is the only sidebar group that routinely grows
into the hundreds or thousands of rows (projects accumulate archived
sessions over time). On main, expanding the bucket mounts every row
at once — for a project with ~1400 archived sessions that's
thousands of subscriptions, layouts, and event handlers committed
on a single render.

Wrap the archived bucket's row map in `@tanstack/react-virtual`
(already in deps) when the visible count crosses 50. DOM stays
bounded to the visible window + overscan regardless of total size.

Implementation notes:
- Threshold + estimate are local constants; only the archived
  bucket is gated, every other group keeps its eager render.
- The virtualizer needs a scroll element. The sidebar's scroll
  lives on an ancestor `ScrollableOverlay`; rather than threading
  a ref through every intermediate component for this one use
  case, we walk up the DOM from the row container.
- `measureElement` handles variable row heights (subagent
  expansion, status indicators, etc.).
- ResizeObserver re-measures on sidebar resize, mirroring the
  pattern in ChangesSection.

Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com>
2026-05-18 18:07:12 +03:00
Junlang HuangandBohdan Triapitsyn 48af84222a fix: prevent mobile keyboard from occluding terminal viewport (#1303)
* fix: prevent mobile keyboard from occluding terminal viewport

On mobile, 100dvh does not shrink when the soft keyboard opens, causing
the terminal and other absolute-inset-0 views to extend behind the keyboard.

Add useVisualViewport hook that tracks window.visualViewport.resize/scroll
via RAF. On mobile, use the visual viewport height (actual visible area)
for the root container instead of 100dvh, so content resizes above the
keyboard when it opens.

* fix: skip unchanged visual viewport updates

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-18 18:05:13 +03:00
Roberto Bertó 76eef473aa feat: close sortable tabs with middle-click (#1304)
Aligns tab behavior with browsers and editors: pressing the middle mouse
button on any closable tab in SortableTabsStrip closes it without going
through the visible close icon.

- onAuxClick handler triggers onClose when event.button === 1.
- onMouseDown preventDefault on middle button suppresses the browser's
  autoscroll affordance.
- Both handlers no-op when the tab is not closable, keeping non-editor
  tab strips (skills catalog, settings sections, etc.) untouched.
2026-05-18 18:02:27 +03:00
vhqtvnandvhqtvn 9ab90d4f1a fix(sidebar): key parent-expansion per render context (#1302)
The same session can appear in two render contexts at once — most
visibly under "Recent" and under its project's root group. Expansion
state was keyed by bare session id, so toggling the chevron in one
context flipped it in the other; the user clicked one row and a
duplicate elsewhere expanded/collapsed in sync.

Switch the expansion set to composite keys of the form
`<renderContext>:<active|archived>:<sessionId>`. SessionNodeItem
already computes this exact shape for its menu instance key
(menuInstanceKey); reuse it as expansionKey so the two render-time
instances of one session keep independent expand state. Updated:

- isExpanded reads `expandedParents.has(expansionKey)`.
- Chevron click / Enter / Space pass `expansionKey` to toggleParent
  instead of session.id.
- The memo-equality check compares the same composite key on both
  sides so it correctly re-renders when only one context's state
  changes.
- toggleParent's parameter is renamed `expansionKey` to reflect the
  new shape; the implementation is unchanged.
- The auto-expand-parent-on-subagent-navigation effect doesn't know
  which context the user will look at the parent in, so it fans the
  parentID out to all four (project|recent) × (active|archived)
  combinations.

Storage is bumped to `oc.sessions.expandedParents.v2`. Existing v1
data (bare session ids) is one-shot migrated by fanning each id
across all four contexts and rewritten under the v2 key; the v1 key
is then removed so a downgrade-then-reupgrade doesn't re-migrate.

Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com>
2026-05-18 17:57:05 +03:00
JovinesandJovines 7582db6df6 fix(mobile): sync project switch with draft target state (#1307)
When a new session draft is open on mobile and the user switches
projects via the ProjectBar, the draft target was not updated,
causing a mismatch with the desktop project/branch selectors.

handleProjectSwitch now mirrors the desktop handleDraftProjectChange:
when a draft is open, it uses setActiveProjectIdOnly +
setNewSessionDraftTarget instead of setActiveProject. The draft is
also guarded against preserveDirectoryOverride to prevent
overwriting a worktree-locked directory.

When no draft is open, the original setActiveProject behavior is
preserved.

Co-authored-by: Jovines <jovines@qq.com>
2026-05-18 17:54:54 +03:00
Roberto Bertó fbffce4fdf feat: add copy-as-markdown and copy-as-json buttons to question card (#1305)
* feat: add copy-as-markdown and copy-as-json buttons to question card

Adds two small icon buttons in the QuestionCard header so users can copy
the full question payload (text, options, descriptions) to the clipboard
in either Markdown or JSON form. Useful for forwarding interactive
questions to an external LLM or note.

- Markdown serialization preserves question headers, multi-select hints
  and option descriptions.
- JSON serialization mirrors the on-wire QuestionRequest shape for
  programmatic reuse.
- Toast feedback on success/failure using the shared @/components/ui
  wrapper and the existing copyTextToClipboard helper (clipboard API
  with execCommand fallback).
- New i18n keys added across all 7 locales.

* test: extract question serializers and cover with 15 unit tests

The two serializers backing the copy-as-markdown and copy-as-json
buttons on QuestionCard were defined as top-level consts inside
QuestionCard.tsx. Exposing them for unit tests would trip the
react-refresh/only-export-components ESLint rule because that file
also exports the QuestionCard component.

Move serializeQuestionAsMarkdown and serializeQuestionAsJson into a
React-free sibling module, questionSerializers.ts, and re-import them
from QuestionCard.tsx. Behavior is byte-identical.

Add 15 unit tests via bun:test covering header fallback, multi-select
hint gating, blank-description elision, ordering across multiple
questions, JSON canonical shape (no transient id/sessionID), boolean
normalisation, and the empty-questions edge case.
2026-05-18 17:54:19 +03:00
vhqtvnandvhqtvn ff35f40b43 fix: resilient reconnect — preserve state on fetch fail, pause when offline (#1308)
* fix: preserve state when reconnect-time fetches fail

Several client API methods swallowed fetch/SDK errors and returned an
empty value (`[]`, `{}`), which was indistinguishable from a successful
"server says nothing here" response. Reconnect resync paths trusted that
empty result as authoritative and deleted local state — so after a
network blip (sleep/wake, wifi reconnect, tunnel switch), the UI could
show:

- sessions stuck on the "running" indicator (status never cleared)
- pending permission prompts disappearing from the UI
- pending question prompts disappearing from the UI

and only a page reload would recover. A related case: `listAgents`
silently returning `[]` defeated the 3-attempt retry loop in
`useAgentsStore` because the loop never saw an error.

The systematic fix:

- `getSessionStatusForDirectory` now returns `null` on fetch failure
  (vs the previous `{}`); the reconnect resync treats only a non-null
  response as authoritative — candidates missing from the response are
  written as `{type: "idle"}`, candidates after a failure are left
  untouched.
- `listPendingPermissions`, `listPendingQuestions`, and `listAgents`
  now throw on SDK/network failure. The pre-existing outer try/catch
  blocks in `resyncBlockingRequestsForDirectory` and the retry loop in
  `useAgentsStore` were already in the right shape — they just never
  fired because no exception was thrown. A small `formatSdkError`
  helper renders the SDK `{data, error}` shape into the thrown message.
- `permissionStore.setSessionAutoAccept` catches the new throw and
  falls back to whatever sync-store snapshots provide; the next SSE
  event or reconnect resync will catch up anything missed.

AGENTS.md gets a new "Distinguish fetch failure from empty success"
subsection documenting the principle (throw vs `T | null` patterns,
when to pick which, the retry-loop trap) so this doesn't regress.

Adds 3 regression tests covering the resync paths: existing
questions/permissions are preserved when the corresponding `list*`
method throws, and a permission-fetch failure does not block the
question block from running (verifies per-block try/catch isolation).

* fix: pause reconnect loop when offline or hidden

The SSE/WebSocket reconnect loop retried indefinitely with no awareness
of whether the browser was online or whether the tab was even visible.
Three issues compounded:

- No `online`/`offline` event handling. With a foreground tab on a dead
  network, we'd hit the server every ~5s forever, and on network
  recovery we'd wait up to ~5s for the next probe instead of reacting
  to the `online` event.
- No visibility awareness. A backgrounded PWA on a flaky link kept
  probing at the same rate as a foreground tab. The browser does
  throttle hidden-tab timers, but the intent wasn't expressed in code.
- The "exponential backoff" math
  `min(5000, max(retryDelayMs, 250) * (failures <= 1 ? 1 : 2))`
  re-initialized `retryDelayMs` to 250 every iteration, so the cap of
  5s was never reached — we waited 500ms forever after the second
  failure. Not actually exponential.

Now:

- `online` event aborts the current attempt (if disconnected) and
  cuts inter-attempt waits short. `offline` event aborts so the loop
  enters the slow-probe path immediately.
- `computeRetryDelay` returns the long cap (60s) when `navigator.onLine`
  is false or the tab is hidden; the short cap (5s) when foreground +
  online. The `online` event is the expected recovery path; the 60s cap
  is a fallback for browsers that miss the event.
- Real exponential growth: `BASE * 2^min(failures-1, 8)`, clamped.
- New `waitForRetry` helper interrupts on `online`,
  visibility-becomes-visible, and abort signal — so visibility/network
  recovery doesn't wait out the rest of the current sleep.

AGENTS.md gets a "Reconnect-loop pacing" subsection alongside the
fetch-failure rule, since they're the same family of resilience
concerns.

One regression test: simulates offline + failed first attempt + `online`
event after the failure; verifies the next attempt fires within seconds
instead of waiting the full 60s offline cap.

* fix: long-cap backoff for permanent 4xx server errors

Before this commit the reconnect loop didn't distinguish HTTP error
types. A stuck-path client (wrong URL after server upgrade) or an
expired-auth client (stale token) would hit the server at the normal
5-second cap forever — ~12 reqs/min, indefinitely, with no path to
recovery besides the user reloading.

Now the catch block extracts an HTTP status (looking on `error.status`
and `error.response.status` — the SDK exposes both depending on the
code path) and overrides the backoff:

- 4xx other than 408/429 → use the long cap (60s) immediately.
  Blind retries won't fix wrong path / bad auth / forbidden, so don't
  pound the server. waitForRetry's `online` / visibility-visible
  interrupters still apply — when an operator fixes the server-side
  config and the client comes back to foreground, recovery is prompt.
- 408 (Request Timeout) and 429 (Too Many Requests) → normal
  exponential path. Those are retryable in spirit.
- 5xx / network / unknown → normal exponential path. Unchanged.

AGENTS.md gets a new bullet under "Reconnect-loop pacing" covering
this — the rule fits naturally alongside the existing `navigator.onLine`
and visibility signals.

Two regression tests:
- A 404-throwing SDK doesn't fire a second attempt within 250ms (proves
  we left the exponential path). After `online` interrupts the wait,
  subsequent attempts fire promptly — proves the override doesn't break
  recovery once the underlying problem is fixed.
- A 429-throwing SDK recovers within 2s — proves 429 still hits the
  fast exponential path and isn't caught by the permanent-error branch.

---------

Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com>
2026-05-18 17:47:20 +03:00
Bohdan Triapitsyn d5cdf464fa release v1.11.2 2026-05-18 01:01:31 +03:00
Bohdan Triapitsyn 233c3bc1ad fix: align chat expansion animations
Use the same spring height animation for thinking and tool details
Reduce mismatch between auto-collapse and manual collapse behavior
Keep existing expanded state behavior unchanged
2026-05-18 00:58:26 +03:00
Bohdan Triapitsyn 9cf0fe0551 feat: animate expandable tool details
Add smooth expand and collapse transitions to tool details
Match the existing thinking block animation style
Keep existing default expanded states unchanged
2026-05-17 23:55:22 +03:00
Bohdan Triapitsyn 5c1333a139 fix: restore typing in branch dropdown search fields
Prevent menu typeahead from intercepting search input keystrokes
Fix branch search in worktree and Git sidebar dropdowns
Cover related directory tree and integrate branch inputs
2026-05-17 23:48:05 +03:00
Bohdan Triapitsyn 622927c34c fix: suppress non-git remote lookup noise 2026-05-17 23:36:17 +03:00
Bohdan Triapitsyn 2290cf4b59 Reduce React Doctor diagnostics in FilesView (#1271)
* Reduce React Doctor diagnostics in FilesView

* fix: address Greptile review feedback — use Base UI initialFocus and restore useMemo for displayedContent

- Replace useEffect-based dialog focus with Base UI Popup's initialFocus
  prop to avoid race with built-in focus management
- Drop unused no-op onOpenAutoFocus/onCloseAutoFocus from dialog wrapper
- Restore useMemo for displayedContent (200K char slice) to avoid
  per-render string allocation for large files
- Update DirectoryExplorerDialog to use initialFocus={false}
2026-05-17 23:31:28 +03:00
Erman HAVUÇandBohdan Triapitsyn 6369cf76a7 feat(ui): context panel enhancements — resizable panels, drag-and-drop todo ordering, and persistent sizes (#1269)
* fix: remove max-h-80 cap on quick notes textarea so resized height is respected

* feat: add drag & drop reordering to project todo items

* feat: make todo panel resizable with density-aware sizing

* feat: open plan import file picker at project root

* feat: persist quick notes and todo panel sizes across sessions

* refactor(ui): scale content height with padding in projectnotestodopanel

* Update packages/ui/src/components/session/ProjectNotesTodoPanel.tsx

Signed-off-by: Erman HAVUÇ <ermanhavuc@gmail.com>

* fix(ui): harden context panel resizing and import

---------

Signed-off-by: Erman HAVUÇ <ermanhavuc@gmail.com>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-17 23:26:26 +03:00
Erman HAVUÇandBohdan Triapitsyn 631905764e feat(git): inline file diffs in commit history rows (#1291)
* chore: add .worktrees/ to gitignore for worktree workflow

* feat(git): add getCommitFileDiff service function

* docs(git): document getCommitFileDiff in module docs

* feat(git): add GET /api/git/commit-file-diff route

* feat(git): add CommitFileDiffResponse type and GitAPI method signature

* feat(git): add getCommitFileDiff HTTP client function

* feat(git): add getCommitFileDiff API facade

* feat(git): add getCommitFileDiff stub to VS Code bridge

* feat(git): add getCommitFileDiff to VS Code gitService and bridge handler

* feat(git): add inline file diff to history commit rows

* fix(git): consolidate CommitFileDiffResponse import to gitApi facade

* fix(git): pass directory through history, validate hash, propagate git errors

* fix(git): use exit code check for VS Code getCommitFileDiff error detection

* fix(git): VS Code rename detection, hash validation parity, retry on error

* fix(git): register scroll container as virtualizer root to fix empty space in history diffs

* fix(git): address greptile review — rename key extraction, directory guard, language detection, isBinary cleanup

* fix(git): harden history inline diffs

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-17 20:08:17 +03:00
Erman HAVUÇandBohdan Triapitsyn fa8fac2590 fix(git): use local-first base ref resolution in getLog, port to VS Code (#1284)
* chore: add .worktrees/ to gitignore for worktree workflow

* fix: resolve remote-tracking base ref in getLog for PR description generation

getLog was calling git log <base>..<head> with a bare branch name that
often doesn't exist locally (e.g. main when only origin/main is present),
causing a fatal 'unknown revision' error and HTTP 500.

Apply the same origin/<base> resolution already used in getRangeDiff and
getRangeFiles: check refs/remotes/origin/<base> first and prefer that ref
if it exists.

Also fix getGitLog in gitApiHttp.ts to read the JSON error body on
failure instead of falling back to response.statusText, so the actual
git error message surfaces in the toast instead of 'Internal Server Error'.

* fix(git): use local-first ref resolution in getLog and port to VS Code

- Replace unconditional origin/<from> preference in getLog() with a
  local-first fallback: prefer the local ref, only use origin/<from>
  when the local ref cannot be resolved, and pass through unchanged
  when neither resolves so git surfaces a meaningful error.
- Extract the logic into an exported resolveBaseRefForLog(from, checkRef)
  helper so it is unit-testable without a real git repo.
- Add service.test.js with 6 cases covering local-wins, origin-fallback,
  neither-exists passthrough, and falsy/empty inputs.
- Port the same local-first resolution to packages/vscode/src/gitService.ts
  getGitLog() to close the cross-runtime parity gap; also handles
  from-only ranges as from..HEAD, matching the web service contract.

* fix(vscode): add missing to-only range branch in getGitLog

When only 'to' is supplied (no 'from'), the web service appends it as a
positional git-log argument. The VS Code port was missing this branch and
silently returned unbounded history instead. Adds the else-if to restore
full cross-runtime parity.

* fix(vscode): surface git log errors

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-17 19:28:08 +03:00
Bohdan Triapitsyn 77b4f27053 chore: fix bun dev script 2026-05-17 18:30:13 +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
Bohdan Triapitsyn 0779656d80 chore: add script for generating changelog preview 2026-05-17 14:51:25 +03:00
Bohdan Triapitsyn bbc297202e fix: use opencode skills as source of truth 2026-05-17 14:51:25 +03:00
Bohdan Triapitsyn 8900b8f0f2 fix(ui): smooth reasoning collapse animation 2026-05-17 14:51:25 +03:00
youfch fec4846ce5 fix(ui): prevent revert dock from occluding pending changes popover (#1286)
* fix(ui): use absolute positioning for pending changes popover to prevent z-index occlusion by revert dock

* refactor(ui): reuse changedFilesPopover constants instead of inlining styles
2026-05-17 14:47:56 +03:00
Erman HAVUÇandBohdan Triapitsyn e1977bbe63 feat(ui): collapsible thinking blocks with merged per-turn view and user toggle (#1273)
* feat: add collapsible reasoning traces with animated labels

* feat(ui): redesign reasoning blocks with merged collapsible Thought view

- Replace per-part reasoning blocks with a single merged block per turn
  (VSCode Copilot pattern), controlled by new `groupReasoningBlocks` store flag
- `ReasoningTimelineBlock` redesigned: chevron toggle, summary preview on
  collapsed header, 'Thinking'/'Justification' label when expanded, BusyDots
  while streaming, auto-scroll to bottom during live streaming
- Short texts (< 120 chars) render inline without a toggle
- Summary now strips markdown and truncates at a word boundary with ellipsis
- New `MergedReasoningPart` component merges all reasoning parts for a message
  into one block at the position of the first reasoning part
- `defaultExpanded` prop lets callers override initial expand state
- Remove `.thinking-dot` CSS animation (replaced by BusyDots component)
- Fix reasoning markdown font-size: use `--text-markdown` instead of `--text-meta`

* refactor(ui): scope working phrases inside useAssistantStatus and simplify reasoning status

- Move WORKING_PHRASES array and getRandomWorkingPhrase() inside the hook
  so they are no longer exported (were only consumed by ReasoningPart which
  no longer needs them)
- Change the 'reasoning' activity status text from a random working phrase
  to the deterministic string 'thinking' — matches the new UI label

* test(ui): expand ReasoningPart tests for new collapsible and summary behavior

- Update baseline test to use text long enough to trigger the collapsible
  path (short texts now render inline) and assert on the correct aria markup
- Add test for 'Justification' label when pre-expanded via defaultExpanded
- Add test for 'Thinking' label for the thinking variant when expanded
- Add test verifying summary is a word-boundary-truncated excerpt ending with
  an ellipsis character

* i18n: rename 'Reasoning Traces' to 'Thinking Blocks' and add thought key

- Rename settings label from 'Show Reasoning Traces' → 'Show Thinking Blocks'
  across all supported locales (en, es, ko, pl, pt-BR, uk, zh-CN)
- Add `chat.reasoningTrace.thought` key to all locales (used by merged
  reasoning block header in completed state)

* feat(ui): add collapsibleThinkingBlocks setting with full persistence wiring

- New boolean store field `collapsibleThinkingBlocks` (default true) with
  `setCollapsibleThinkingBlocks` action; persisted to localStorage
- Threaded through DesktopSettings, SettingsPayload (API types), desktop
  persistence (sanitize + apply), web appearance persistence, appearance
  auto-save watcher, and server-side settings-helpers sanitize/format
- Server defaults to true when the field is absent in formatSettingsResponse
- MessageBody reads the flag: false → render reasoning as plain AssistantTextPart;
  true → existing collapsible/merged block path

* feat(settings): expose Collapsible Reasoning Blocks toggle in visual settings

Add a checkbox under the 'Show Thinking Blocks' row (visible only when
showReasoningTraces is enabled) that toggles the collapsibleThinkingBlocks
preference. Follows the existing toggle pattern: div role=button, keyboard
handler for Enter/Space, Checkbox primitive, aria-pressed attribute.

* i18n: revert showReasoningTraces label rename and add collapsibleThinkingBlocks strings

- Revert 'Show Reasoning Traces' → 'Show Thinking Blocks' rename (the
  collapsibleThinkingBlocks toggle is now a separate control, so the parent
  label stays as 'Reasoning Traces' for clarity)
- Add `collapsibleThinkingBlocks` / `collapsibleThinkingBlocksAria` strings
  across all seven supported locales (en, es, ko, pl, pt-BR, uk, zh-CN)

* test(server): add settings-helpers coverage for collapsibleThinkingBlocks

- Verify sanitizeSettingsUpdate accepts boolean true/false and rejects
  non-boolean values (string, number)
- Verify formatSettingsResponse forwards the value correctly for both true
  and false, and defaults to true when the field is absent

* fix(ui): respect defaultExpanded prop and remove dead alwaysShowActions from ReasoningTimelineBlock

The useEffect on [isStreaming] was firing on mount and immediately calling
setIsExpanded(false) (since isStreaming is false for completed blocks),
overriding any defaultExpanded={true} passed by callers. The fix uses a
prevIsStreamingRef so the effect only collapses the block on a true→false
transition and is a no-op on initial mount.

Also removes alwaysShowActions from ReasoningTimelineBlockProps — the new
header design always shows the chevron, making the prop obsolete. The prop
was already absent from the component destructuring (a dead type entry) and
was silently ignored at runtime. Removed it from ReasoningPartProps,
MergedReasoningPartProps, and the two call-sites in MessageBody as well.

* chore: remove unused reasoningpresentation module and test

* fix(ui): polish collapsible reasoning block UI

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-05-16 17:20:16 +03:00
Isaac Sanchez-HawkinsandIsaac Sanchez 00c112077d fix: match root project sessions in switcher (#1274)
* fix: match root project sessions in switcher

* test: cover project path matching edge cases

---------

Co-authored-by: Isaac Sanchez <isanchez-hawkins@arize.com>
2026-05-16 16:46:05 +03:00