Import Maple Agent GPUI with shared dependencies and isolated CI - #885
Merged
Merged
Conversation
The screenshot, display, and upload instructions only applied to the original developer's workstation. Keep the build, log, and performance guidance that applies to any machine.
Start the version history fresh for the handoff.
Startup validated the persisted session and deleted auth.json on any failure, including a timeout or an offline machine. A user who opened the app without a network was signed out of a still valid session. Only remove the file when the backend refuses the credentials or they belong to another account. Transport errors keep the file for the next launch.
Only the login form created at startup listened for a successful sign-in. The form created after a sign-out had no listener, so a second sign-in in the same run did nothing until the app restarted. Subscribe in one place for every login form, and make the switch to the login form idempotent so the Settings sign-out button and the later backend confirmation do not replace the form twice.
The Settings screen saved a full snapshot taken at startup on every toggle, which overwrote the pinned projects and project names that the chat screen had saved since. Closing Settings then copied the stale pin list back into the sidebar. Chat writes also raced each other with separate load-modify-save threads, so a pin followed by a rename could lose one of the two. Route every write through a single writer thread that applies each change as a read-modify-write of the file in call order. Callers now name the field they change instead of passing a snapshot, and the quit path waits for the queue to drain so a toggle just before quit is not lost.
A permission request that arrived while another task was on screen was dropped, and opening the asking task cleared the card again. The run stayed blocked until the user pressed Stop. Hold pending requests per session, like questions, and show the one for the selected session. A reply or a decided row removes only its own request, so a second request that replaced the card is not lost when the first reply lands.
The Web chip kept the value of the previous task when the user switched tasks, so the chip showed the wrong state and the next toggle sent the opposite of what the runtime had. Copy the session's stored value when a task is opened and when the runtime reports an update for the selected task.
Switching tasks set the new selection before it ended the queued message edit, so the release went to the new task and the chip stayed held in the old one.
The task list auto-selected the newest task under the current project without checking the archived flag, so archiving the last open task could select an archived one and send new messages to it. Removing a project also left a pinned project in the sidebar forever: the pin was never dropped, and an archived task counted as a reason to keep the project. Drop the pin, ignore archived tasks when deciding which projects exist, and choose the fallback project from every known project rather than from a sidebar list that a search may have narrowed.
Url::host_str keeps the brackets around an IPv6 address, so the address never parsed as an IP. A loopback MCP server on [::1] was refused as non-loopback, and the public-host rules for IPv6 web fetches were never reached. Strip the brackets before the address is parsed.
Starting the runtime marked the launch root as trusted whenever the saved answer was not trusted, which also replaced an explicit refusal. Project skills then loaded without the prompt the README promises. Only trust a root that has no saved answer.
Three paths could slice the composer text at a stale or wrong offset: the IME selection was offset by the end of the replaced range instead of its start, set_text and clear kept an open IME composition, and the right-click menu items held a word range that later typing could move off a character boundary. Each could panic on copy or on a suggestion click. Offset the IME selection from the start of the replaced range and clamp it, reset composition and menu state whenever the text is replaced, and check the menu range with str::get before slicing.
Two stub functions existed only to keep an import alive, a relative time formatter had no caller outside its own test, and the task delete path had no UI entry point.
Several runtime handle methods, two request types, and a billing constructor had no caller outside their own crate or tests. Helpers that only tests still use are now compiled for tests only.
save_config_for_scope used std::fs::write followed by a chmod. A crash between the truncating open and the final write leaves a partial JSON file that load_config rejects, and `maple-gpui acp` then refuses to start until the file is removed by hand. Route the write through private_file::write_private_json, which writes a 0600 temp file in the same directory, syncs it, and renames it over the target, so the old config survives an interrupted save. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
new_session validated the created session id before it wrapped the tool context lease in UnpublishedAcpSession. An empty id returned early with the lease still unowned, so the tool context was never revoked and the half-created session was never discarded. Take ownership of the lease first so every early return goes through the guard's drop and releases the context. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BillingClient::new fell back to reqwest::Client::default() when the builder failed. The default client panics under the same conditions that make the builder fail, and when it did succeed it silently lost the 15 second request timeout. Make the constructor fallible and surface the error to the backend constructor, which already returns Result. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
set_auth held the mutation lock across credential validation, which can take up to 30 seconds. A sign-out issued during that window waited behind the sign-in, so logout appeared to hang. Validate first and take the lock only to publish. To keep the guarantee that a sign-out during validation wins, clear_auth bumps an epoch and set_auth refuses to publish when the epoch moved while it was validating. The existing account-scope check still rejects a publish for a different signed-in account. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
is_remote_file_source only recognised UNC prefixes written as two
backslashes or two forward slashes. Windows also resolves a mixed
prefix such as /\server\share to a network share, so a read of that
path skipped the remote approval prompt.
Classify any path that opens with two separators from {/, \} as
remote.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read only mode auto-approved read and read_image for any local path, while the shell classifier refuses commands that read the same files. An agent could read ~/.ssh/id_rsa or a project .env without a prompt by using the read tool instead of the shell. Add a deny-list of likely-secret paths (SSH, AWS, GnuPG, gh, kube, docker, netrc, non-template .env files, key and credential files). Reads of those paths now fall through to the normal approval prompt. Web search sent the query off-machine without any approval in Read only mode. Drop the auto-approval so the user is asked; a prompt is the safe default until a real classifier exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The request_user_input tool awaited the question broker without watching the tool call's cancel token. Stopping a task while a question card was open left the tool waiting until the user answered a card the UI had already torn down. Race the ask against the cancel token. The broker's pending guard drops the entry when the ask future is dropped, so a cancelled question does not leak its sender. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the first selected line exceeded the read limit, the notice only suggested a shell command. Every other truncated read ends with a "Use offset=N to continue" hint, so a model that hit a long line at the start of a range had no cue to move past it and could loop on the same read. Check whether any data follows the long line and append the next offset when it does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
read_image built a reqwest client with the default redirect policy and fetched any http or https URL. The approval prompt showed the user one URL, but a 302 could send the approved request to 127.0.0.1 or the cloud metadata address, and a loopback URL was never rejected at all. Validate the host with the same public-host rules the web tools use, reject embedded credentials, and disable redirects so the bytes come from the host the user approved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SDK rotates the access and refresh pair during API calls and the runtime reports it through MapleApiAuthEventSink, but the app passed NoopAuthEventSink everywhere. auth.json kept the pair from sign-in, so once the refresh token had rotated the next launch restored stale credentials and the user had to sign in again. Carry the live snapshot in the event and give the app a sink that writes it to auth.json off the runtime thread. Only the revision is logged; the tokens never reach the log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
notify-send parses positional arguments that start with a dash as options. A run summary that begins with "- " or "--" made the notification fail or, worse, set an unrelated flag. Ending option parsing with "--" sends any text as the title and body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parse_version dropped everything after the patch number, so 1.2.0-beta.1 compared equal to 1.2.0 and a beta build never learned that the final release shipped. The version now keeps the pre-release suffix and sorts it below the bare triple, as semver does. Build metadata is still ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MAPLE_API_URL, MAPLE_BILLING_API_URL, MAPLE_UPDATE_REPO, MAPLE_DISABLE_UPDATE_CHECK, and MAPLE_CLIENT_ID each had their own copy of "read, trim, drop when empty", and the API URL did not trim or drop at all, so a blank value became an empty backend URL. One module now owns that rule: env_string for values and env_flag for 1/true/yes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A malformed override fell back to the production client id without a trace, so a test build that meant to talk to a staging project silently talked to production. The fallback stays, with a warning that names the bad value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The redirect URL's query values were used raw. Google authorization codes contain "/" and are sent as "%2F", so pasting a Google callback failed the token exchange. Both values are now form-decoded, including "+" as a space, with the percent-encoding crate that is already in the dependency tree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…elds gpui at the pin ships an AccessKit tree and Maple exposed nothing to it. The transcript is a log of articles labelled by sender with their position in the conversation; thinking headers and tool cards are disclosure triangles; the waiting indicator is a status. The sidebar's task list is a list box of options with title, project, selection, and the Application-Vim active descendant; its headers are labelled buttons and its popups menu items. Settings navigation is a tab list. Every text input announces itself by its placeholder. Nothing is computed unless a screen reader is attached.
A plain element drives any surface through a five-method trait, implemented for the list state and the scroll handle. A press on the thumb captures the pointer, so the drag survives leaving the track and ends on the mouse up wherever it lands; a press on the track pages by a viewport; the wheel passes through; while the content fits, nothing is painted or blocked. The transcript's hand-drawn bar, its three container-level mouse handlers, and the screen's drag field are gone. The sidebar task list and the settings pane, which had no scrollbar, get the same one.
The bar appears when the user scrolls, while the pointer is on its strip, and during a drag, then fades after a second of quiet; instant under reduce motion. A list following its tail moves its top and range together and stays at the end, which counts as growth rather than a scroll, so a streaming transcript never flashes the bar. The fade drives itself frame by frame only while fading, and one timer task arms the hide. A hidden strip blocks nothing but still notices the pointer, so hovering it brings the bar back.
The document becomes Send (Rc to Arc) so the parse runs on the background executor. On a miss the cache spawns the parse and keeps the previous document on screen; a chunk that lands mid-parse is served the same document, and the re-render that installs the result starts the next parse, so the rate is bounded by parse latency rather than the 50 ms timer, its stale flag, and its deferred repaint, all of which are gone. A short cold source still parses inline so ordinary rows never show raw text. After a timeline load the newest long messages parse in the background at low priority so the first scroll finds them ready.
…ables Inline code renders in the monospace face inside the same shaped line as its prose through font-family overrides, so no paragraph splits. The single-line clamp sites gain an ellipsis (gpui's clamp only clips): the project chooser's path keeps its file name by truncating at the start, and thinking and tool titles truncate in the middle. Code block bodies scroll sideways instead of wrapping, restricted to their axis so the transcript still takes the vertical wheel. Table column weights are computed once at parse time rather than on every frame, and tables and cells carry roles and indices for the accessibility tree.
Monospace inline code, real ellipses, sideways code blocks, cheaper tables
The task sidebar was a set of methods on the chat screen, so any notify on the screen rebuilt its element tree, and the running-task spinner notified the screen fifteen times a second. It is now an entity of its own that owns the list, sections, search, menus, renames, the pinned and settled sets, and its Application-Vim row targets. The screen pushes what it shows (sessions, selection, running and unread sets, recent roots) and embeds it with `cached`, so the subtree is reused until the sidebar itself changes. The sidebar talks back through events, never by updating the screen from inside its own update, so nothing re-enters; clicks that need the window call the screen from plain closures. The trust prompt, the remove-project confirmation, archiving, and leaving a task stay on the screen, in a dialogs module, since they touch the canonical session list and project context.
`Window::refresh` discards every cached view in the window, so the copy button's two refreshes had to go before any panel was cached. The render context now carries the id of the view being rendered down to the button, which asks for one repaint of that view instead. CLAUDE.md records how panel entities work: the screen pushes state in, panels answer with events or window-carrying closures, listeners never call the screen, and a cached view needs a definite size.
The screen fixture loads the settings file of the machine running the tests, and tests that settle or pin persist their fixture ids into that file, so a later fixture could inherit a settled "s1" and shift every sidebar row. The fixture now clears the persisted sets after loading.
Sidebar becomes its own cached entity; copy buttons stop refreshing the window
The screen pushes its session list on every sync (run start, session update, selection change), and the sidebar rebuilt its sections and repainted each time. It now compares the incoming list field by field and returns early when nothing changed, like its other setters do. The one test that renamed a project by reaching into the map now goes through a helper that rebuilds, as a real rename does.
…ssions Sidebar ignores an unchanged session list
Since the gpui pin, Linux had no windowing backend compiled (gpui_platform exposes wayland and x11 as features the app must enable) and could not build at all because ashpd was asked for both its async backends: gpui's Linux port selects async-io through oo7 while the Cua driver selected tokio. The app enables wayland and x11, which forward to a Linux-only crate and change nothing elsewhere, and the driver pin moves to a rev that selects async-io.
Build on Linux again: windowing features on, one ashpd backend
Show who is signed in (email or anonymous account id, sign-in method, member since), offer to resend the verification email, and call the server logout route during sign-out (the backend does not revoke the refresh token yet). The SDK user lookup that validation already performed is now surfaced through MapleApiSession::account instead of being discarded.
Email and anonymous accounts get a change-password form in the Account section. The backend rotates the token pair on success; the SDK stores it and the session publishes it to the persisted auth record, so the next launch restores the new credentials.
Typing DELETE asks the server to email a confirmation code; the code plus a client-held secret (SHA-256 hash sent up front, plain text on confirm, as the web app does) deletes the account. The agent runtime stops before the irreversible call and the local credentials are dropped after it, without a server logout the deleted account could not honor. The app returns to the login form on AccountDeleted.
The billing client now decodes the whole subscription status and adds the public product list, the Stripe customer portal, checkout, and the API credit balance. The Billing section shows the plan, its renewal or expiry date, credits, and the plans on sale. Free accounts start a Stripe checkout in the browser; Stripe subscribers change plans in the portal; Bitcoin, pass, and team plans point at the web pricing page. Return URLs are the web app's, as in the Tauri build, until the desktop app has its own URL scheme.
List, create, and delete the account's API keys for the OpenAI-compatible API and the local proxy. A new key is shown once with a copy button, a delete asks for confirmation on the row, and the section is gated on a Pro, Max, or Team plan with a link to the Billing section otherwise, as the web app does.
Prompts for the email and password (never a flag, so the password stays out of shell history) and saves the same session the desktop app writes, so `maple-gpui acp` can run on a machine that never opened the window. OAuth stays desktop-only for now.
Add maple-gpui login for terminal sign-in
Contributor
|
Maple development preview: https://038da9fa.maple-ca8.pages.dev Commit: Uses development API, billing, flags and PCR configuration. Cloudflare Access applies. |
This branch was successfully deployed
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.
Bring the GPUI desktop-v2 prototype into
apps/maple-agentso Agent work can use Maple's shared SDK and proxy without coordinating a forked SDK release. Research remains the shipped React/Tauri application with unchanged source, identity, update URLs and production deployment configuration.The first commit preserves all 338 reachable upstream commits from
benthecarman/maple-gpuimaster6bedcf938b41629fca375ec96caaf5b8ddccaf67. Its imported subtree exactly equals source treefeaa3bc67584d881aa5c85d742302afc52a620b4; subsequent commits adapt the integration. Use a normal merge, not squash/rebase, to retain that ancestry. Provenance and the separate disposition of open upstream work are indocs/maple-agent-import.md.opensecretSDK andmaple-proxy, remove the fork patch, and adapt catalog vision metadata with fallback tests. Package names and registry publishing are unchanged.maple-agent-vX.Y.Ztags. Ignore Research tags and unsafe release URLs. The app still only displays a link. Research release jobs and downstream publishers skip Agent releases; no Agent publisher is activated. Future Agent releases must usemake_latest: false.bin/maple-agentand distinct state alongside legacy GPUI checkouts.Validation:
just ci: formatting, four Clippy configurations, workspace build/tests and headless tests passed: 755 passed, three existing ignored. Includes update discovery and catalog-adapter regressions.--versionpassed. The exact native bundle rendered an empty login screen; process open files proved the task's isolated state root. The test app was stopped. Authenticated chat, audio and embedded CUA behavior were not exercised.nix flake check --no-update-lock-filepassed, including Agent workflow/dependency/state-isolation checks, selectors, actionlint, Pages and release gates. Component all-system Nix evaluation passed; Linux pure packaging is not claimed built on this macOS host.The native link retains an upstream duplicate Swift type-symbol warning; source and executable inspection found identical bridge definitions and one linked copy per symbol, and packaged startup passed. It was not suppressed.
SDK renaming/official publishing, upstream feature-branch replay, official Agent distribution, backend import and PCR migration are separate follow-ups. No release or registry publication is created here.