Repository navigation
Pin today's GitHub behaviour before adding GitLab - #5
Open
HugoHeneault wants to merge 2243 commits into
Open
HugoHeneault wants to merge 2243 commits into
HugoHeneault wants to merge 2243 commits into
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
setuid clears the dumpable flag, and reading /proc/<pid>/exe of a non-dumpable process needs CAP_SYS_PTRACE, which a Sprite does not grant root. The supervisor could identify no exec and refused every sandbox. The child restores the flag itself after dropping privileges; the shield still makes the credential-bearing process undumpable, after the exec stop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "Worked for 9m 24s" rule was a dead control on most turns. `isOpen` asked one question, `unfolded.contains(firstSeq) || !hiddenSeqs.isDisjoint(revealed)`, and `revealed` is the rows something has asked to be on screen: the unread mark, a search hit, the place a returning reader was restored to, and every tool result they had ever opened. A turn that ran while somebody was away holds the unread mark by definition, so it was open, and stayed open, whatever they pressed. The click flipped a set the question then ignored, the chevron never moved, and the entry's content key never moved either, so the table did not even reload the cell. Nothing happened at all. So the reader's own answer is asked first, and it has three states rather than two: opened, closed, and never said. One set holds two, which is why "not in unfoldedTurns" was read as "closed" and was not. `refoldedTurns` is the third. And `expanded` comes off the segment's question entirely. Keeping a run open around a tool result somebody opened costs a handful of rows; a segment is the whole of a turn's working, seventy rows of it in the report this was written for, and one expanded call anywhere inside pinned all seventy for the visit. Collapsing a segment is allowed to hide an expanded row: that is what collapsing means. The second half is the line that could not hide anything either way. `shows` was `hasHiddenRows`, which ignores the drawn window, while the removal refuses any segment the window stops inside. `wouldHide` is now the one answer both take, so the rule is drawn only over work it can actually take off the table, which is what `foldEntry` has always done and for the reason written there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A run of background agents reporting in drew one rule between every pair of notices: "Background agent finished Run Pint", a rule saying 18s, the next notice, a rule saying 21s, and so on down the pane. Each of those stands for a span with nothing in it a reader would go looking for, so what they add is furniture. A background wake still opens a segment, because the work under it is a turn nobody sent and the whole of `BackgroundWake` exists to say so. What it no longer does is close the one above it behind a rule. The rule is a turn's answer to how long that took ME, and nothing about a task finishing in the background is an answer to that. A person interrupting still gets one, which is what it was asked for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The plate under a finished turn put "A background command running" in the emphasised line and the name of the work in the quietest ink beneath it, which is the opposite of the argument written at the head of BackgroundWork: named rather than counted, because a count answers the question with another one. One running task now leads with its own name and demotes the noun and a clock under it; several keep the count in the lead, because the names no longer fit there. The clock is a start date on the note rather than a rendered duration, so a tool_progress tick a second does not make a new note and rebuild the transcript's table, which is what TranscriptModel.backgroundWork exists to avoid. The footer ticks it in a TimelineView, and only when there is one clock to read. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The app's own Settings window lists its panes down the left, and this window chose between them in a preference toolbar at the top, so the two settings windows disagreed about where that choice lives. It is a sidebar here now, the same fixed-width List, and the NSToolbar that drew the old row is gone. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sidebar held the set of workspaces somebody had OPENED, so a fan-out drew a count and nothing else until it was clicked. The rows that count stands for are the one thing the pane says about what a running turn is doing, and a reader who has to click to find that out is a reader who does not know there is anything to click. The set now names the exceptions instead: open by default, folded for the workspace that spawns a dozen. What made open affordable is that the rows were already bounded from the other end after this was first decided. A success is gone 2.5 seconds after it lands, crosses stop at three, and a backgrounded command never had a row at all. See SubagentRetention. The state moves to SubagentDisclosure in the core, because a default held in a view's @State is a default nothing can test, and this one has now been decided twice. And the count button stops taking clicks it cannot be seen to take. It yields to the hover controls with .opacity(0), which still receives hits, so reaching for the archive box occasionally folded a fan-out instead. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Folding the sidebar animates the centre pane's width over a quarter of a second, and the whole conversation rewrapped on every frame of it: line counts changing under the reader twenty times on the way to a layout they did not ask to watch. This is the behaviour #255 took out, put back. Its reasoning was sound as far as it went: measuring a row fell from about two milliseconds to a tenth of that, so reflowing the visible rows on the frame became affordable. Affordable is not calm. What the reader wants from a pane changing width is the pane changing width. So TranscriptHoldView keeps the scroll view at the frame it had for the whole of a width change, clipped so the frozen pane cannot draw over its neighbour, and lays out once and crossfades when the width stops moving. Every width change takes this path: a pane divider, a window edge, a zoom, the inspector, and the sidebar that prompted it. CenterPaneDivider posts the began and ended pair again, because a SwiftUI drag is not a live resize as far as AppKit is concerned, and a sidebar fold sends nothing at all and is released by the quiet deadline. Gone with it: didChangeWidth off the table view, and widthChanged, settleWidth, measureMargin and reflowStep off the coordinator. Kept from #255: setColumnActive for the two column cache, the layout probe in Snapshot.isProbing, and the compact_boundary correction in the tests. Its probe block asserting that a resize does not hide the transcript is gone, since it now asserts the opposite of what the pane does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six days of runs to 22 September came to 8,584 macOS minutes, around 3,400 US dollars a month, and every Linux job in all three workflows together came to about 34. So `build` and `test` are the bill and nothing else is. Two branches that were not ready to merge took 309 of the 491 pull request runs and 51 per cent of those minutes, and a fifth of all macOS minutes went to runs that `cancel-in-progress` killed part-way, which is the concurrency rule working and still money. Both are the same thing: the app compiled and the suite run on every intermediate commit of a branch about to be rewritten. So the macOS pair is skipped while a pull request is a draft, with `ready_for_review` added to the trigger so the full run arrives the moment it stops being one. `lint`, `linux` and `packaged-runner` still run on every push of a draft, so the house rules, the protocol schema, the archive modes and BloomCore compiling for Linux answer for every commit as before. Second and much smaller, `changes` skips the same pair when a pull request touches only prose or the Python the Linux jobs drive. Thirteen per cent of a 120 commit sample touched no Swift at all. It fails open on anything it cannot read, including the 3,000 file ceiling that endpoint does not announce, and it deliberately does not list `Tools/**.sh` or `Protocol/`: the macOS jobs execute the first and `StoredPayloadVectorTests` reads the second. The Linux dependency cache is from #355, which this supersedes. It buys wall clock rather than money, and the comment says so. `build` and `test` are left as two jobs. Merging them was considered and does not save: `Tools/test-core.sh` builds a mirrored package under its own `--scratch-path`, so the two share no compiled output and merging would only serialise them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the reflow" This reverts commit ecb7e8d.
…385)
* Ask the launch gate what a credential is for, not the process tree
Every Cloud workspace has refused to clone a private repository since person-bound
credentials landed, and the reason was one design choice inside them: a credential
request proved itself by walking the caller's process tree. The credential helper's
parent had to be git-remote-https, whose parent had to be git, whose parent had to be
the runner's wrapper, each read out of /proc and matched against a fixed shape.
That shape cannot hold on a machine. Reading /proc/<pid>/exe, cwd or environ of a
process that is not dumpable needs CAP_SYS_PTRACE, which no Sprite grants to anyone,
and the credential wrappers make themselves non-dumpable before asking, so the
supervisor could not read the very process it had to identify. On top of that the
shape encoded how git spawns its helpers, which git does not promise: it runs a second
copy of itself out of its exec path, so there is one more generation than the model
allowed and the copy is a different file from /usr/bin/git. Four separate links broke,
one at a time, each hidden behind the last, and every one of them passed in CI because
CI runs --privileged and therefore has the capability.
What decides now is the launch gate, which was already there and has never failed: the
uid the kernel reports for the socket peer, the uid and cgroup of the reservation the
one-time launch nonce opened, the process group, and a sandbox leader still alive and
unchanged. The caller states what the credential is for, and the git wrapper passes
that down to the credential helper git spawns, because the helper is only ever told
`get`. Taking the caller's word is worth what the sandbox is worth, which is what every
other decision here already rests on; what a token is ultimately good for is scoped by
runbloom.app, on the server, not here.
Three things found while proving this, each of which was the next failure in line:
- A network git command runs with GIT_CONFIG_NOSYSTEM=1 so nothing a caller writes
into a config file can reach the transport, and that also discards the
safe.directory provisioning writes. A worktree cloned under one person's uid
refused every push by another's as "dubious ownership" while git commit beside it
worked. The worktree is named as a safe directory from machine configuration.
- The launcher strips every BLOOM_PERSON_BOUND_ key before exec, which is right, but
it took the worktree with it. It goes back under a plain name: it is the directory
the agent is working in, not a secret.
- The runtime self-test asked twice on purpose, and kept the second answer, so it
reported attested=false on a host where the exchange had in fact worked. It keeps
the first, and the second ask is a replay of the same operation, which is what the
guard it is testing actually defends against.
The setup note that used to say only that service identities could not be registered
now says which error, because without it a failed setup beside a clone that plainly
worked gives a person nothing to act on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Make the smoke clone, commit and push, and say what setup recorded
The Runner faults of 21 and 22 September were all in one sequence, and the smoke ran
none of it. It named a repository that does not exist, so setup never cloned; it waited
for a `.git` directory, which git makes in the first second and leaves behind empty
when a clone fails, so it reported CLONE OK through six failing clones in a row and the
commit that followed said "root-commit" where nobody read the word. Meanwhile setup
writes a file per step under the data directory rather than to stderr, so the sentence
naming a failure sat in a file the smoke never opened.
So: name a repository and it performs setup's real clone, asserts it by the commits it
brought, then commits and pushes through the hardened git, and prints what setup itself
recorded either way. The push goes through the real RPC, which registers a turn, takes
a credential grant, launches a person-bound sandbox and runs git push inside it, so it
is the path an agent takes rather than a stand-in for it. The default stays offline and
clones nothing, because two CI jobs run this and neither should need github.com.
`Tools/sprite-smoke.sh --from-source` is the loop to work in. It builds the Runner for
this architecture into a cached volume and boots it on a host with a Sprite's
capabilities, so a change is measured in about two minutes rather than in a seventeen
minute release somebody is waiting on. Every fault above was found with it, after the
seventeen minute loop had been chosen four times in a row.
Two smaller things it turned up. The fake runbloom.app had no do_PUT, so http.server
answered 501 to every PUT and the /service-manifest branch below it could never run:
setup treats that failure as fatal and runs none of the project's own scripts, so a
smoke without it reported a machine that set itself up when it had stopped after the
clone. And it can serve a real token when a rig supplies one, which is what makes a
private clone a real clone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Apply the local diff without A && B || C
shellcheck's SC2015 on the export step, which the lint job runs and this Mac does
not: `cmd | (cd x && apply || true)` reads as if-then-else and is not one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Answer "may this ask" by reading what the supervisor recorded
The first pass at this removed the process-tree walk and put nothing in its place,
and CI was right to refuse it: `liveLinuxFoundation` runs a fixture that is a
repository's own script asking for a credential, and it got one. A checkout is
somebody else's code. A `.bloom` setup script that can reach the credential exchange
holds the person's GitHub token and can spend it, or read it straight off the
credential helper's stdout, and every repository opened in a Cloud workspace would
have been able to. That was mine to get right and the test caught it.
What is put back is not the walk. The sandbox supervisor already traces every exec in
a reservation, already marks a process running code out of the worktree as tainted,
already inherits that taint to children, and already defaults to tainted; it has been
writing a record per exec for some time, owned by root and named by pid and start
time, and only the readers of those records were deleted. So the question is answered
by reading the one that was written when the exec happened, rather than by
re-deriving it from /proc at the moment of the request, which is the part that could
not work: reading another uid's `exe` or `cwd` needs CAP_SYS_PTRACE and no machine
grants it.
The whole check is now: the caller is still the process that asked, its record exists,
and the record says trusted. It is a separate root process for one reason, which is
that the record is `0400 root` so that the sandbox user can neither forge nor read
one. Nothing in it walks a tree, recognises a git helper, or depends on a capability.
It refuses both shapes the fixture can take, which the walk did not: a script that
spawns the wrapper is refused because taint is inherited, and a script that `exec`s
into the wrapper, leaving no shell above it and an ancestry identical to an agent's
own command, is refused because the record belongs to the process rather than to what
it is currently running.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…s refused (#386)
No Cloud workspace has been able to clone since person-bound credentials landed, and
the cause was one null. GitHub returns `expires_at` for a user token only when the
OAuth app has expiring user tokens switched on; with that off the token is valid
indefinitely and runbloom.app has nothing to send, so it sends null. This required the
field, discarded the whole answer when it was missing, and the clone died as "fatal:
unable to get password from user".
An absent expiry now means "hold it for an hour", which is what the field is for on
this side: it bounds how long the supervisor keeps a credential, not how long the
token works, and the next operation fetches another one anyway. Measured on a machine
with the token's expiry back to null, which is its real state: a private repository of
3,341 commits clones.
**The null was not what cost the day. The silence was.** Seven places in this chain
returned a bare `1` or `nil`, git renders every one of them as the same sentence, and
the machine logged nothing beside it, so three wrong diagnoses were paid for with three
Runner releases before anyone could see which check had refused. Every one of them now
names itself:
- `git-v2` and `gh-v2`, on classification and on an untrusted tool path
- `git-credential-v2`, on each of its four refusals, into the clone's own step log
where a person reads it
- the broker, on the version, the repository, an unmatched launch-gate reservation,
an attestor that would not vouch, and a spent or missing grant
- the exchange with runbloom.app, which was caught and dropped without a word, and
which is where this bug actually was
- the credential socket, when a request is unreadable or undecodable
- the accept loop, which stopped serving for good on any errno but EINTR and said
nothing, so a caller saw a socket that accepts and then answers nothing
- `isolateNetworkGit`, whose refusal silently falls back to a plain `git clone` that
cannot authenticate, which is the shape this whole failure wore
`store` and `erase` are answered rather than refused. git calls `store` after every
successful authentication, and refusing it put a line that reads like a fault into the
log of a clone that had just worked.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…IDs (#387) Two faults, each hidden behind the last and behind the credential expiry before them. With all three fixed a Cloud workspace clones a private repository, provisions, runs the project's prepare script, obtains its setup secrets and runs its setup script. **A service without an id stopped setup before any repository code ran.** Every `[[services]]` entry is registered with runbloom.app as an identity first, an empty id is refused there, and the refusal is fatal to the whole run. Settings that name their services and give no ids are what they looked like before ids existed, and they are what repositories still have, so the answer was "The secret handoff request is not valid" and a workspace that had cloned and then did nothing. Asking for an id and a name separately buys nothing when the name is already unique, so the name becomes the id, lowercased, with anything outside the allowed set turned into a hyphen. A service that writes its own id keeps it. **A handoff id in lower case is a ULID too.** Crockford's base32 is case insensitive and runbloom.app writes its ULIDs in lower case, but this accepted only upper case and therefore refused every secret handoff it was ever given: the machine installed its packages and then would not run the setup script, saying only that it could not obtain the setup secrets. The handoff was left pending as well, because the rejection happens before the path that cancels one. The test that covered this used an upper case id, which is why it never caught it; the new one uses the id the server actually sends. Measured on a machine, each step of it: 3,341 commits cloned, php8.5 and postgresql-18 installed by the repository's own prepare script, a setup handoff consumed at 16:28:46, and the setup script running to a failure of its own inside the repository. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Give a setup script the groups its user is actually in A workspace cloned, installed its packages, and then could not write to its own checkout: `composer install` stopped at "The bootstrap/cache directory must be present and writable" on a directory that is present, is group writable, and which the same user can write to one line later. Supplementary groups are fixed when a process starts, and the Runner starts without any: the provider launches it with an empty set, `provision-sandbox.sh` adds the machine's user to the shared group afterwards, and nothing re-reads it. `/etc/group` says the user is a member and `/proc/<pid>/status` says `Groups:` and nothing else. That cost nothing while the worktree belonged to that same user. It costs the whole setup now that the clone runs under a person's own uid, because the checkout is left owned by that uid and shared through the group this process has not got. So a privileged script could read every file in the repository and write none of them. `sudo` calls `initgroups`, so asking it for the user we already are is the whole fix, and it is asked only on a machine that has the shared group and is not in it. Measured on a Sprite, one line apart: the same PHP answers NO without this and YES with it, and with it a workspace runs its prepare script, obtains its setup secrets, migrates, builds its assets and reports "There There is set up in 74s". This is the fourth failure from one decision, after `safe.directory` on read, the same on push, and the umask that left the clone's files not group writable. They are all the same shape: the person-bound work made the checkout belong to a per-person uid, and each thing that touches a file had to be taught about it separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Ask sudo whether it will answer, rather than whether it exists The Linux CI job runs the test suite before it writes any sudoers rule, so a wrapped command there could not run at all and the setup output it was meant to stream never arrived. A machine has the rule from provision-sandbox.sh, which has to have succeeded for the Runner to start, so the difference is worth one `sudo -n true` asked once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… (#390) Opus 5.5 came out on 22 September 2026. The picker already lists it, because Claude Code's models come from the CLI, so what changes is the written fallback label, the prices ClaudeModelRank is ordered by, and ModelLabel, which read the CLI's own id claude-opus-5-5[1m] as "Opus 5 5 (1m)". Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…#389) * Find an agent on PATH when the sandbox starts it Every agent turn on a Cloud workspace exited 127 with "person-bound exec failed with errno 2", with the binary sitting exactly where it always had. Agents are launched by name, `claude`, and found through the root-owned links provisioning puts in /usr/local/bin. The sandbox this replaced started them with `setpriv`, which searches PATH; the person-bound launcher calls `execve`, which does not. Every test launched an absolute path, and no agent turn had run through it on a machine. A command named without a directory is now looked up on the PATH the child is about to run with, checked with `access` as the uid it has just become, which is what a shell does. The failure message also names the path it tried, because "errno 2" on its own was the whole of what anyone got. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run every sandboxed process as the one agent user Every reservation used to get the next uid from 200000, so a workspace's clone, its setup and each agent turn ran as different Unix users, even for a single person, and every file one of them wrote was a permission boundary for the next. That one decision is behind a whole day of failures, each hidden behind the last: git refused its own checkout, pushes refused it again, setup scripts could not write, the agent could not be found on PATH, and then could not read its own MCP config. A second person in the same workspace would have hit all of them again between two sandbox uids. What it protected was thin. People sharing a workspace already share its checkout, its database and its services. So every reservation now runs as the machine's one agent user, `bloom-agent`, looked up rather than assumed. What keeps a reservation's GitHub credential its own is unchanged: the launch gate's one-time nonce and the cgroup and process group it binds, which a shared uid does not affect, and cleanup still acts on the process group and the cgroup, never on the uid. A repository's own scripts are still refused a credential by the supervisor's provenance record, and the provider socket is still masked. The launcher accepts exactly the agent user's uid, not any low uid, and skips the "unused uid" check only for that one user, which is shared on purpose. With no agent user, as on a Mac or a test host, the old fresh-uid mode is kept. What is given up: two collaborators' agents in one workspace are the same Unix user, so one could in principle read the other's in-flight process memory while a token is held. The bridge is also shared with the sandbox group explicitly now. The runtime chose owner-only whenever it had no `sandbox`, and the person-bound machine passes none, so the agent could neither read its MCP config nor reach the socket: claude exited 1 with "Invalid MCP configuration: ... EACCES". Measured with CI's privileged live sandbox test run locally, agent user present: the live exchange succeeds, a replay is refused, and a repository script is refused as tainted-inherited. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Package the CI Runner into an empty directory A cache saved before runner-package was excluded from it is still restored by the restore-keys prefix, so a stale tarball from an earlier run sat beside this run's and tar -xzf package/*.tar.gz expanded to two files, failing packaged-runner with 'Not found in archive' on a branch that had been pushed twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Forward the Anthropic betas Claude names Claude sends fields such as context_management that Anthropic refuses unless their beta is named in anthropic-beta. The local gateway kept only the body, so every turn came back 400. It now passes the names to runbloom.app as anthropic_beta, plain tokens only, Anthropic only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep the agent's home, and leave privileged setup group writable A launch as the agent user got a home that was removed when the launch ended, and Claude's conversations went with it: the next message in the chat resumed a session that no longer existed and never reached the model. The agent user now keeps its own home. A numeric uid still gets the throwaway one. Privileged setup ran under the Runner's umask 077, so vendor/ and .env came out owner only and the resume script, running as the sandbox user, could not read them. It now runs under umask 002. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Accept the model Claude resolves from its grant claude-opus-5[1m] goes out as claude-opus-5 and opus[1m] as the current claude-opus model, so the exact comparison refused every such turn with "The model request does not match its grant". The window is dropped, an alias admits only its own family, and the resolved model is passed on for runbloom.app to check again and send upstream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A new Cloud workspace used to show a centred placeholder and a banner saying the same thing, then a blank pane, then the raw setup log, which grew, turned into a grey card half way through and began losing its first lines. It now shows one card from the first second, in the web client's shape: the workspace's name, its repository and branches, five steps with the current one alive, the step's newest heading and output line under it, per step times where this window watched them, and the log folded away. The same card is drawn in the empty pane and at the head of the conversation, so the first chat arriving adds a composer under it rather than replacing the screen. The resume script is part of "Starting services", a failure marks its step and opens the log, Stop stays as a quiet link, and the queued message says "Sends when the workspace is ready." The steps, the line under the current one and the failure are CloudSetupProgress in BloomClient, with tests; the times are CloudSetupClock. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Switched off in #329 until terminals had a person-bound identity of their own, which was never built, so a Cloud workspace had no shell and the client retried for ever. Terminals run as the sandbox user every agent already runs as. Network git and gh in a pane are refused rather than handed a person's credential. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
RootView lost its navigationTitle in c11b44a, and dev-build.sh was updated for it; cloud-build.sh was not, so make cloud refused to build. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Keep sidebar row content visible at rest
Keep Bloom responsive after interrupted text selection
Remove closed search overlay from window
The arrival effect wrapped a reply in TimelineView(.animation) behind an if let, so the whole reply could be rebuilt on its second token while its markdown VStack was growing. That crashed inside SwiftUI's stack layout a few seconds after thinking ended and the answer began to stream. The content now keeps one structure and the fade is a single explicit opacity animation.
The live scope already existed and was only missing from the strip. It is labelled Active and carries a count like All and Archived.
spatie#22) * Keep the keyboard in the composer while an agent drives a browser pane * Shorten the browser keyboard comments
The last model, effort and backend picked in the composer or create window feed the next workspace. A default preset and a repository models.default still outrank it, and Settings sits below. A Codex model missing from the account's fetched model list is never used as a default, so the next layer answers instead.
…tie#16) * 🐛(archive): offer to archive anyway when the archive script fails A failing archive script left the workspace stuck with no way out in the UI. The refusal now shows the script output and an Archive Anyway button that skips the script, with a warning that whatever it was meant to remove is left behind. * ✨(archive): add a retry button to the archive script failure dialog Retry runs the archive script again for failures that are the machine's, such as Docker not running yet. A second failure raises the same dialog.
…d lists (#5) - TranscriptTextView kept one measurement per run, so every size question at a new width typeset the whole run again in TextKit. - SwiftUI asks a run inside an HStack, which is every markdown list item, at 0, infinity, about 1 and its own width in every pass. Each question evicted the previous answer. - TranscriptTextMeasureCache (BloomCore) keeps up to eight answers per run, drops the least recently used first, and is cleared when the text changes. The key is the proposal plus alignsBubbleInk, which is what lastMeasurement effectively compared. - A cache hit still moves the text container to the asked width and restores the bubble ink offset, so what is drawn is unchanged. - Tests for the cache rules in TranscriptTextMeasureCacheTests, and one in TranscriptTextMeasureTests for why zero and a hair need separate keys. Why: on a contributor's conversations, returning to the heaviest workspace held the main thread for a median 5.2 s (worst 9.7 s) over six visits, and 1.0 s (worst 1.1 s) with this change. About 82% of roughly 320,000 size questions had missed the single-entry cache and were typeset again. Co-authored-by: Frederik Vincx <frederik.vincx@gmail.com>
…patie#11) Selection stopped at the end of the answer it started in, and a sent message could only be copied by selecting it first. Select All (Cmd+A) and a drag that leaves the answer now span the conversation. The table recycles cells as they scroll away, so a selection kept in text views would lose rows on a long drag. It is kept as a description instead: the two rows the drag started and ended in, the words selected in each, and every row between. Copy composes the text from the stored rows (ConversationText), labelled You and Agent, and the highlight is painted onto whichever views exist and onto any that appear. The table takes the keyboard while a selection spans rows, so Copy and Select All survive the view the drag began in being recycled. The drag loop no longer asks the text view it started in for its window or scroll view on each pass, which ended the drag, and the autoscroll with it, as soon as that row was recycled. A sent message gets the copy button an agent answer already has, shown on hover in the margin beside the bubble.
) Every crash of one shape, EXC_BAD_ACCESS and once an impossible allocation inside SwiftUI's stack layout with no frame of ours on the stack, came while a long answer was streaming, and the layout frames read back to the streaming row's MarkdownBlocksView. While the answer arrives it is now one Text, and the stored row draws markdown as before. A fenced block shows its markers until the answer lands.
…atie#30) Preserve the file review Option+V shortcut. Co-authored-by: Hugo Heneault <4112256+HugoHeneault@users.noreply.github.com>
Carry forward the public fixes and the private release source, including project agent defaults and local web access.
Bring private Bloom history into the public repository
The split button is hosted in AppKit and keyed on the merge method only, so it could keep its disabled look after a pipeline turned green while the headline already said Ready to merge. Its identity now includes canMerge.
Characterisation tests for every gh command run through GitHub.run, the remote to host and repository resolution, the default prompts and instruction files, the pull request tools' listings, the direct create and merge routes' refusals and outcomes, and the keys and raw values that outlive a launch. Long texts are held in fixtures under Tests/fixtures/characterisation, recorded from a CI run.
HugoHeneault
force-pushed
the
HugoHeneault/add-gitlab-support
branch
from
October 13, 2026 12:35
b69b32f to
75ffe2a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Généré par Claude Opus 5.5
Step 1 of 6. Part of a stack for GitLab support, issue 4 on spatie/bloom. Each PR targets the previous step, so its diff is that step alone. Nothing changes for a project on GitHub: the characterisation tests from step 1 are never edited by later steps.
Characterisation tests only, no app code. They pin today's GitHub behaviour exactly, so the following steps can prove they leave it alone:
ghcommand run throughGitHub.run, argument by argument--repogh is pointed atTests/fixtures/characterisation