Skip to content

fix(desktop): restore maximized windows on their saved display - #60482

Merged
Hona merged 4 commits into
devfrom
windows-mouse-hijacks
Oct 30, 2026
Merged

Hona merged 4 commits into
devfrom
windows-mouse-hijacks

Conversation

@Hona

@Hona Hona commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Closes #58247
Related: #56970, #59229, #57732

Visual

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 72" width="240" height="72" fill="none">
  <rect x="12" y="14" width="96" height="52" stroke="currentColor" stroke-opacity="0.35" />
  <rect x="124" y="6" width="104" height="60" stroke="currentColor" stroke-opacity="0.35" />
  <rect x="128" y="10" width="96" height="52" fill="currentColor" fill-opacity="0.14" stroke="currentColor" stroke-opacity="0.75">
    <animate attributeName="x" values="34;34;128;128" keyTimes="0;0.35;0.55;1" dur="3.2s" repeatCount="indefinite" />
    <animate attributeName="y" values="26;26;10;10" keyTimes="0;0.35;0.55;1" dur="3.2s" repeatCount="indefinite" />
    <animate attributeName="width" values="52;52;96;96" keyTimes="0;0.35;0.55;1" dur="3.2s" repeatCount="indefinite" />
    <animate attributeName="height" values="30;30;52;52" keyTimes="0;0.35;0.55;1" dur="3.2s" repeatCount="indefinite" />
    <animate attributeName="stroke-dasharray" values="3 2;3 2;0;0" keyTimes="0;0.35;0.55;1" calcMode="discrete" dur="3.2s" repeatCount="indefinite" />
  </rect>
  <rect x="128" y="10" width="96" height="5" fill="currentColor" fill-opacity="0.3">
    <animate attributeName="fill-opacity" values="0;0;0.3;0.3" keyTimes="0;0.35;0.55;1" dur="3.2s" repeatCount="indefinite" />
  </rect>
</svg>
Before After

Summary

  • Maximized state kept (desktop: maximized state lost when saved bounds exceed the display #58247): resolveWindowState reset the whole saved state when the normal bounds did not fit a display, isMaximized included. On a 1366×768 panel (1388×781 DIP) the 1280×800 default never fits, so the window opened windowed on every launch. Every fallback now keeps isMaximized and isFullScreen.
  • Maximized on the saved display: normal bounds are only recorded while the window is restored. A window maximized on another display kept x/y on the first one, and maximize() used that display. While the saved displayBounds is still connected, the normal bounds now move into it, including displays at negative coordinates.
  • Primary fallback: a window whose display is gone opens at the primary display's origin instead of (0, 0), which is not always on the primary display on Linux.
  • One decode at the boundary: readWindowState decodes the file with Schema.fromJsonString, so resolveWindowState takes a typed state.
  • Opens maximized on Windows: a window saved as maximized is created hidden. Electron's maximize() shows a hidden window with SW_SHOWMAXIMIZED, so it never paints at its normal bounds first. Other platforms keep showing first, because macOS does not reliably enter fullscreen while hidden.
  • Embed corners: embeds refresh on move and display-metrics-changed, because their corner masks are rendered for the display scale.
flowchart LR
  File["window-state-*.json"] --> Decode["readWindowState: Schema.fromJsonString"]
  Decode --> Resolve["resolveWindowState"]
  Resolve -->|"maximized, saved display connected"| Anchor["move normal bounds into displayBounds"]
  Resolve -->|"fits a display"| Keep["saved state"]
  Resolve -->|"display gone"| Primary["primary origin, flags kept"]
  Anchor --> Create["BrowserWindow show: false (win32, maximized)"]
  Keep --> Create
  Primary --> Create
  Create --> Max["maximize(): SW_SHOWMAXIMIZED"]
Loading
// packages/desktop/src/main/windows/window-state.ts
const target =
  saved.isMaximized || saved.isFullScreen ? all.find((bounds) => sameBounds(bounds, savedDisplay)) : undefined

if (target && !within(saved, target)) {
  const width = Math.min(saved.width, target.width)
  const height = Math.min(saved.height, target.height)

  return {
    ...saved,
    x: target.x + Math.floor((target.width - width) / 2),
    y: target.y + Math.floor((target.height - height) / 2),
    width,
    height,
  }
}

Not in this PR


Added by the opencode bot (AI-generated).

Fixes #58247

Related: #56970, #59229, #57732

Copilot AI balanced review requested due to automatic review settings October 30, 2026 05:32
@Hona
Hona requested a review from Brendonovich as a code owner October 30, 2026 05:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't verify this: it needs the Windows desktop app on mixed-DPI multi-monitor hardware. What I ran: the desktop window tests and gui-plugins browser tests pass, and tsc -b passes for packages/desktop. I also ran the saved state from #58247 through resolveWindowState on both checkouts. On the base it loses isMaximized; with this PR it keeps the flag and re-anchors the bounds onto the saved display.

Public API: no SDK, server, config or CLI change. There is one behaviour change for plugin code that uses Electron sessions directly: every session now denies all permissions by default (Electron's default grants them), passing null to setPermission*Handler now means deny-all, and pointer and keyboard lock are always denied.

Concerns, most important first:

  • #57732: the session hardening may not change the behaviour this PR closes it for (inline).
  • macOS: the deferred reveal applies to all platforms, which risks breaking fullscreen restore on macOS (inline).
  • Tests: the wireDisplayRecovery test is large for what it checks (inline).

For #59229 and #57732, could you post before/after evidence from a Windows multi-monitor setup? For example a GetClipCursor log, and a lock/unlock or sleep/wake run with the window maximized on the negative-Y monitor.

Fixes #58247
Related: #45171, #56970

Drop the session permission hardening and embed focus release: the browser pane and the app window already deny pointer lock, so they do not change #57732. Defer the first show only for maximized Windows windows, and cut the wiring test to one trigger plus listener cleanup.
@Hona Hona changed the title fix(desktop): prevent Windows multi-monitor mouse hijacks fix(desktop): restore maximized windows across display changes Oct 30, 2026
@Hona

Hona commented Oct 30, 2026

Copy link
Copy Markdown
Member Author

Pushed b15489a, which addresses the three inline comments. The PR body is updated.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new commit addresses all three earlier points. The session permission hardening is gone and #57732 is no longer closed. The deferred reveal now applies only to a Windows window saved as maximized, so macOS restores fullscreen as before. The wireDisplayRecovery test is now one short test.

I still couldn't verify #59229 itself, because it needs the Windows desktop app on mixed-DPI monitors. The desktop window tests pass and tsc -b passes. Public API is unchanged. I have no new concerns.

The invisible window after a mixed-DPI wake (#59229) comes from Chromium's HWNDMessageHandler and is being fixed there. Keep only OpenCode's own fixes: restore maximized windows on their saved display, open them maximized on Windows, and decode the saved window state with Schema.
@Hona Hona changed the title fix(desktop): restore maximized windows across display changes fix(desktop): restore maximized windows on their saved display Oct 30, 2026
@Hona

Hona commented Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

Pushed 2e6ed66. This PR now carries only OpenCode's own fixes.

  • Removed display.ts: the post-wake bounds reset and forced repaint worked around an upstream rendering problem instead of fixing it. Closes #59229 is now Related; the cause is being reproduced against plain Electron and Chrome, to be fixed upstream.
  • Saved state: readWindowState now decodes the file with Schema.fromJsonString, and the tests cover the desktop: maximized state lost when saved bounds exceed the display #58247 display size (1388×781 DIP). The new cases fail on dev.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this PR, the #58247 saved state still comes back maximized on its saved display. On the base it opens windowed at the default size. I checked this by running resolveWindowState with the issue's saved state on both checkouts. The desktop window tests pass. Public API is unchanged.

One blocker: tsc -b in packages/desktop now fails in window-state.ts and passes on the base (inline).

  • packages/desktop/src/main/windows/window-state.ts:152: This breaks the type check. bun run typecheck:tsc in packages/desktop now fails with TS18048: 'state.width' is possibly 'undefined' at line 156, and the same for state.height at line 158. It passes on the base. hasBounds now takes Partial<WindowState>, and Number.isInteger doesn't narrow its argument, so state.width > 0 no longer type-checks. A typeof state.width === "number" check (or an isInteger type guard like the one the previous commit had) would fix it.

@opencodejr opencodejr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type-check failure in window-state.ts is fixed: tsc -b in packages/desktop now passes. I re-ran the #58247 saved state through resolveWindowState: with this PR it is still restored maximized on its saved display, and on the base it opens windowed at the default size. The desktop window tests pass. Public API is unchanged. I have no new concerns.

@Hona
Hona merged commit 7b67adf into dev Oct 30, 2026
13 checks passed
@Hona
Hona deleted the windows-mouse-hijacks branch October 30, 2026 06:11
@amrkmn amrkmn mentioned this pull request Oct 30, 2026
1 task
divyanshusingh2903 added a commit to divyanshusingh2903/forge that referenced this pull request Nov 4, 2026
* fix(core): have agents ask before generating an image nobody asked for (anomalyco#60256)

* release: v2.2.1 (anomalyco#60113)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>

* feat(core): turn experimental_image_generate on by default (anomalyco#60258)

* feat(app): open the worktree picker from the new session branch (anomalyco#60263)

* feat(app): redesign the todo pill and share the composer pill styling (anomalyco#59979)

* fix: type the Go and ChatGPT usage limits (anomalyco#57897)

Co-authored-by: Aiden Cline <aidenpcline@gmail.com>

* chore(core): use @opencode/pty 0.3.3 (anomalyco#60276)

* fix: show the Go usage limit's reset time, Go page link and a composer tray (anomalyco#60284)

Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>

* fix(tui): preserve captured attachments and source paths (anomalyco#60222)

* perf(app): keep each tab's timeline when switching directories (anomalyco#59168)

* fix(tui): prevent truncateMiddle overflow at small widths (anomalyco#60287)

* fix(core): reject persistent terminals with a missing cwd (anomalyco#60300)

* feat(core): gate scheduled prompts with shell checks (anomalyco#60137)

* fix(session-ui): name a pending steer's model only when it changes (anomalyco#59310)

Co-authored-by: vimtor <36263538+vimtor@users.noreply.github.com>

* feat: link live system status from site footers (anomalyco#60223)

* feat(go): add Go Teams plan and limits to /go (anomalyco#60308)

* fix(tui): match the empty dialog search cursor to the highlighted row (anomalyco#60310)

* feat(core): integration.connect and integration.use policies (anomalyco#60315)

* fix(core): send execute progress at most every 100 ms (anomalyco#60120)

* fix(core): keep permission reasons and feedback in MCP tool failures (anomalyco#60102)

* release: v2.2.2 (anomalyco#60337)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>

* fix(app): keep question options and submit controls reachable on mobile (anomalyco#60335)

* fix(tui): resolve default cursor style across config reloads (anomalyco#60302)

* fix(console): polish Go plan cards and audience switch (anomalyco#60345)

* fix(tui): keep # in file search base when not followed by a line range (anomalyco#60336)

* feat(core): add Code Mode checks with static tool scanning to schedules (anomalyco#60140)

* refactor(core): finish moving shell permission checks off tree-sitter (anomalyco#60332)

* fix(core): fail schedule creation when a code check throws and pause invalid checks immediately (anomalyco#60349)

* feat(tui): load stored images by reference, like desktop (anomalyco#60116)

* fix(tui): highlight PHP code blocks without opening tag (anomalyco#60359)

* fix(app): keep inbox scroll position when opening a session (anomalyco#60356)

* chore(console): move console and stats to released solid-start (anomalyco#60077)

* feat(app): add question notifications and sounds (anomalyco#60358)

Co-authored-by: Sayuri Nekomiya <contactme@awsl.rip>

* fix(core): remove a session's terminals when it is deleted (anomalyco#60322)

* fix(app): preserve command selection under a stationary pointer (anomalyco#60368)

* fix(app): preserve simultaneous request notifications (anomalyco#60378)

* feat(app): allow selecting question answer text (anomalyco#60372)

* chore(release): announce releases in Discord again (anomalyco#60371)

* feat(process-daemon): rename terminals (anomalyco#60324)

* chore(process-daemon): release 0.4.0 (anomalyco#60325)

* feat(app): group added projects in settings (anomalyco#60221)

* refactor(client): share the persistent pty protocol (anomalyco#60323)

* fix(core): check redirects, PowerShell method calls, and dynamic directory changes (anomalyco#60376)

* test(app): mock persistent pty routes (anomalyco#60327)

* fix(console): use default OG image on /go (anomalyco#60397)

Co-authored-by: adamdotdevin <adamdotdevin@users.noreply.github.com>

* fix(core): run dynamic provider npm fallback after external sdk hooks (anomalyco#60380)

* fix(ai): accept typed tools in loop (anomalyco#60390)

* feat(tui): mark the selected composer suggestion under new designs (anomalyco#60393)

Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>

* feat(ai): type OpenRouter options on the Responses route (anomalyco#60044)

* fix(ai): don't send filename with file_url on Responses (anomalyco#60382)

* fix(app): tidy the Extensions submenus (anomalyco#59468)

* chore(release): require notes for stable releases and remove old changelog tools (anomalyco#60429)

* fix(core): show Code Mode programs the same failure text as direct calls (anomalyco#60109)

* fix(app): ⌘W closes the side panel when nothing in it is left to close (anomalyco#59002)

* fix(app): keep the mobile drawer open while a session loads (anomalyco#59427)

* fix(codemode): preserve null in text encoding (anomalyco#57639)

* fix(app): keep the @ menu's highlight and Enter on current results (anomalyco#59434)

* refactor(ai): one tool() for both clients (anomalyco#60407)

* fix(ai): keep polling queued generations through unknown statuses (anomalyco#59988)

* fix(codemode): copy extension results without trusting their own methods (anomalyco#58031)

* fix(desktop): prevent main bundle require collision (anomalyco#60430)

* fix(app): stabilize side-panel click targets across reactive updates (anomalyco#60436)

* fix(core): preserve plugin-supplied model variants (anomalyco#60097)

* fix(app): give focus back when ⌘W closes the side panel's last New tab (anomalyco#60435)

* fix(app): feather the sidebar list's scroll edges (anomalyco#59467)

* feat(app): page the timeline from the composer (anomalyco#59426)

* fix(cli): allow blob frames and objects in web UI CSP (anomalyco#60453)

* fix(ai): keep tools with a throwing execute runnable (anomalyco#60431)

* fix(app): derive model reasoning capability from variants and compatibility (anomalyco#60457)

* fix(desktop): sign zip data descriptors in debug log export (anomalyco#60456)

* fix(app): match own server loopback port before treating http servers as local (anomalyco#60460)

* feat(desktop): spellcheck toggle and system-language dictionaries (anomalyco#59129)

Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>

* fix(app): insert consecutive dropped file mentions in composer (anomalyco#60463)

* fix(app): render inline content for built-in skills (anomalyco#60464)

* fix(desktop): retain renderer permissions across shared-session windows (anomalyco#60458)

* fix(client): evict missing sessions on 404 instead of retrying or toasting (anomalyco#60459)

* fix(app): keep project avatars on busy session rows (anomalyco#60384)

Co-authored-by: iamdavidhill <1879069+iamdavidhill@users.noreply.github.com>

* fix(app): keep sidebar search muted on hover and show its shortcut (anomalyco#60046)

Co-authored-by: iamdavidhill <1879069+iamdavidhill@users.noreply.github.com>

* fix(desktop): ship the Icon Composer app icon on macOS (anomalyco#60020)

Co-authored-by: iamdavidhill <iamdavidhill@users.noreply.github.com>

* fix(app): tidy the pairing dialog's layout (anomalyco#59659)

Co-authored-by: iamdavidhill <1879069+iamdavidhill@users.noreply.github.com>

* fix(ai): match the Promise client to the Effect API (anomalyco#60465)

* chore(tui): use OpenTUI 0.8.0 (anomalyco#60385)

* feat(app): fit the session header at narrow timeline widths (anomalyco#59512)

Co-authored-by: iamdavidhill <1879069+iamdavidhill@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>

* fix(app): match the Context tab's close button to other side panel tabs (anomalyco#59511)

Co-authored-by: iamdavidhill <1879069+iamdavidhill@users.noreply.github.com>

* fix(app): remove context tab indicator tooltip (anomalyco#59505)

Co-authored-by: iamdavidhill <1879069+iamdavidhill@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>

* fix(app): show the review icon on the files changed tab (anomalyco#58358)

Co-authored-by: iamdavidhill <1879069+iamdavidhill@users.noreply.github.com>
Co-authored-by: Luke Parker <10430890+Hona@users.noreply.github.com>

* fix(app): size the agent menu to its options (anomalyco#60026)

Co-authored-by: iamdavidhill <1879069+iamdavidhill@users.noreply.github.com>

* fix(app,gui-plugins): port renderer UUID generation to HTTP-safe uuid() (anomalyco#60462)

* fix(app): fade home project list edges on scroll (anomalyco#52358)

* feat(app): copy version from about settings (anomalyco#52356)

* fix(app): tighten tab preview spacing and wrap paths between folders (anomalyco#60467)

* fix(app): never hide every server from Home (anomalyco#60454)

Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>

* feat(ai): keep partial output on errors (anomalyco#60477)

* fix(cli): report usage for steps that end after run goes idle (anomalyco#60475)

* fix(core): keep inner tool progress from replacing execute's call rows (anomalyco#60480)

* fix(app): align queue row action buttons (anomalyco#60485)

* fix(app): stop the ResizeObserver loop error while timeline rows grow (anomalyco#60481)

* fix(session-ui): compare timeline rows without Effect structural hashing (anomalyco#60489)

* refactor(ai): move speech, transcription, video and realtime to experimental (anomalyco#60484)

* fix(app): stop scrolling toward an end that page zoom cannot reach (anomalyco#60488)

* fix(app): make the session details popover match menus (anomalyco#60494)

* fix(desktop): restore maximized windows on their saved display (anomalyco#60482)

* feat(app): split new session project and worktree triggers (anomalyco#60486)

* fix(core): apply webfetch permissions to Code Mode fetch (anomalyco#59954)

* fix(ui): remove select dropdown animation (anomalyco#60499)

* fix(desktop): describe macOS folder access in the app bundle (anomalyco#60503)

* fix(app): keep models with unknown release dates visible (anomalyco#60496)

* fix(app): keep settings nav scroll inside the list (anomalyco#60504)

* feat(core): add Claude Agent provider (anomalyco#58782)

* fix(session-ui): compute highlight styles once per subtree in Chromium (anomalyco#60513)

* docs(go): raise DeepSeek V4.1 Flash limits to $100/$200/$105 (anomalyco#60515)

Co-authored-by: MrMushrooooom <19261047+MrMushrooooom@users.noreply.github.com>

* docs(go): revert DeepSeek V4.1 Flash limits to $80/$160/$85 (anomalyco#60532)

Co-authored-by: MrMushrooooom <19261047+MrMushrooooom@users.noreply.github.com>

* fix(ui): watch the toast portal instead of the whole page (anomalyco#60537)

* fix(session-ui): relabel code copy buttons only when the language changes (anomalyco#60538)

* fix(app): stop :has() rules restyling every ancestor on DOM insertion (anomalyco#60521)

* fix(session-ui): send and render only the markdown blocks a delta changed (anomalyco#60562)

* fix(session-ui): allocate less per streamed markdown delta (anomalyco#60549)

* release: v2.2.3 (anomalyco#60564)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(tui): span vertical session tab pulse across full tab width (anomalyco#60338)

* test(tui): drop PHP highlighting test (anomalyco#60408)

* fix(app): simplify the session location popover (anomalyco#60492)

* fix(session-ui): stop re-checking the timeline projection on every streamed delta (anomalyco#60577)

* fix(session-ui): copy user messages from the actions menu (anomalyco#60576)

* fix(core): drop Claude Code's /login hint from login errors (anomalyco#60592)

* feat(app): guide Claude Agent setup before connecting (anomalyco#60603)

* feat(app): add base branch picker shortcut (anomalyco#60579)

* feat(core): add experimental artifacts with pluggable providers (anomalyco#60351)

* feat(core): rename persistent terminals (anomalyco#60329)

* fix(tui): add right padding to edit and patch diffs (anomalyco#60614)

Co-authored-by: Tarquinen <78942835+Tarquinen@users.noreply.github.com>

* fix(app): list commands above sessions in command palette search (anomalyco#60616)

Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>

* fix(tui): keep plugin-set prompt cursor styles (anomalyco#60599)

Co-authored-by: Tarquinen <Tarquinen@users.noreply.github.com>

* chore(core): use @opencode/pty 0.4.0 (anomalyco#60387)

* feat(core,server): enable schedules by default and run loop in opencode serve (anomalyco#60478)

* docs(go): raise DeepSeek V4.1 Flash limits to $100/$200/$105 (anomalyco#60625)

Co-authored-by: MrMushrooooom <19261047+MrMushrooooom@users.noreply.github.com>

* refactor(ai): one ai object for Promise and Effect (anomalyco#60516)

* chore(release): leave AI package changes out of the changelog (anomalyco#60631)

* Update CHANGELOG.md to remove breaking changes

Removed breaking changes section regarding `@opencode/ai` and updated the changelog with new features and fixes.

* Update CHANGELOG with new features and breaking changes

Added breaking changes section and updated features.

* refactor(core): store background job markers in a SQLite job table (anomalyco#60491)

* feat(app): move the desktop terminal to persistent PTYs (anomalyco#60346)

* fix(cli): keep loopback requests off environment proxies (anomalyco#60441)

Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
Co-authored-by: pj.reid <pj.reid@zetier.com>

* fix(core): allow local config to override Console-supplied providers

Reverts the lock from anomalyco#60315 that ignored user, project and remote overrides on providers the Console supplies. It also blocked harmless overrides such as model limits, names and variants (anomalyco#60597) and internal gateways.

* fix(app): keep terminal tab clicks and renames on the right tab (anomalyco#60645)

* chore: update nix node_modules hashes (anomalyco#60187)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(core): keep schedules off unless experimental.schedules is enabled

Restores the opt-in default from before anomalyco#60478. The schedule loop still runs in every server process and keeps its leader election; it only fires schedules in locations that enable the flag.

* feat(ui): add flat input surfaces and compact sizing (anomalyco#60598)

* fix(app): add top inset to recently closed menu search (anomalyco#60652)

* fix(core): cap compaction's prompt size at the context window (anomalyco#60660)

* release: v2.2.4 (anomalyco#60664)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(ai): stop demoting unsigned and interrupted reasoning to text (anomalyco#60498)

* feat(core): accept a percentage for compaction.buffer (anomalyco#60647)

* fix(app,tui): hide MCP integrations from the connect menu (anomalyco#60675)

* fix(core): keep MCP quiet while a location shuts down (anomalyco#60668)

* feat(app): send quotes directly from the comment popup (anomalyco#60640)

* fix(core): run calls to a bash tool as shell (anomalyco#60687)

* fix(stats): scroll dense charts to latest data in Safari (anomalyco#57459)

* fix(core): show the closest lines when edit misses oldString (anomalyco#60689)

* fix(ci): fail desktop publish when an R2 upload fails (anomalyco#60696)

* fix(core): say which folder is missing when read misses (anomalyco#60688)

* fix(ai): recognize LiteLLM context overflow errors (anomalyco#60692)

* fix(core): frame MCP stdio messages without the SDK read buffer (anomalyco#60694)

* feat(app): add worktree picker shortcuts and toggle shell mode (anomalyco#60713)

* chore: refresh docs and provider ordering (anomalyco#60731)

* release: v2.2.5

* refactor(tui): drop the full-screen TUI and keep only what cli imports

Forge's interface is the desktop app, and the opencode binary only hosts its
server. packages/tui keeps the modules cli still imports (config, mini, form,
plugin discovery, editor, model preference, spinner, logo) as an internal
library and loses its root entry. The default command now prints a pointer to
the desktop app, `opencode mini` and `opencode run`.

Removes the TUI dev harness, update preflight, the dev:vite scripts, and the
tests for deleted files.

* chore(desktop): rebrand to Forge and port the release setup

Names, app IDs (io.github.divyanshusingh2903.forge), the forge:// scheme,
window and app titles, desktop icons, the macOS Icon Composer art, favicons,
the terracotta accent, and the FORGE wordmark (home, splash, About, connect).
Publishing points at the Forge GitHub releases. Adds forge.json, the
forge-release workflow (builds the server for each target, then the app),
the version and update-manifest scripts, NOTICE and a README that lists what
is not ported yet.

Removes the OpenCode-shaped lodger, pin board and brick breaker on the home
screen, and the legacy OpenCode Linux launcher.

* feat(desktop): draw Linux window controls on the tab bar

Linux hides the native title bar and uses Electron's title bar overlay, as
Windows does, so minimize, maximize and close sit on the same bar as the tabs.
The tab bar, top inset, header drag areas and splash follow the Windows overlay
sizing, and the overlay refreshes with zoom and theme changes.

* feat(app): add persistent projects sidebar with pinned and collapsible sessions

A resizable, collapsible Projects sidebar in the shell for the horizontal-tabs
layout on every route. Each project lists its sessions (remembered open or
closed, eight at a time), sessions can be pinned into a Pinned section that
shows each session's project icon, and only the session on screen is
highlighted. Home drops its own Projects column while the sidebar is showing.
Adds the pin icons, English strings, and a per-project session index on the
Home sessions controller.

* feat(app): give the projects sidebar its own open setting

The top-tabs Projects sidebar had shared the side-tabs sidebar's open state,
so hiding one hid the other after a layout switch. It now saves projectRailOpen
separately, and the sidebar toggle acts on whichever sidebar the layout has.

* chore(app): use the Forge name in app strings

Replaces OpenCode with Forge where the text names the app: settings
descriptions, the About page and its website link, project and workspace
hints, error and pairing messages, and the voice assistant label, in every
locale. OpenCode's own services (Go, Console, Free), the opencode command and
file names, and the trademark attribution keep their names.

* feat(app): float the hidden Projects sidebar on hover

Resting on the window's start edge or the show button peeks the hidden rail over the content, reusing the vertical-tabs sidebar peek. Its header button docks it, and leaving it hides it again.

* feat(core): add permission modes and Plan mode

Sessions carry a Forge permission mode (Manual, Accept edits, Auto) in their
metadata, applied by a permission hook that only ever turns asks into allows.

Plan mode replaces upstream's Plan agent with Forge's v1 workflow: the plan
agent may edit only plan files under .opencode/plans, follows the v1 plan
prompt and format, and presents the plan with present_plan. Accepting switches
to Build in the chosen mode; revising carries the user's feedback. Build can
suggest Plan with plan_enter, and a prompt that asks for a plan is nudged to it.

* feat(app): add mode picker, plan dock and plan review tab

The composer's agent picker becomes one mode list (Manual, Accept edits, Auto,
Plan) that Mod+. cycles, and the auto-approve setting picks a new session's
starting mode.

A Plan tab in the side region shows the plan file live with a section outline,
opens when a plan is presented, and lets the user quote parts of it. The plan
dock answers plan forms with numbered shortcuts; quotes and their comments join
a revision. Plan files stay out of Review changes, and the session header shows
while a plan is being written or reviewed.

* feat(app): add built-in MCPs with their own header button

Ship GitHub, GitLab, Linear, Notion, Sentry and Jira as built-in remote
MCPs with brand icons, a dedicated MCP button next to the context icon in
the session header, and an MCPs settings tab. GitHub signs in with an
access token sent as a Bearer header or a pre-registered OAuth app, since
it has no dynamic client registration. MCP on/off choices are saved and
apply in every project, open session and after restarts.

* feat(app): show hover cards for issue and PR links

Add a link preview endpoint on v2 core and server that resolves GitHub,
GitLab, Linear and Jira issues, PRs, merge requests, releases and projects
through the gh and glab CLIs or the connected MCPs, and show a hover card
with state, author and activity for those links in session messages.

* feat(app): sign in to the GitHub MCP with the GitHub CLI

Add a GitHub CLI sign-in method to the GitHub MCP that reuses the token
from `gh auth token`, or runs `gh auth login --web` first and shows its
one-time code in the sign-in dialog. The new tab is the dialog's default;
the access token and OAuth app options remain.

* feat(app): add launcher buttons to the session header

Show Review, Browser and Terminal as icon buttons in the session header
through a new `header` flag on side region launchers, rename the plan
button to Review Plan, and show context usage against the model's
window, e.g. 20K / 200K.

* feat(app): add session options menu to sidebar sessions

Give sidebar sessions a three-dots menu with Rename, Export and Delete,
shared with the session title menu. While either menu is open, R, E and
D choose an item directly, and each item shows its letter. Rename edits
the sidebar title in place.

* fix(core): use the GitHub MCP token for GitHub link previews

Pass the GitHub MCP's saved access token to `gh api` as GH_TOKEN, so
previews work where gh has no login of its own, such as under a separate
XDG config directory. Report a CLI that is not signed in as unavailable
rather than forbidden, so the preview retries after sign-in.

* fix(core): show GitLab issue previews through the GitLab MCP

Recognise GitLab's /-/work_items/N issue links, and resolve GitLab issues
and merge requests through the connected GitLab MCP's get_work_item and
get_merge_request tools, falling back to glab. Map work items' uppercase
states, camelCase dates and label widgets onto the preview card.

* feat(app): show hover cards for Notion links

* feat(app): add git staging, commit and remote actions to the review panel

- Add a core GitActions service and /api/git routes for state, stage, unstage,
  discard, commit, generated commit messages, fetch, push, pull and stashes,
  ported from v1
- Watch the index, refs, config and working tree and send git.changed so the
  panel follows changes made outside Forge
- Show changes as a compact tree with staging checkboxes on files and folders
  in every mode, scoped to the listed files outside Uncommitted
- Port v1's remote and stash dialogs and git translations
- Count unpushed commits on branches without an upstream and offer
  Push and set upstream
- Add the outline split button variant from v1

* feat(app): lead the session header with context usage

* fix(desktop): show Forge name and icon in notifications

* feat(app): expand every timeline action by default

Default the timeline to the Everything preset. A Compact preset saved before this default is read once as unchosen, since v2 saved its defaults with any other setting change; a later Compact choice stays.

* feat(app): show the Forge mark while a session loads

Keep the startup splash's mark over the timeline until history loads and a cold timeline reveals its rows, then fade it out.

* feat(core): add local auto memory

Port Forge's v1 local memory onto v2. Each repository gets a memory folder
under the data directory, keyed by the Git common directory so worktrees share
it. Its guidance and bounded MEMORY.md index load as the forge/memory
Instruction Source, so a saved note reaches the session as an instructions
update. The forge.memory plugin approves the folder for every agent and keeps
subagents read-only. `memory: false` turns it off and keeps existing notes.

* feat(app): break down context usage by part

Add a session.usage endpoint that describes the next model request: the
system prompt, tools, MCP and Code Mode tools, skills, instruction files,
memory and messages, each estimated from its text and scaled to the
provider's latest count, plus the input window and the auto-compact
threshold. The Context tab shows it as a colour-coded bar and legend with
the auto-compact buffer and free space, above OpenCode's existing stats.
The System Prompt section now shows the assembled prompt part by part
instead of the last instruction update.

* fix(app): keep the thinking level picker visible

* feat(app): show question dock shortcuts on its buttons

* docs: update maintaining, agents and contributing guides for v2

* feat(session-ui): show web search results as cards and direct tool responses

* feat(core): let tools describe their Code Mode call rows

* feat(app): show browser actions and screenshots readably in the timeline

* fix(app): retire the app-side auto-approver

The client answered every permission request itself while the auto-approve
setting was on, whatever mode the session was in, and hid the approval cards.
Modes already settle requests on the server, so the setting now only picks the
mode new sessions start in, and Mod+Shift+A switches the current session
between Auto and Manual.

* fix(desktop): update ad-hoc macOS builds from the release page

Squirrel.Mac rejects updates for builds without a Developer ID signature, so
those builds now report a download and open the latest release instead. The
update check sends a Forge user agent, and What's New no longer reads
OpenCode's changelog.

* fix(core): ask before shell commands and file edits by default

Upstream agents allow every action, so Manual mode had nothing to ask about.
The asks follow each agent's base allow-all rule, so agent rules and config
still override them.

* feat(app): ask before browser actions outside Auto mode

Each browser call now checks permission when it runs, with its operation as
the resource. Attaching the pane allows reads and asks for everything that
acts, and drops the allow-all grant earlier versions left behind. Approval
cards and timeline rows name the element, URL or tab a call works on.

* feat(app): number the permission dock's choices

* fix(app): show the Forge mark while a session opens

* fix(desktop): stamp release builds with the full Forge version

The update manifests carry v<opencode>-v<forge>, but the app took only the
OpenCode half, so every install would read the latest release as a different
version. v1 stamped the full version the same way.

* ci: run checks on GitHub-hosted runners for main, including macOS

* ci: run e2e tests on macOS

* fix: repair unit tests that broke in CI

* style: add the blank lines the anti-slop lint asks for

* ci: add macOS e2e summary job and drop the TUI simulation build check

* test(app): align e2e specs with Forge's timeline, project rail and mocks

* test(sdk): run the lazy-provisioning shell test in Auto mode

* test(app): expect the thinking picker to stay visible

* test(core): count the GitActions repository probe in the boot git-spawn budget

* test(core): expect Forge's plan tools in the Location tool list

* test(core): keep Forge's memory guidance out of the recorded runner prompt

* test(core): compare git-actions file contents with normalized line endings

* test(server): create the schedule test session in Auto mode

* test(app): point the branding specs at Forge's title, site link and changelog

* test(app): update composer specs for /agent and the always-visible thinking picker

* test(app): open MCPs from Forge's header button and settings tab

* test(app): drop MCP from the mobile details drawer specs

* test(app): move MCP expectations from the details menu to the header popover

* test(app): fit the session header spec to Forge's menu keys and breakpoints

* test(app): find review changes by Forge's staging rows and mirror the diff in git state

* test(app): expect Forge's context usage text and header width

* test(app): fit settings search specs to the MCPs page and the narrower empty state

* test(app): move the settings MCP specs to the MCPs page and clear the rail's resize handle

* test(app): widen the nested-session header test for Forge's header

* ci: install the pinned ripgrep for the Linux and macOS unit tests

* test(app): drop the focus-into-list steps the Skills list can't take

* test(core): keep Forge's memory guidance out of the session runner prompts

* chore: clear the anti-slop lint findings in Forge's changed files

* ci: lint v2-port's changes against the upstream release it was ported from

* chore(protocol): regenerate the OpenAPI document

* fix(core): keep the plan file path slash-separated on Windows

* ci: install the pinned ripgrep on Windows too

* test(app): retry Escape until the MCP popover closes in the RTL spec

* test(core): expect the plan file's native absolute path in the reminder

* test(app): give the large review list time to render on slow runners

* test(app): give the all-locales dictionary check time on slow Windows runners

* test(server): give the artifacts API test time on slow Windows runners

* ci: give macOS a current bash and fewer e2e workers

* test(cli): wait longer for a managed service on slow runners

* test(cli): give the upgrade help test room on slow runners

* test(app): press and expect the platform modifier in e2e specs

* test(app): use the literal Control binding where the app has no mod shortcut

* test(app): press the literal Control bindings for tab cycling and new terminal

* test(server): give the VCS 503 test room to boot the location on slow runners

* test(app): use macOS shortcuts for document edges, file browser and zoom in e2e

* test(app): scroll regions with Home and End on macOS in touch-scroll e2e

* test: give SDK bundling room on macOS and skip the missing Ctrl+Home check there

* test(server): give slow location and OAuth fallback tests room on macOS

---------

Co-authored-by: Aiden Cline <63023139+rekram1-node@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
Co-authored-by: Dax <mail@thdxr.com>
Co-authored-by: Aarav Sareen <96787824+arvsrn@users.noreply.github.com>
Co-authored-by: Kit Langton <kit.langton@gmail.com>
Co-authored-by: Aiden Cline <aidenpcline@gmail.com>
Co-authored-by: James Long <longster@gmail.com>
Co-authored-by: OpeOginni <107570612+OpeOginni@users.noreply.github.com>
Co-authored-by: Filip <34747899+neriousy@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <219766164+opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: vimtor <36263538+vimtor@users.noreply.github.com>
Co-authored-by: Adam <2363879+adamdotdevin@users.noreply.github.com>
Co-authored-by: David Hill <1879069+iamdavidhill@users.noreply.github.com>
Co-authored-by: usrnk1 <7547651+usrnk1@users.noreply.github.com>
Co-authored-by: Sayuri Nekomiya <contactme@awsl.rip>
Co-authored-by: adamdotdevin <adamdotdevin@users.noreply.github.com>
Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>
Co-authored-by: xjh <87483213+xiajiahang@users.noreply.github.com>
Co-authored-by: Brendonovich <14191578+Brendonovich@users.noreply.github.com>
Co-authored-by: iamdavidhill <iamdavidhill@users.noreply.github.com>
Co-authored-by: Simon Klee <hello@simonklee.dk>
Co-authored-by: frank <48937772+joe-ieta@users.noreply.github.com>
Co-authored-by: DS <78942835+Tarquinen@users.noreply.github.com>
Co-authored-by: MrMushrooooom <19261047+MrMushrooooom@users.noreply.github.com>
Co-authored-by: Tarquinen <Tarquinen@users.noreply.github.com>
Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
Co-authored-by: pj.reid <pj.reid@zetier.com>
Co-authored-by: Dax Raad <d@ironbay.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

desktop: maximized state lost when saved bounds exceed the display

2 participants