Skip to content

[pull] main from xintaofei:main - #216

Open
pull[bot] wants to merge 80 commits into
MoozLee:mainfrom
xintaofei:main
Open

[pull] main from xintaofei:main#216
pull[bot] wants to merge 80 commits into
MoozLee:mainfrom
xintaofei:main

Conversation

@pull

@pull pull Bot commented Aug 25, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

Adam-Dalloul and others added 9 commits August 22, 2026 09:25
The in-position image card always said Image generation, even for a Read
of a screenshot or a fetched docs page. Codex generation still uses that
label. Everything else uses the tool title, URL slug, or filename.
A task in review reported a change set it had not produced, so the board
offered the wrong acceptance. Two causes, both in what the counters were
measured from.

A task triggered from a pull request is checked out ON that pull
request's head while its recorded base is the merge base, so the whole
contribution sat in the worktree before the agent started. Counters and
the landable-changes probes now measure from the point the worktree was
checked out at — which is also the point an acceptance delivers from:
the pull request's head there, the recorded base otherwise, advanced to
the live merge base when the branch has absorbed base content. The
drawer keeps the merge-base view and says when it holds the pull
request's own changes.

Nothing makes an agent commit before its task reaches review, and
`git diff` enumerates paths from the index, so work left in new,
never-added files read as no work at all. The measure now runs against a
throwaway index — a copy of the worktree's own, plus intent-to-add
entries — so counters, the acceptance probes, the drawer's list and its
patch all see uncommitted work, ignored artifacts stay out, and the real
index is never touched.

Also: a push back carrying nothing is refused in favour of completing; a
boot pass re-measures every task still in review, since counters are
written once at settle; and the engine subscribes to the event bus
before that boot work, so a task started under it cannot lose its
TurnComplete and hang in `running`.
The clone dialog built its preview as `${targetDir}/${repoName}`, so a
Windows target rendered `C:\work/codeg` — a native prefix with a stray
forward slash bolted on. Seven more sites shared that shape or its
mirror image (a `/`-only split that never finds a segment boundary in a
backslash path):

- clone dialog: the preview and the path actually cloned into
- project-boot "project will be created at" hints (shadcn, hyperframes)
- skills settings "skills directory" draft hint
- branch dropdown's prefilled worktree path, which came out as the bare
  relative "/C:\work\repo-main-abc123" because `lastIndexOf("/")` is -1
- tool-call titles, which showed a whole absolute path instead of its
  last two segments
- the working-diff overview tab, titled after the folder path
- automation's per-run worktree, whose sibling path degraded to a
  relative name git would have planted inside the repo

Route the joins through the existing `joinFsPath`, which follows the
base path's own separator, and add `fsSeparator` / `fsBaseName` /
`siblingFsPath` beside it for the basename and sibling cases.

Roots need their own handling: `C:\`, `/`, and `\\server\share` have no
parent to hang a sibling off, so the derived name lands inside the root
instead, keeping the result absolute. A relative one would make git
resolve a worktree inside the repository and then register that
unresolved string as the folder's working directory. `basename` reports
no name at a root rather than feeding a colon into a directory name.

(cherry picked from commit 9810b1a)
Delivering a forge-sourced task — opening its pull request, or pushing back
onto the one it came from — was the only acceptance that could not take its
checkout with it, leaving a worktree the user had to find on the card and
remove by hand. Both shapes of the delivery dialog now offer the same
checkbox the merge and complete dialogs do, seeded from the folder's
`delete_worktree_default`.

The cleanup rides on the delivery rather than gating it: it runs only after
the settle, and a removal that fails flags a retryable `cleanup_state`
rather than turning a pull request that was already pushed into a reported
failure.

Two probes decide whether the checkout may go, because each is blind to
exactly what the other sees: uncommitted files, which reached no forge, and
a branch tip that outran the OID the delivery published — a commit made in
that window leaves `git status` spotless and is just as unpublished.
`has_landable_changes` cannot serve as the second probe here, being true of
every delivery by construction. The tip then rides into the removal itself,
so `update-ref -d <ref> <oid>` compares and deletes in one operation instead
of leaving a window between the check and `branch -D`; every local-merge
caller passes `None` and is unchanged.

(cherry picked from commit 1bb6909)
The helper that drives git in these fixtures neuters the global and
system config, so a run cannot depend on whatever the developer's own
git happens to say. The code under test spawns its own git through
`crate::process`, which inherits the real environment — and Git for
Windows ships `core.autocrlf=true` in its system config. One worktree
then means two different things: the engine checks a file out as CRLF
while a `git add` from the helper stores those bytes verbatim, leaving
the index naming blobs the commit it was checked out from never had.

A diff measured from that index counts files nobody touched. It cost a
pull-request task's own-work stat exactly that — every file in the tree
where only the one the agent added belonged, which is the class of
answer that measure exists to prevent.

Repo-local config is the one layer both sides read, so `init` and
`clone` now pin `core.autocrlf=false` on the repository they create.
It rides in the helper rather than at each fixture: a new fixture has
no reason to suspect this, and the answer only ever comes out wrong on
Windows.
## Why

A user can put a reverse proxy in front of a codeg-server. The proxy can
require extra HTTP headers, for example a Cloudflare Access service token.
Codeg could not send those headers, so the connection failed.

## What

Each remote workspace connection can now carry custom HTTP headers. The
desktop client attaches them to every request it sends to that connection.

The manage dialog gets a "Custom headers" section below the access token.
The section stays collapsed unless the connection already has headers. Each
row holds a name and a value. Values are masked, because a header usually
carries a credential.

## How

- A new migration adds a `headers` column. It stores a JSON array of
  `{"name","value"}` objects in a TEXT column, which follows the convention
  of the other JSON blobs in this schema. Existing rows default to `[]`.
- An array, not a map: HTTP allows a repeated header name, and the array
  keeps the order the user typed.
- `RemoteWorkspaceHeader` lives in `models/`. It converts to an
  `http` pair through `to_header_pair`, and a slice of rows converts to a
  `HeaderMap` through the `ToHeaderMap` trait.
- The service validates each row before it writes: it drops the blank rows
  the editor leaves, rejects a value with no name, rejects a name or value
  that `http` cannot parse, and rejects 10 reserved names. The client sets
  those names itself, so a custom one would break the request. The save
  fails rather than silently drop what the user typed.
- `commands/remote_proxy.rs` attaches the headers at all five request
  sites: the HTTP call, the attachment upload, the workspace file upload,
  the download ticket, and the ticket download. The last one sends no
  bearer token, because the ticket in the URL is the credential — but a
  fronting proxy still inspects it, so it needs the headers too.
- The WebSocket handshake carries the same headers.
- The health check sends the headers as well, so the test and the save
  exercise the same configuration as a later request.

## Tests

Rust, in `remote_workspace_connection_service`:

- `validate_headers_trims_and_drops_empty_rows`
- `validate_headers_keeps_repeated_names_and_order`
- `validate_headers_rejects_reserved_names`
- `validate_headers_rejects_invalid_name_value_and_missing_name`
- `to_header_map_keeps_repeats_and_skips_unparsable_rows`
- `create_list_update_delete_roundtrip`, extended to store and read back
  the headers

Frontend, in the new `remote-workspace-manage-dialog.test.tsx`:

- `keeps the editor collapsed when the connection has no headers`
- `opens the editor when the connection already has headers`
- `masks every header value`
- `sends the added header on save and drops a removed one`

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Aug 25, 2026
@pull pull Bot added ⤵️ pull merge-conflict Resolve conflicts manually labels Aug 25, 2026
dawNotPoi and others added 18 commits August 26, 2026 06:29
Mark every custom header value sensitive, so it stays out of the HTTP/2
HPACK dynamic table and out of any `{:?}` of the request, the same way
reqwest treats its own bearer token.

Reserve `transfer-encoding` next to `content-length`: hyper frames the
body itself and picks chunked for the streaming workspace upload, and it
is the header a request-smuggling attempt rides in on — the one thing not
to hand to a fronting proxy. The reserved list is a slice now, so its
length is no longer a second thing to remember to bump.

Cover the upgrade path the migration exists for: a row written before the
`headers` column has to read back as `[]`, not as a NULL that fails to
deserialize and takes the whole connection list with it.
The pill still clipped into the file path bar when a selection was
anchored on the second visible line: one line of room (20px on macOS,
18px on Windows) is less than the pill's 25px box, so ABOVE was kept and
overflowed the editor's top edge by 5-7px.

Replace the first-visible-line proxy with the test Monaco itself applies
to non-overflowing widgets — `fitsAbove = anchor.top >= height` — fed
with `getTopForPosition() - getScrollTop()` (Monaco's own `anchor.top`)
and the pill's measured height. This also makes the decision exact under
word wrap and fractional scroll offsets, where a model-line comparison
is only an approximation.

Monaco keeps the widget node at `display: none` until after it asks for
a position, so cache the last real measurement and re-lay out once when
the first-ever show finally yields one.
…host

The custom headers are credentials, and two paths let the remote decide who
receives them.

`absolute_remote_ticket_url` returned a server-supplied absolute URL verbatim,
and the download that follows it is the one request carrying the headers with
no bearer token gating them. `codeg-server` only ever answers with a path, so
pin the resolved URL to the connection's origin and refuse the rest.

`reqwest` drops `Authorization` once a redirect crosses hosts but knows nothing
about these headers, so a redirect carried them anywhere the remote pointed.
Refuse that hop instead, on the same host-and-port test reqwest itself applies
— the hop could only have come back 401 anyway. Same-host redirects still
follow, under reqwest's own bound. The health check gets the same policy, so
"test succeeded" means what "save succeeded" means.

Surface the reason: reqwest's Display stops at "error following redirect for
url (…)", which is the layer that says nothing, so flatten the source chain
into the detail the UI renders.
Port the rest of Monaco's `_layoutBoxInViewport` decision, not just its
above-edge half, so every way the pill could land outside the editor is
covered:

- room below the anchor now decides too, from `getLayoutInfo().height`
  (Monaco's own `viewportHeight`) and the anchor line height. An editor
  dragged shorter than one pill plus one line takes the roomier side.
- an anchor line scrolled wholly out of view withholds the pill. Monaco
  renders widgets a little past the viewport, and from out there both
  placements land in the surrounding chrome.
- every layout path now carries the measure-and-correct handshake. A live
  zoom resizes the pill without touching the selection, so the paths that
  only re-laid out were re-using a stale height and put the pill back
  behind the path bar.

`ContentWidgetPositionPreference.EXACT` looks like the answer for a short
editor — it is the one preference that cannot leave the viewport — but for
an overflowing widget Monaco positions it at `anchor.left + contentLeft`
with no `scrollLeft` term and no clamp, which throws the pill outside the
editor once it is scrolled horizontally.

Keep `allowEditorOverflow`: dropping it lets Monaco decide placement from
viewport geometry for free, but the editor's own scrollbar then paints
over the pill near the right edge and the node shrink-to-fits to
min-content.
…ecrets

tungstenite TRACEs the serialized handshake in full — `trace!("Request:
{:?}", …)` over the raw bytes. For a remote workspace connection those bytes
carry the bearer token, which travels as a `Sec-WebSocket-Protocol` value, and
every custom header the user configured, which for the case the feature exists
to serve is a Cloudflare Access service token. Marking the values sensitive
does not reach this: that governs HPACK indexing and the value's own `Debug`,
not bytes already written out. So raising the level to trace to diagnose a
connection, then attaching the log to a bug report, shipped the secrets with it.

Add the standing ceiling the codebase already uses for firehoses of this shape.
`Debug` keeps the crate's useful lines and drops only trace, which is where both
the dump and the per-frame flood live.

Also tighten `same_remote_origin` to compare scheme as well: on a connection
configured as `https://box:8443`, a hop to `http://box:8443` left host and port
identical, so the host-and-port rule borrowed from reqwest would have allowed
the credentials onto the wire in plaintext.
…crate

A crate-level ceiling is meant to be re-openable by naming a submodule — that
is what keeps `tungstenite::protocol` pinnable to trace for frame debugging.
That escape hatch is right for a firehose and wrong for a credential, and the
Settings UI accepts a per-target directive, so the handshake dump could still
be asked for by name.

Give it its own backstop entry. Backstops are appended last and win at equal
specificity, and no target is more specific than the module the `trace!` lives
in, so neither `CODEG_LOG` nor the Settings UI can reopen it. Asking for less
is still honored — the clamp never raises verbosity.

Also build the redirect tests' clients with `no_proxy`. A developer box with
HTTP_PROXY set routes loopback through the proxy, which answers 502; the
cross-host test would then have passed on an error its own policy never raised.
The level table cannot close this on its own. `EnvFilter` ranks a
field-qualified directive above a plain target one, and the dump is a formatted
event, so `CODEG_LOG='tungstenite::handshake::client[{message}]=trace'`
outranks the backstop entry and reopens it — the comment claiming otherwise was
wrong.

A ceiling is the wrong shape here anyway. There is no verbosity to trade off:
the only tungstenite client in this codebase is the remote-workspace WebSocket,
so every time that line prints at all it prints a bearer token and whatever
custom credential headers the connection carries. Drop the target before the
level filter is consulted, as a second global filter an event must also clear.

The backstop entry stays: it keeps the level story coherent and it is what
`tungstenite::protocol=trace` is measured against, which remains reachable.
…headers

feat(remote-workspace): add custom HTTP headers to connections
fix: keep add-to-chat pill visible for first-line selections
The repository panel's drawer showed an issue/PR's description but not its
comments, because the list payload carries no discussion and folding one in
would spend a request per row to draw a page whose reader opens at most one.

Ask for it when the panel opens instead — one request for the item actually
being read. On GitHub that lands on `/issues/{n}/comments` (which serves pull
requests too) and on the CORE quota, so it cannot starve the 30-per-minute
search budget the list itself lives on; on GitLab it is the item's own notes
collection with the system events ("changed the milestone") filtered out, so
the thread matches the `user_notes_count` the row already shows.

The panel renders each comment through the same Markdown renderer as the body,
with the author, a relative timestamp, an "edited" mark only when the forge
really edited it, and a permalink. "Load more" follows the forge's own
pagination header rather than the row count — GitLab drops its system notes
after paginating, so a page can arrive empty with the discussion continuing
behind it — and a failed page keeps what is already on screen, retrying the
page that actually failed rather than the load-more cursor.
The side-by-side view laid both columns on one grid inside a single
scroller, so reading to the end of a long line on one side dragged the
other side's text off screen with it. Each side now scrolls in its own
container, split by a draggable divider that starts even, and the two are
kept in step on both axes so the row under the cursor stays the row under
the cursor. A side whose content is shorter no longer hauls its neighbour
back to its own end, and the rows stay level down the whole file —
including where a deletion has nothing to face on the other side.

The line numbers, and inline the +/- marker, now hold against the left
edge while the code scrolls under them. They ride an opaque rail that
carries its row's colour, so a tinted row reads as one band from the
digits through to the code and nothing shows through from underneath.

The file header carries the whole summary: the +N -N counters sit with
the path they describe, and one button on the right switches layout,
showing the view it takes you to rather than spending two buttons and
twice the width on a single choice.

The diff body pins itself to LTR. Code reads left-to-right whatever the
interface language, and under the RTL document Arabic switches on, the
panes swapped sides, the divider dragged the wrong way, `scrollLeft`
counted down into negatives — stranding the horizontal sync at zero — and
the number rail stuck to the edge the numbers were no longer on. The
direction has to sit on the scroll container itself, which is what
decides both the layout and the sign of `scrollLeft`, so `ScrollArea`
takes an optional `dir`. The header still follows the interface, with the
counters kept LTR so their signs stay in front of their numbers.
Streamdown's diagram component zooms with `transform: scale()` under an
inline `will-change: transform` and a 150ms transform transition. A drag
restarts that transition on every pointermove, so the diagram spends the
whole gesture on a composited layer the GPU stretches from a single 1x
raster — blurred and smeared exactly while it is being moved. None of it
is reachable from a prop, and dropping `will-change` from a stylesheet
only ever covered half of it.

A mermaid fence now renders through `components.pre`. Streamdown builds
its component map as `{...defaults, ...props.components}` and its own
`pre` does nothing but stamp `data-block` on the child, so claiming that
one element takes the diagram and leaves every other fence — shiki, plain
code, indented blocks — on Streamdown's path, with no remark plugin and
no change to the sanitize schema. The engine is still the same lazily
imported singleton, so nothing is loaded that was not loaded before.

Zoom is the width of the host element rather than a transform: the
browser lays the SVG out at its real size and repaints the vectors, so a
diagram is as sharp at 8x as at 1x. Panning is a whole-pixel translate
carrying neither a transition nor a `will-change`, which is what keeps
the drag off the compositor. Mermaid's own `max-width` comes off the
rendered root, since it would otherwise cap every zoom above the fit.

The palette follows the interface. The block reads the resolved dark mode
from context, which reaches it through the memo around the message and
the one inside Streamdown; neither compares anything theme-shaped, so a
theme handed down as a prop would leave every already-drawn diagram
behind on a switch.

Fullscreen is a dialog — a framed panel with its own header, zoom
readout, and toolbar over a dimmed overlay — and it fits the whole
diagram rather than the column width. Inline, a diagram is capped at 60vh
and fades at the cut instead of ending mid-stroke, the rest one drag or
one click away. Saving as SVG, PNG, or .mmd goes through the system save
dialog, and the PNG takes its pixel size from the viewBox: Mermaid writes
`width="100%"` on the root, which decodes to nothing an `<img>` can draw.
Also name LQ router 中转站 in the English README, the way the other
Chinese-branded sponsors already read there.
The Recent section opens at fifteen conversations and reveals another
page per click on its footer row, but the limit only ever grew: once
expanded, the list stayed long for the rest of the session and pushed the
sections under it off the screen.

The footer now carries a reset back to that first page. While pages
remain it sits at the row's right edge as an icon, revealed on hover and
on the same axis as the section headers' own actions, with the room for
it reserved so the label never reflows. Once the last page is out the row
survives for the reset alone and gives it the whole row — retiring the
row there would drop the only way back exactly when the list is longest.
The reset is offered only when more than a page is really on screen, so
it never proposes a collapse that would hide nothing; a limit raised
earlier outlives the conversations it revealed.

The row lights as one unit, the fill following the row rather than the
label button so that crossing onto the icon does not read as leaving.
Activating the reset hands focus to the footer button that survives it,
which keeps a keyboard user on the row instead of back at the top of the
document.
OpenCode moves to 1.18.23, Cline to 3.0.60, CodeBuddy to 2.139.0,
DeepSeek Harness to 0.7.0, and Qoder to 1.1.31.

Nothing on the wire moves with them. deepseek-acp 0.7.0 holds its
handshake and the three upstream layouts codeg mirrors, so its registry
entry records the two behaviour changes that do land: `session/load` and
`session/fork` now compare workspaces by resolved path rather than by
spelling, which is a loosening that stops a resume failing over
`/var` against `/private/var` or a Windows short name; and the
model-facing shell on Windows becomes pwsh, whose tool presentation is
shaped exactly like the bash one the terminal card already reads.

The Qoder config-dir resolver is unchanged at 1.1.31 down to the
`.qoder` directory name, but its minified identifiers were renamed
again, so the parser's re-verification note carries the current pair
alongside the older ones it already warns about.
A custom model cloned a GPT catalog entry whole, and the entries codex
ships now carry `tool_mode: "code_mode_only"`, `multi_agent_version:
"v2"` and `use_responses_lite: true`. That combination sends an empty
`tools` array, moves the system prompt into a non-standard
`additional_tools` developer item, and drops `instructions` entirely —
nothing a third-party gateway implementing only the public API can
serve. Custom models now come in two shapes: a native GPT one, and an
OpenAI-compatible one that clears those three keys along with
`apply_patch_tool_type` and `supports_image_detail_original`. The same
request then goes out as plain Responses — `instructions`, a `function`
tool array, `reasoning: {effort}` — and without the `type: "custom"`
tool. The template is a preset over ordinary overrides rather than a
stored mode, so it reads off the effective values and can be switched
either way on a model that already exists, and every key it touches
stays editable on its own under a new wire-protocol group. The
`tool_search` and `web_search` tools that remain come from codex's
global feature flags, not from a model entry.

The field list follows codex 0.147. `supports_reasoning_summaries` was
renamed to `supports_reasoning_summary_parameter`, which defaults to
true and is skipped while it holds that default, so the old switch wrote
a key codex ignores and read as off when the real answer was on.
`web_search_tool_type`, `supports_image_detail_original` and the three
`include_*_usage_instructions` flags join it. Sanitization now knows
each key's nullability and type: a `null` in `default_reasoning_summary`,
`shell_type` or `web_search_tool_type`, or a string where a boolean
belongs, makes codex reject the entire catalog and every model vanish
from the picker, so those values are dropped before they can reach the
file. The bundled fallback snapshot is regenerated from the same version.

Codex retires a model by flipping it to `hide` and keeping it as a
migration stub rather than deleting it, which is what it did to
`gpt-5.4` and `gpt-5.4-mini`. A removal recorded while one of those was
still listed lingered as a customization with nothing in the interface
to show for it, holding the notice up and keeping codeg in charge of the
whole model table for no gain. A removal now applies only to a model
codex still lists, so hidden stubs and codex's own internal entries
survive the rewrite; the notice and the decision to manage
`model_catalog_json` follow the removals that still apply, and stale
ones are dropped on the next edit. A list that no longer deviates from
codex's own hands the key back and takes the generated files with it —
together, since codex refuses to start on a reference to a file that is
gone, and only when the key points at the catalog codeg generated rather
than one the user wrote.
xintaofei and others added 30 commits August 28, 2026 20:50
…llapse

feat(chat): collapse completed turn progress
fix(claude): coalesce adjacent thinking fragments
Hand-rolling `cmd /C <shim> <path>` gave the target path the standard
CommandLineToArgvW quoting, which leaves `&` live for cmd to parse — a
workspace file named `a&calc` ran `calc` on launch. Passing the `.cmd`
shim as the program instead lets std build the cmd.exe line with
batch-specific quoting.

Spawning through tokio so the handle lands in the runtime's orphan
queue: a dropped `std::process::Child` is never waited on, leaving a
defunct entry per launch on unix.

Disable the row in remote-desktop windows, where Code would open on the
far host and read as a no-op here.
WebKit resolves `rem` on the root element against `font-size`'s initial value of
16px rather than the root's own font-size, and does so for every property, not
just `font-size` itself. Window zoom is one declaration — the appearance provider
writes `16 * zoom / 100` px onto `<html>` — so `:root`'s `line-height: 1.5rem`
was pinned at 24px forever on the engine the desktop app ships on. At 300% a 42px
CJK glyph carries a 50px content area into that 24px line box, and `truncate`'s
`overflow: hidden` cut 13px off the top and bottom of the sidebar nav labels,
conversation titles and the folder select alike. It is not a stale invalidation
either: a cold start at 300% computes 24px too.

`em` on the root element is its own font-size, which both engines resolve live —
24px at 100%, identical to the value it replaces, and 72px at 300%.
`calc(1.5 * 1rem)` and a `var()` holding `1.5rem` are pinned the same way, so em
is the only spelling that can stay on `:root`. Chromium follows the spec here,
which is why server mode in a browser never showed any of this.

A guard test holds `:root` to declaring no rem-valued regular property, and pins
the 16px baseline that the zoom level is read back out of. Custom properties are
exempt and stay in rem: their values are substituted at the use site and resolve
against the element that consumes them, not against the root.
The row already prints one of the two timestamps — its `timeLabel` is
`formatRelative` over `updated_at` or `created_at`, whichever the active sort
mode is keyed on — so spelling both out again in absolute form was the one part
of the bubble that repeated the row instead of completing it. They were also
its only half-width cells, every other field spanning both columns, so the
two-column grid and the locale-aware `Intl.DateTimeFormat` behind them go with
them. Created and updated stay in the right-click Session Details dialog, which
is what a full record is for, and the shared translation keys with them.

Branch moves above path. Both answer "where", but a branch is short and exact,
and it is the value that differs between two worktrees of one repo — the case
the bubble was built for — while an absolute path is the long wrapping line
that belongs under it. The `dir="ltr"` test reads its values in document order,
so it pins the new order; a second one holds the timestamps out.
`open in code & canary` has spaces, so the quoting the hand-rolled
`cmd /C` wrapper would have gotten quotes it anyway and the assertion
could not tell the two implementations apart. A whitespace-free name
falls through `append_arg` unquoted, which is the case that leaks.

Also walk back the reaping comment: tokio documents orphan cleanup as
best-effort with no timing guarantee.
Gemini CLI 0.55.1 to 0.57.0, OpenCode 1.18.23 to 1.18.25, Hermes 0.20.5 to
0.20.6, CodeBuddy 2.139.0 to 2.141.0, Kimi Code 0.39.0 to 0.39.1, Qoder 1.1.31
to 1.1.33. Every launch contract the registry encodes still holds under the new
pins: `--acp` and `--skip-trust` remain registered options on Gemini, `--acp` on
CodeBuddy, `acp` a subcommand on OpenCode, and each package's declared
`engines.node` still matches its `node_required` floor.

Hermes rides a community npm bridge, so its pin is exact and re-audited every
bump: the whole tarball except `package.json` is byte-identical to 0.20.4,
`postinstall.js` matches by digest, and the new upstream tag v2026.8.27
dereferences to the pinned commit 5fc308a7. Kimi stays clear of the
0.37.0-0.38.0 hole, where any stdio MCP server killed the session outright — its
converter still reads an absent transport as stdio and supplies the runtime_id
itself, so the codeg-mcp companion rides every session.

OpenCode publishes all six pinned platform assets under the new tag and each
archive still holds a bare `opencode`, so `dir_entry` stays `None`. Qoder's
config-dir resolver and Kimi's mcp.json schema are unchanged beneath the new
pins; the comments mirroring them now name the version they were last checked
against.
Preserve Codex parent thread metadata in list and detail summaries so the generic importer can reject delegation children before creating root rows.
fix(codex): skip native subagent session imports
Review fixes on top of the numbered-jump / reopen-closed-tab bindings.

closeTab now takes recordForReopen. The delete paths pass false: deleting
a conversation (detail header) and the sidebar / manage-dialog bulk delete
both close the tab right after deleteConversation, and closeTabsByFolder
runs when the folder itself stops existing. Without the opt-out, one press
of the reopen binding minted a tab — and an opened_tabs row — pointing at
a conversation that is gone.

A diff tab is no longer recorded. It carries the path it compares, but
reopening goes through openFilePreview, so restoring one silently swapped
the branch comparison or conflict view for the source editor.

The digit-row positional fallback now declines a key that typed a
character another binding owns by name. matchesDigitRowCode already
documented this hazard as "latent while nothing binds digits 1-9";
binding them makes it live, because AZERTY puts `-` on Digit6 and `_` on
Digit8 — one Ctrl+- press matched mod+- by key and mod+6 by code, and the
zoom listener preventDefaults without stopping propagation, so the window
zoomed out AND the app jumped to tab 6. shortcutsConflict cannot see the
pair, so Settings warned about nothing. Ctrl+Shift+6 still reaches tab 6
there, via the surplus-Shift tolerance digits already have.

pushClosedTab keys on the tab id and moves an existing entry to the top.
The file-tab closers record from inside a setFileTabs updater, which
React double-invokes under StrictMode and may replay on a discarded
render; move-to-top makes any number of extra passes land on the same
stack, including for a close-all loop.

Also: "close other file tabs" now records, matching its conversation
twin; and Ctrl+<digit> defers to a focused terminal, where it is a
control code (Ctrl+6 is vim's alternate-file Ctrl+^), the same carve-out
the zoom listener makes for Ctrl+-/Ctrl+=.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up. Declining to record at close time only covers a tab that
is still open. A conversation closed BEFORE it was deleted is already on
the stack: closeConversationTab finds no tab and returns early, so the
entry survived and Ctrl/Cmd+Shift+T reopened the deleted conversation.
Same for entries recorded before their folder was removed.

applyConversationRemove and applyFolderRemove now purge the stack. They
are the right funnels — the backend broadcasts Deleted to every client
including the one that asked, so this covers a local delete, another
window's, and a bulk delete where only some requests succeeded (each
success emits on its own).

The close-time opt-out stays: the broadcast can land before or after the
delete path's own closeTab, and only the opt-out stops that closeTab from
putting the entry straight back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`test_credential_helper_e2e_server_mode` overrode CODEG_DATA_DIR with a
bare `set_var` while holding only STATE_LOCK, which is private to this
module. `forge::auth`'s token-resolution test guards the same variable
with `temp_env::async_with_vars`. Two locks that don't know about each
other, and `keyring_store::tokens_file_path` re-reads the variable on
every call: within the window one test's `set_token` wrote under its
data dir while the other's reader looked under a different one.

That is the "no stored token for account acc-alt" failure that has been
landing on the ubuntu server CI cell and reading as though whichever PR
was in flight had caused it. The racer is `cfg(all(unix, not(tauri-
runtime)))`, which is why only the unix server cells ever showed it.

Both temp_env entry points take the one SERIAL_TEST mutex, so routing
this test through `with_vars` makes the pair mutually exclusive. It also
restores the variable on unwind, which the hand-rolled save/restore did
not.

Running the two together reproduced it 3/3 before and passes 5/5 after:
cargo test --no-default-features --bin codeg-server --lib -- forge::auth git_credential
Review follow-up. Purging on applyConversationRemove only sees entries
that are on the stack when the delete lands. A remote delete does not
close an open tab — it only drops the sidebar row — so the user can close
that tab by hand afterwards, recording a conversation this client already
knows is gone.

The reopen path now consults `deletedIds`, the permanent tombstone the
store already keeps for exactly this hazard (it is what stops a late
upsert resurrecting a deleted row). A deletion seen at any point wins,
whichever side of it the entry was recorded on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex after `codex archive`: reconnecting a conversation fails
session/load with an Internal error whose body names the archived
session and the unarchive command. That error fell outside
classify_session_load_failure, so it took the raw "Failed to load
session, starting new" path — the history was silently orphaned even
though a one-command fix exists.

Classify it as a third known case ("session_archived"). It now rides
the same SessionLoadFailed banner as resource_not_found /
session_unavailable (Reload or start new), and the banner text names
the exact unarchive command — the id is extracted from the error body
on the frontend; a body without a parseable id falls back to the raw
message so nothing actionable is lost. Custom agents keep their
silent local recovery (their history is codeg's own transcript).

i18n: backendErrors.sessionArchived x 10 locales, key parity kept.
PR #598 shipped the frontend `session_archived` case and its i18n keys,
but not the backend classification they depend on. Nothing in
classify_session_load_failure ever returned "session_archived", so the
new switch arm was unreachable and `codex archive` still fell through to
the raw "Failed to load session, starting new" path — the exact bug the
PR set out to fix. Its claimed Rust test did not exist either.

Classify "is archived" as the third known case, checked after the
structured -32002 verdict (which keeps its documented precedence) and
before the crash/ended family, so the more specific and recoverable
verdict wins if a body ever carries both signals.

Frontend, two corrections to the now-live path:
- Take the session id from the event instead of regex-scraping the RPC
  body. `session_id` IS the session the load failed for, so it is exact
  by construction, while the body only spells it out by convention —
  recovering it from there drifts the moment codex rewords the error.
- Only name `codex unarchive` when codex is actually the agent. The
  classification is matched on the wire message, so it is not
  codex-exclusive by construction, and prescribing a codex command to a
  Claude user is worse than showing the agent's own text.

Tests: the Rust classifier case the PR described, plus three frontend
cases covering the command, the reworded-body path, and the non-codex
fallback. Verified failing without their fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `code` field is documented in four places; the archived case updated
two of them. Bring the Rust event definition and the runtime store's
field comment in line too, so none of them still claims -32002 is the
only value a consumer can see.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The banner's whole promise is a command the user can run to get their
history back, but the surface it lands on is a one-line strip
(`text-ellipsis whitespace-nowrap`) sharing its row with two shrink-0
buttons. With the agent name and session id substituted in, the message
runs ~178 chars and the 36-char id sits at chars 57-109 — cut mid-UUID
at any realistic panel width, and unselectable from the `title`
tooltip. The one thing the user had to copy was the one thing they
could not.

Carry the command on the connection beside the localized message
(`loadErrorCommand`), and give the banner a copy action for it. The
message text is unchanged; this adds a way to actually obtain what it
names. Null for the failures with no way back, so the button is absent
rather than offering a recovery that does not exist.

The button sits outside `canShowDetailErrorActions` on purpose: that
gate is there because Reload refetches the DB detail and New session
opens a tab in the folder. Copying needs neither.

i18n: messageList.errorActionCopyCommand / errorActionCommandCopied
x 10 locales, key parity kept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems with the copy action as first written.

The banner is a single non-wrapping flex row whose every action is
shrink-0, so the message is the only child with any give. Adding a third
action therefore did not shrink anything — it crushed the message to
zero width and pushed "New conversation" out past the banner edge, which
the tab wrapper clips. Measured in headless Chrome across en/fr/de at
320/375/384px: 34-172px of overflow, worst in French. Adding a recovery
action was breaking the two recovery actions already there, at exactly
the narrow widths that motivated the change.

Give the row `flex-wrap` and the message a `min-w-40` floor. Measured
after: no overflow at any tested width, and at >=700px the layout is
byte-identical to before (one row, 39px, same message width) — the
second row appears only when the panel really is too narrow. This also
fixes a pre-existing clip: French at 320px overflowed by 7px with just
the original two buttons.

Second, the command is a string we invite the user to paste into a
shell, so the id must be the whole of what gets interpolated. Taking it
from the event (correct, and exact) dropped the implicit guard the
original regex had by accident — a scrape for a UUID could only ever
yield a UUID. Shape-check `session_id` against an anchored UUID pattern
and fall back to the raw message otherwise, so a value carrying a space
and a second word can never become a second command.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(chat): jump to a tab with Ctrl/Cmd+1-9 and reopen with Ctrl/Cmd+Shift+T
fix(acp): surface archived sessions as the known load-failure banner
Every assistant reply now carries a header: "Working..." while the agent is
writing, "Worked for 1m 9s" once it settles, and "Finished working" for the
window before the post-turn reparse backfills the duration. That header is
the only place a per-turn duration lives now — the footer's Timer chip
showed the same number behind a hover.

The fold state moved off the turns and onto the view. A reply's identity
changes twice on its way into history (the stream settling into a promoted
local turn, then the authoritative detail refetch renaming it), so keying
the expansion to the adapted `parts` dropped it at exactly the wrong moment
and a reply folded itself up as soon as it finished. `advanceReplyFold`
tracks the newest run positionally instead: it arms on the running edge,
holds that round expanded through settlement, and folds the thread behind
the next user send — which is also the state a historical conversation opens
in, nothing being live. Older replies keep an epoch-stamped override, so one
epoch bump folds everything above a new message without walking the thread.

A round is identified by the live message rather than by a render item,
which keeps a fresh reply that merges behind a settled one from reading as
that one resuming. An id that moves while the same reply keeps running — a
viewer attaching mid-turn, or a reconnect rebasing the client-minted id onto
the backend's — is latched rather than treated as a boundary; the one
sequence that still fools it, a boundary falling entirely inside a reconnect
gap, is documented at the branch. Replies that end on a tool call with no
trailing prose remain un-foldable, so no send can hide a stopped turn or a
card that IS the answer.

The header reads as a section head: full-width rule, chevron beside the
label, no hover treatment, no rail down the expanded body. The body slides
on a `grid-template-rows: 0fr <-> 1fr` keyframe instead of cross-fading, so
the answer below is pushed down and pulled back up. Animating the track
rather than a measured height keeps the promise `instant-collapsible` exists
for — nothing here reads layout — and a keyframe rather than a transition is
what its `animationend` presence check needs to unmount closing content. The
enter half is opt-in per toggle: this component re-mounts already open all
the time (the row key flips at settlement, the virtualizer recycles rows),
and every one of those would otherwise replay the unfold.
The hover bubble named the agent and then the status, which is what the row
underneath already badges where it matters — a spinner while a session runs, a
cross once it is cancelled. The model takes that place instead, lifted out of
the field list to sit beside the agent behind a middle dot. Agent and model are
one answer to what a session is, and the model had been a labelled row of its
own carrying a single short token, in a list otherwise made of folders, paths
and branches.

The dot rides inside the model chip rather than between two of them. A chip row
has no width of its own to truncate against and a model id's tail is what
distinguishes it, so a long one has to wrap — and a separator that is its own
flex item stays behind on the line above when it does, orphaned at the end.

`SessionIdentityChips` is shared with the Session Details dialog, so both chips
are opt-in now and each surface takes the one it lacks: the dialog has a
labelled field for the model and a title worth qualifying with the status, and
answers the bubble exactly. The row gap tightens so the separator hugs the two
names it divides, with the status chip carrying the difference as its own
inline-start margin, so the dialog reads as it did.
The hover bubble's model chip only ever appeared for imported sessions.
`conversations.model` is written once, at insert: from the parsed session file
on import, and NULL for a row the app creates itself — which it has to be,
since the row exists before the agent has named a model. Nothing updated it
afterwards, so it stayed NULL for the life of every session started in-app. The
details dialog hid this by parsing the transcript on open and patching the
summary it returns; the sidebar is served from the rows themselves and had
nothing to show.

An interactive open now seeds the column with the model that parse already
found, in the same shape as the auto-title seed beside it: one UPDATE with the
guards as SQL filters, writing the `model` column and nothing else.
`updated_at` must not move, or merely reading a conversation would float it to
the top of a recency-sorted sidebar. First value wins, and the sidebar upsert
that was already there carries it to the row, so the chip appears without a
refresh and without a frontend change. A title and a model landing on the same
fetch now broadcast once between them rather than twice.

Storing it at all has two consequences, and both are handled. The parse now
wins over the column when the returned detail is built, rather than filling it
only when NULL: with a value persisted, the old precedence would have pinned
the details dialog — which reads the summary ahead of the turns — to the first
model a session ever used, and a mid-session `/model` switch would never have
shown. And an unrelated rebind clears it, because `bind_external_id` re-points
a row from one session to another: the outgoing model travels to the row that
keeps that history, and a copy left behind would name the old session's model
on a row that is now the new one, uncorrectably, since the seed only ever fills
an empty column.
Read a pull request and merge it without opening a browser.
Every assistant reply now sits under a header you can fold away.

## New

- **A pull request opens as three tabs, and there's a Merge button.** Conversation, Checks and Files changed; click a file to see its diff; the merge menu lists only what the repository allows — merge commit, squash, or rebase.
- **Write a comment, close or reopen an issue, and file a new one — all in the repository panel.** These used to mean going to GitHub or GitLab in a browser.
- **Every reply has a header: "Working…" while the agent writes, "Worked for 1m 9s" when it's done.** Click it to fold the whole reply away; sending your next message folds the one above it. A reply that stops on a tool call keeps everything on screen.
- **Hover a session and a card floats out to the right** with its full title, agent and model, folder, branch and path. Sessions started inside codeg now show a model too — before, only imported ones did.
- **Ctrl/Cmd+1 through 9 jump to a tab, and Ctrl/Cmd+Shift+T reopens the one you just closed.** (#502, @Adam-Dalloul)
- **VS Code joins the "Open in" menu**, for folders, directories and files. (#591, @markos-ttl)
- **Antigravity can sign in on a machine with no browser.** codeg gives you a link, you sign in to Google on your phone or another computer, then paste the address it lands on back into codeg.
- **A conversation whose session was archived says so, and hands you the command to get it back.** One click copies `codex unarchive <id>`; before, it just said "Failed to load session, starting new". (#598, @asteroida123)
- **Codex now says why it needs your approval**, instead of a fixed title like "Edit files". Comes with the codex-acp 1.7.0 upgrade.

## Improved

- **Updated bundled agents:** Gemini CLI 0.57.0, OpenCode 1.18.25, Hermes 0.20.6, CodeBuddy 2.141.0, Kimi Code 0.39.1, Qoder 1.1.33.
- **Kimi Code takes MCP servers again**, which is what unblocked the jump from 0.36.1 to 0.39.1; its login hint now includes the `--region global` a kimi.ai account needs.
- **Saving MCP settings leaves fields codeg has no UI for exactly as they were**, instead of dropping them.
- **The pull request detail panel lines its description, comments and composer up on one left edge**, keeps the author's avatar next to you down a long comment, and is wider.

## Fixed

- **The interface grows when you zoom the window.** At 150% the text got bigger, but column widths, small labels and popovers stayed at their 100% size.
- **Chinese, Japanese and Korean text isn't clipped at high zoom** in the sidebar, conversation titles and dropdowns.
- **One stretch of thinking stays one card** while sub-agents are streaming beside it, instead of being cut into several. (#494, #586, @dawNotPoi)
- **A reply you're watching in a sub-agent transcript stops being stuck on "responding".** Its timing stats and artifacts card show up.
- **An image card keeps its filename or page name after a reload**, rather than reverting to "Image generation". (#504, @Adam-Dalloul)
- **An OpenCode tool card shows the result, not a blob of raw JSON.** Watching live, a question you'd already answered read as "no selection".
- **Sessions Codex delegates to sub-agents no longer show up as separate sessions in your history.** (#593, @TroyMitchell911)
- **When a turn produces nothing, nothing comes back** — not the previous turn's answer.

Thanks to @Adam-Dalloul, @asteroida123, @dawNotPoi, @markos-ttl and @TroyMitchell911 for contributing to this release.

-----------------------------

# 发布版本 0.29.0

读 PR、合并 PR,不用再打开浏览器。
每一轮回复都收进一条可以折叠的标题栏里。

## 新增

- **PR 打开后分成三个标签页,右边还有「合并」按钮。** 「对话」「检查项」「文件更改」各占一页,点开文件就能看 diff;合并方式只列这个仓库允许的那几种——创建合并提交、压缩后合并、变基后合并。
- **在仓库面板里就能发评论、关闭或重新打开议题,也能新建议题。** 以前这些都得跳去 GitHub、GitLab 的网页上做。
- **每一轮回复顶上都有一条标题栏:智能体在写时显示「正在工作中…」,写完变成「工作了 1 分 9 秒」。** 点它就能把整轮回复折起来,发出下一条消息时上一轮自动折叠。停在工具调用上、还没给出回答的回合不会被折走。
- **鼠标停在会话上,右边会浮出一张卡片**,写着完整标题、智能体和模型、所在文件夹、分支和路径。在 codeg 里新建的会话现在也能看到模型名了,以前只有导入的会话才有。
- **Ctrl/Cmd + 1 到 9 直接跳到第几个标签,Ctrl/Cmd + Shift + T 重新打开刚关掉的那个。**(#502@Adam-Dalloul)
- **右键菜单的「打开于」里多了 VS Code**,文件夹、目录、文件都能用。(#591@markos-ttl)
- **Antigravity 可以在没有浏览器的机器上登录。** codeg 给你一个链接,你用手机或另一台电脑打开、登录 Google 账号,再把跳转后的地址粘回来就完成了。
- **会话在 Codex 那边被归档时,横幅会直接说明,并给出找回历史的命令。** 点一下就复制 `codex unarchive <id>`;以前只会显示「加载会话失败,正在新建会话」。(#598@asteroida123)
- **Codex 会说明它为什么要请求这次授权**,不再只显示「编辑文件」这种固定标题。随 codex-acp 1.7.0 升级一起到来。

## 改进

- **内置智能体版本更新:** Gemini CLI 0.57.0、OpenCode 1.18.25、Hermes 0.20.6、CodeBuddy 2.141.0、Kimi Code 0.39.1、Qoder 1.1.33。
- **Kimi Code 重新支持 MCP 服务器**,卡在 0.36.1 的版本这才升到 0.39.1;登录提示里也补上了 kimi.ai 账号要加的 `--region global`。
- **保存 MCP 设置时,codeg 界面上没有的那些字段会原样留着**,不会被清掉。
- **PR 详情面板的描述、评论和输入框对齐到同一条左边线**,读长评论时作者头像一直停在旁边,面板整体也更宽了。

## 修复

- **调窗口缩放时,整个界面跟着一起变大。** 之前调到 150%,字是变大了,但栏宽、小号标签和弹出层还是 100% 的尺寸。
- **缩放调高后中文不再被切掉上下半截**,侧边栏、会话标题、下拉框都恢复正常。
- **旁边有子智能体在输出时,一段思考仍然是一整张卡片**,不会被切成好几段。(#494#586@dawNotPoi)
- **打开子智能体的转写去看,回复不会一直卡在「响应中」**,耗时统计和产物卡片也能正常显示。
- **图片卡片刷新页面后还是原来的名字**(文件名或页面名),不会变回「Image generation」。(#504@Adam-Dalloul)
- **OpenCode 的工具卡片显示的是结果,不是一段原始 JSON。** 之前实时看的时候,已经回答过的提问会显示成「未选择」。
- **Codex 委托给子智能体的会话,不会再作为独立会话出现在历史列表里。**(#593@TroyMitchell911)
- **某一轮什么都没输出时,返回的就是空**,不会把上一轮的回答顶上来。

感谢 @Adam-Dalloul@asteroida123@dawNotPoi@markos-ttl@TroyMitchell911 为本次发布做出的贡献。
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

⤵️ pull merge-conflict Resolve conflicts manually

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants