| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
shell: Add `with_follow_mode` to make a list follow its tail (#3548) Stacked on #3547; only the last commit is new here. ## Description A transcript that is still being written is read at its end: it should open there, stay there as messages are added and as the last one grows, stop following when the reader scrolls up, and follow again when they scroll back down. GPUI's `ListState` does this in `FollowMode::Tail`, but a script's `list` had no way to turn it on, so a chat view had to scroll to the bottom itself after every change. `list(...).with_follow_mode("tail")` sets it on the list's retained state, and `"normal"`, or leaving the call out, sets it back. The mode is set only when it changes, because setting `Tail` scrolls to the end. Stopping and resuming as the reader scrolls is GPUI's behavior. A list that is following is not moved back to the remembered item when its count changes (the anchoring added in #3547): scrolling it to an item would stop it following. A list that is not following, including one whose reader scrolled up, keeps the anchoring. Any other mode throws a `TypeError` at the call site. `uniform_list` and the virtual lists have no follow mode; they log that the call is ignored, and for the virtual lists the log points to `scroll_to_bottom` on their scroll handle. ## Public API ### Script (`gpui-kit.d.ts`, `Element`) ```ts with_follow_mode<Self extends Element>(this: Self, mode: "normal" | "tail"): Self; ``` Honored on `list(...)`. Any other value throws `with_follow_mode(mode) expects "normal" or "tail"`. ## How to Test ```sh cargo test -p gpui-shell --lib -- a_list_following_its_tail a_follow_mode_other ``` - `a_list_following_its_tail_stays_at_the_end_until_the_reader_scrolls_up` draws 100 rows of 20px in a 200px list that follows its tail. It checks that the list opens at the end (`m99` at the bottom), that an appended row is shown at the bottom, and that growing the last row to 60px keeps its end in view. Then it scrolls up 60px and appends: the row being read stays where it was. Then it scrolls back past the end and appends: the new row is at the bottom again. - `a_follow_mode_other_than_normal_or_tail_is_refused` renders `with_follow_mode("bottom")` and checks the error. The first test fails when the `set_follow_mode` call is removed (the list opens at the top, `m9` at the bottom), and when a following list is anchored like any other (the appended row is not shown, `m99` instead of `m1100`). The second fails when the mode check is removed. AI-assisted: written with Claude Code and reviewed by me before submission. ## Checklist - [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and followed the guidelines. - [x] Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate. - [ ] Passed `cargo run` for story tests related to the changes. (No story changes.) - [x] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific). (Not platform-specific.) --------- Co-authored-by: Jason Lee <huacnlee@gmail.com> | 5 天前 | |
shell: Fix more runtime gaps found by the benchmark apps (#3510) ## Summary A batch of gpui-shell runtime fixes and additions found while running the benchmark apps (omanta, disktree). Each fix is its own commit; later shell changes in this series land here too. ### Virtual list rows and error messages A row built in a virtual list's item renderer could not register handlers: `row.on_click(...)` threw `` `on_click` cannot be registered from a virtual list's item renderer… ``. That is the way anyone writes a clickable row (the omanta benchmark app hit it), and a row with several clickable parts had no way to work at all. **Row handlers** (`engine/quickjs/mod.rs`, `spec.rs`, `materialize/components/virtual_list.rs`) - The refusal existed because callbacks lived as long as the render's snapshot while rows are rebuilt every frame, so per-row handlers would pile up. This is the batch-scoped generation the module comment recorded as the way to lift it. - Each batch of rows now records into a callback generation of its own, owned by a `RenderSnapshot` of the batch. Every row drawn from it holds the snapshot in a listener; GPUI keeps a frame's listeners until the next frame replaces them, so the generation is retired exactly when nothing on screen can deliver an event to it. This is the same lease the interactive inline renderers already use. - A batch that registers nothing takes the old path and adds no wrapper. - `on_item_click` / `on_item_secondary_click` are unchanged and remain the cheaper choice for a whole-row click. - Layout code without a lifetime of its own (dock chrome, or a batch reached while another generation is recording) still refuses, with a message that no longer assumes it is a list. **Whole error messages** (`engine/quickjs/js_error.rs`) - rquickjs's `Exception::throw_type` and its siblings copy the message into a 256-byte buffer, so longer runtime messages lost their second half, usually the part saying what to do instead. The same refusal above was cut at `on_item_secondary_click((key, event`. - All 354 call sites now throw through the realm's own `TypeError` / `RangeError` / `Error` constructors, which take any length and capture the stack the same way. If the constructor cannot be reached, they fall back to rquickjs. - Displayed stacks drop the runtime prelude's `eval_script` frames. The TypeScript declarations (`typings.rs`) now describe row handlers as supported. ### Unbounded JavaScript heap The shell capped each QuickJS runtime's heap at 256 MiB. A GPUI application written in Rust never budgets its memory, and the cap only turned legitimate data sets into broken applications. The disktree benchmark scanning a home directory (~218k files) hit it: - QuickJS throws `null` when it cannot allocate even the `out of memory` error (`quickjs.c`: "out of memory: throw JS_NULL to avoid recursing"). The app's `catch (e)` received `null`, and the runtime reported "A script threw null". - A failed reload keeps the old application (and its data) alive, so every later reload ran out while loading modules. Only a restart recovered. **Heap** (`engine/quickjs/sandbox.rs`, `mod.rs`) - No heap ceiling. Memory is the operating system's to manage, as it is for a Rust GPUI app. An embedding host can still bound its own runtime. - When a heap does run out, the failure says so. That covers `InternalError: out of memory` with no ceiling (the system could not allocate), a host ceiling being hit, and a `null` thrown while a ceiling is ≥ 90 % used. The message names the limit when there is one and suggests keeping less data in script or using a host module. - `docs/gpui-shell.md` §19.3: the memory row, plus the runaway-execution row, which still listed the 50 ms / 500 ms interrupt budgets replaced in #3509. ### `cx.open_path` A script could not open a file with the user's default application. Running `xdg-open` through `process.run` fails on Linux, because commands start with a cleared environment (`process.rs` `env_clear()`). Without `HOME`, `XDG_*`, `WAYLAND_DISPLAY` and the D-Bus address, `xdg-open` cannot identify the desktop, falls through to text browsers, and fails. The omanta benchmark app's "open with default application" never worked on Linux because of this. - `cx.open_path(path)` hands the path to `App::open_with_system`. GPUI runs the platform opener with the graphical session's environment on Linux, Launch Services on macOS, and the shell on Windows. It is on both `Context` and `AsyncContext`, like `open_url`. - It carries the authority reading the path would. The path must be inside a `capabilities.fs.read` grant and must exist. The check goes through the granted directory handle (`Grant::system_path`), so a symlink leading out of the grant is refused the way a read would be. A denial names `capabilities.fs.read`. - The session environment is not handed to arbitrary commands: `process.run` is unchanged. - Documented in `website/shell/api.md` (en and zh-CN) and in `typings.rs`. ### `process.spawn` `process.run` waits for the command to end, stops it at 30 seconds (`Limits::default`), and has no stdin. AI command lines need more: `claude -p --output-format stream-json`, `codex exec --json`, and long-lived `codex app-server` / `claude --input-format stream-json`. They have to be read line by line while they run, may run for minutes, take input over stdin, and must be cancellable. **Rust** (`process.rs`, engine-neutral) - `spawn_streaming` starts the child with the same rules as `run_bounded`: host-PATH resolution, `env_clear()`, its own process group (a Job Object on Windows). - A reader thread per stream decodes UTF-8 incrementally, so a character split across reads comes out whole and invalid bytes become U+FFFD. In line mode it joins lines across reads, strips `\n`/`\r\n`, and flushes a last unterminated line at EOF. A line over 8 MiB stops the process. - A waiting thread enforces `timeoutMs` when given. After the child exits it kills the rest of the group, so a descendant holding the pipes cannot keep the exit pending or outlive the command. It joins both readers before sending `Exit`, which is why `exit` always comes after the last output. - stdin is fed from its own thread, so a slow reader never blocks the UI. **Scheduler** (`scheduler::stream`) - A new primitive: events produced off the main thread are delivered to one script callback as `dispatch(name, payload, cx)` on the UI thread, with the async `cx` timer handlers get. Events that arrived together go out in one host call. A handler that throws is reported without costing the events after it. - The child is registered as a task owned by the view that spawned it, with "kill the process group" as its cancellation. Releasing the view, reloading the application (`release_application_generation` → `cancel_application_tasks`) or closing the window therefore kills it, through the same paths that already cancel timers and spawned work. - The channel is polled every 10 ms, like actor completions, rather than awaited. A wake from an OS thread is not a foreground schedule GPUI's deterministic test executor accepts. **JavaScript** - `process.spawn` is a thin prelude over a native `__process_spawn`. Handlers registered with `on()` live in script, and Rust holds only the one dispatch function, in the task registry that already releases persisted functions before the VM goes away. - `cwd` uses the same check as `fs`: the read grant, through the granted directory handle. - `run(command, args, { cwd, timeoutMs })` keeps 30 seconds when no timeout is given. - Documented in `typings.rs` (`declare module "process"`), the app template `host/agents.md` ("State, events and asynchrony", with a JSONL example), and `website/shell/capabilities.md` (en and zh-CN). ### Docs: start as a standalone app, move to a Rust host later `website/shell/index.md` and `getting-started.md` (en and zh-CN) described running scripts on the `gpui-shell` command as a development convenience behind plugins. They now recommend starting an application as a gpui-shell standalone app and show how the same scripts move under a Rust program when it needs more, without rewriting any JavaScript: - **Keep the command, add Rust**: register a `HostModule`, then call `gpui_shell::host::main_with_brand_and_components(HostBrand::new(..), components)`. The manifest, grants, data directory (keyed by the manifest `id`), and `dev`/`check`/`types`/`agents` all stay the same. `bundle` is refused under another name, because its launchers install `gpui-shell` releases. - **Build the runtime yourself**: the existing host example, moved to the end of Getting Started. - Getting Started now puts the JavaScript sections first and the Rust host last. - The host example used `gpui_kit::shell`, `set_store_path`, and `Capabilities::store`, none of which exist. It now uses `gpui_shell::`, `set_bundle_id`, and `storage(true)`. Other pages still use `gpui_kit::shell::` and are not changed here. ## Public API **gpui-shell (JavaScript)** - `cx.open_path(path: string): void` opens a file or directory inside a `capabilities.fs.read` grant with the system's default application (`App::open_with_system`). - `process.spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess` starts a command and streams its output. - `interface SpawnOptions { cwd?: string; stdin?: "pipe" | "null"; timeoutMs?: number; lines?: boolean }`. `cwd` must be inside `capabilities.fs.read`. `stdin` defaults to `"null"`. There is no timeout by default. `lines` delivers line by line. - `interface ChildProcess { readonly pid: number; on("stdout" | "stderr", (text, cx) => void): ChildProcess; on("exit", (result: ExitResult, cx) => void): ChildProcess; write(text: string): void; end(): void; kill(): void }` is the running child. `kill()` stops its process group, and `exit` fires once. - `interface ExitResult { code: number | null; signal: string | null; error?: string }`. `error` is set when the runtime stopped the process. - `interface RunOptions { cwd?: string; timeoutMs?: number }` and `process.run(command, args?, options?: RunOptions)` add an optional third argument. Without it, `run` behaves as before (30 s). ## Validation - `cargo test -p gpui-shell` (all pass), `cargo clippy -p gpui-shell --all-targets -- --deny warnings`, `cargo fmt --check` - `a_row_registers_its_own_handler_without_accumulating` replaces the refusal test: a click reaches the handler the third row registered, `on_item_click` still fires, and twenty scroll steps leave the live callback count within two of where it started. The existing "handlers do not accumulate while scrolled" test still passes. - `a_long_message_survives_whole` checks a 400+ character `TypeError` arrives intact and is still `instanceof TypeError`. - Not run against omanta in a window. - `the_shell_sets_no_heap_ceiling` checks that a runtime the shell creates reports no limit. - `running_out_of_memory_is_explained_with_the_limit` checks that a 16 MiB runtime filled by a loop reports the exhaustion and the limit, that a `null` thrown on a nearly full heap does too, and that after GC a `null` is reported as a plain `null` again. - Not re-run against disktree on a large directory. - `open_path_is_confined_to_the_read_grant`: a file and the granted directory itself pass both checks and reach the host-call gate, a missing file reports that it cannot be opened, and a path outside the grant is refused with `capabilities.fs.read`. Not exercised in a running desktop session. - `process.spawn`, Rust level (`process::tests`): - `printf` writes split mid-line are joined, including a trailing line with no newline; - 中 and 😀 split across writes decode whole, and an invalid byte becomes U+FFFD; - `cat` round-trips two written lines and exits 0, and a write after `end()` is refused; - `kill()` called twice ends `sh` and its backgrounded `sleep` with one `exit` (SIGKILL, no error), and the descendant's pid is verified gone; - `timeoutMs: 100` stops `sleep 5` with a "timed out after 100 ms" error; - no timeout means no deadline; - one byte past 8 MiB without a newline is refused; - `run` honours `cwd` and a longer timeout. - `process.spawn`, JavaScript level (`tests::process`): - lines reach `on()` handlers with a `cx` and the exit fires once with `code` 3 and `signal: null`; - an ungranted executable, a `cwd` outside the read grant, a bad `stdin` and `write()` without a pipe all throw; - releasing the application generation the way `dev --watch` reloads kills the child and its descendant. - The 29 process tests pass on three consecutive runs. - The generated `gpui-kit.d.ts` type-checks a script using `spawn`, `on`, `write`/`end`/`kill`, `pid` and `run` options with `tsc --strict`. The file has the same 5 pre-existing errors before and after, none in the `process` module. - Not tested: window close (it uses the same view-release cancellation, covered elsewhere), Windows and macOS locally (CI covers them), and a real `claude` / `codex` session. - Docs: both Rust snippets were compiled as temporary examples in `crates/shell`. The wrapper binary was run end to end: `new`, `check` passes, `types` emits `declare module "native"`, `dev` logs `Hello, world` from a script importing the Rust function, `bundle` is refused, and `--version` prints the brand. `bun test tests/doc-sources.test.ts tests/markdown.test.ts` passes. The full site build was not run, because it needs the sibling `gpui-kit-showcases` checkout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 9 天前 | |
website: Keep versioned docs links inside their version (#3243) Documentation pages read under `/versions/main` or `/versions/<tag>` linked back to the default version through site-root links (`/docs/entity`, `/component/...`), which often 404 there because the default is the latest release. ## Links stay in their version - 124 site-root links in `docs`, `component`, `base`, `shell` (en and zh-CN) now link the `.md` file relatively. zh-CN pages that linked English Base pages now link the Chinese ones. - `remarkDocLinks` prefixes any remaining site-root docs link with the build's base, so released snapshots (v0.6.6 still has such links) stay inside `/versions/<tag>` too. - Redirect destinations (`/docs/dock` → `/component/dock`, …) and the zh-CN → en language switch kept dropping the version base; both are fixed. - The 404 page, which GitHub Pages serves for every missing path, looks for the same page in the other versions and offers it ("This page is not in the v0.6.6 (latest) documentation, but it exists in main."). It also now loads the site stylesheet; it was unstyled before. ## GPUI snapshot version in one place Pages write `{{gpui_pre_version}}` in prose, code and docs.rs links. It resolves from the `gpui` entry of the workspace `Cargo.toml`; `script/build-website-versions` passes each revision's own value. The `.md` endpoints and `llms-full.txt` expand it as well. ## Maturity labels `maturity: [preview | experimental | showcase-only | platform-dependent | stable]` in frontmatter renders mono labels under the title, linking to a new Maturity section on the docs home. Marked: Mobile and WebView (Experimental, Platform-dependent), WebAssembly (Showcase only: it currently serves the component showcases, not shipped applications), all GPUI Shell pages (Preview), Native Extensions, SystemNotification and TitleBar (Platform-dependent). Unmarked pages are Stable. ## Checks - `bun run test:docs` (sources, no build): no site-root docs links; relative links resolve, in the same locale when possible; no hand-written pinned `gpui-pre` version or versioned docs.rs gpui-pre URL; known variables only; en/zh-CN maturity match. - `bun run test:links`: every `<a>` and redirect in a build reaches a built page; `test:versioned-examples` repeats it at `/versions/test/`, where leaving the version fails. - Test Docs now also runs on `Cargo.toml` changes. Verified locally with a production-shaped build (v0.6.6 at `/`, this branch at `/versions/main`): all website tests pass, the v0.6.6 snapshot passes the link check at `/versions/v0.6.6/`, and the 404 fallback works for `/docs/entity` and `/zh-CN/docs/entity`. Publishing a release that contains the new GPUI guides is what makes `/docs/` show them; this PR does not change which version is the default. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> | 28 天前 | |
shell: Ship gpui-shell as an installable runtime with run, new and packaging (#3464) Makes `gpui-shell` a runtime you install and ship applications with, rather than only a development host run through `cargo run`. Releases now publish a prebuilt `gpui-shell` binary, installed applications live under `~/.gpui-shell/apps`, and an application can be packaged as the binary plus its script directory. ## What changes **One package, one command.** `gpui-component-shell` is merged into `gpui-shell` as the default `components` feature (`gpui_shell::components`). Cargo forbids the cycle a feature on a separate crate would need, so the catalog moved in. Without the feature, the runtime still does not depend on `gpui-component`, and CI checks that build. **Command line.** It runs one application per process and has no interface of its own: ```text gpui-shell run <name> | -d <dir> [--watch] [--dev] # ~/.gpui-shell/apps/<name>, or a directory gpui-shell # the application packaged beside the executable gpui-shell new <dir> # scaffold a blank application gpui-shell # with nothing packaged beside it: the help gpui-shell list # installed applications and their requested capabilities gpui-shell check | types | agents ... ``` - An installed or packaged application keeps its storage under its manifest `id`; a directory run with `-d` stays keyed by its path. - With no arguments the binary runs `Contents/Resources/app` inside a macOS bundle, or `app/` beside itself. This is what lets a `.app` or a Linux package be the binary plus a directory of scripts. - `GPUI_SHELL_HOME` replaces `~/.gpui-shell`, both for applications and for the Git dependency cache. - `agents` prints, or writes as `AGENTS.md`, development instructions for coding agents. They are generated from the running binary, including its module and component list. - The window is brought forward on launch, and Cmd-Q / Alt-F4 quits. - The manifest gains an optional `window` field: `{ "width": 360, "height": 520 }` sets the opening size. **Release and install.** `release.yml` builds `gpui-shell` for aarch64/x86_64 macOS, x86_64 Linux and x86_64 Windows. Asset names carry no version, e.g. `gpui-shell-aarch64-apple-darwin.tar.gz` plus `.sha256`, so `releases/latest/download/<asset>` always resolves. `curl -fsSL https://gpui-kit.com/shell/install.sh | sh` installs it to `~/.local/bin`; the script source is `website/public/shell/install.sh`. **Catalog.** - New `TitleBar` binding. - Catalog windows use `TitleBar::window_options()`, so Sheets and notifications line up with the title bar. - `js_story`, `js_todolist` and `js_dock` now render a `TitleBar`. **Fixes.** - `localStorage.getItem` / `key` return `null` rather than `undefined` for a missing entry. This is what made `js_todolist` warn "storage unavailable". - OtpInput repaints on every keystroke. Its state was a plain entity, so its notify dirtied no view, and the cached content replayed until the pointer moved. - Importing `gpui-component` without a catalog now explains how to get one, instead of listing it as a built-in module. - The load-failure page's copy action is an icon button (copy, then check), so it can no longer overflow the panel. **Docs.** - Getting Started, en + zh-CN: installing, `run` / `new` / `list` / `agents`, and installed and packaged applications. - New Packaging page, en + zh-CN: macOS `.app` with `Info.plist`, `lipo`, signing and notarization; a Linux archive and `.desktop` entry; a Windows folder. ## Public API ### gpui-shell - Feature `components` (default): the `gpui-component` catalog. Optional dependency on `gpui-component`. - `pub mod components` (with `components`): - `pub fn init(cx: &mut App)`: initializes `gpui-component` and the runtime (was `gpui_component_shell::init`). - `pub fn components() -> Result<FrozenComponentRegistry, RegistryError>`: the frozen catalog (was `gpui_component_shell::components`). - `pub fn new_isolated_runtime() -> anyhow::Result<Rc<ShellRuntime>>`: an isolated runtime with the catalog (was `gpui_component_shell::new_isolated_runtime`). - `pub struct plugin::WindowSize` with `width(&self) -> f32` and `height(&self) -> f32`: a manifest's opening window size. - `PluginManifest::window(&self) -> Option<WindowSize>`: the manifest's `window`, if any. - `ManifestProblem::Window(String)`: an invalid `window` field. - `host::InvocationKind::{List, Agents, New}`: classifications for the new commands. - Manifest field `window: { width: number, height: number }`: logical pixels, positive, no other keys. - JavaScript: `window.start_window_move(): void` (GPUI `Window::start_window_move`), so a script-drawn title bar can drag the window. - JavaScript, `"gpui-component"`: `TitleBar` (`new TitleBar()`, with styles, `child`/`children` and `on_close_window(callback: (cx) => void)`). It is declared in the generated `gpui-kit.d.ts`. ## Validation - `cargo test -p gpui-shell --no-fail-fast`: 34 test binaries, 990 tests, no failures. - `cargo clippy -p gpui-shell --all-targets -- -D warnings`, `cargo fmt --check -p gpui-shell`, `cargo machete`, `typos`: clean. - `cargo check -p gpui-shell --no-default-features --features quickjs`: the runtime builds without the catalog. - `gpui-shell check -d` passes for `examples/js_todolist`, `examples/js_dock` and `examples/js_story`. - `npm --prefix crates/shell/tests/types test` and `node examples/js_story/fixtures/verify-coverage.mjs` pass. - Manually: `new` → `check` on a scaffolded application; a hand-built `Demo.app/Contents/{MacOS,Resources/app}` starts its bundled application with no arguments; OtpInput repainting on every keystroke was confirmed by eye in the running gallery. Not verified: - The release job and the install script have not run against a real release. - Bringing the window forward could not be confirmed from a background shell; macOS will not let it take focus from another app. It needs a check from a terminal launch. - Cmd-Q / Alt-F4 was not pressed in testing. - The manifest `window` size was unit-tested only, not opened end to end. - The TitleBar in `js_story`, `js_todolist` and `js_dock`, and whether a Sheet now opens flush under it, has not been looked at in a running window. ## AI-generated This change was written with Claude Code (one main session, two sub-agents for TitleBar and OtpInput, and a separate session for the manifest `window` field), reviewed and directed by the author. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 11 天前 | |
shell: Fix the runtime gaps found by the gpui-shell benchmark apps (#3505) ## Summary [gpui-shell-benchs](https://github.com/huacnlee/gpui-shell-benchs) builds real applications on gpui-shell 0.7.3 with AI coding agents, and its README records the runtime problems those agents hit and worked around. This PR fixes all twelve. | # | Problem | Fix | | - | ------- | --- | | 1 | Manifest `fs` roots outside the app were ignored | `local_capabilities` replaced the manifest's roots with the app and data directories; it now adds them. New `${homeDir}` placeholder. `$HOME`, `${HOME}`, `$USERPROFILE`, `%USERPROFILE%` and `~` are refused with the `${homeDir}` spelling to use, instead of silently becoming a relative directory | | 2 | No home directory, environment, data directory or `import.meta.url` | `os.homedir()`, `process.dataDir`, `import.meta.url`, and `process.env` (see below) | | 3 | No `TextEncoder` / `TextDecoder` | Both added as globals (UTF-8, `fatal`, `ignoreBOM`, `stream`) | | 4 | `window.open_dialog` / `window.push_toast` threw "needs a ShellRoot" | The CLI window's first view is a component `Root` with the `ShellRoot` inside it; `ShellRoot` is now found there too. This also fixes tooltips and the reload-failure toast in CLI windows. Calls from `init()` are still refused (the window has no root yet), now with an error that says so | | 5 | `gpui-component` needed `new Type(...)` while the guide said `Type.new(...)` | Every `gpui-component` export also accepts `Type.new(...)`; the old forms keep working | | 6 | Component `InputState` had no change or submit events | It gains `on(event, handler)` with the same events and payloads as `gpui-base`'s. A `gpui-base` state passed to a component `Input` is now refused instead of possibly binding an unrelated state | | 7 | No pinch gesture | `on_pinch(handler)`, forwarding GPUI's `PinchEvent` (all three desktop platforms emit it) | | 8 | `run -d` stored data under `<dir>-<hash>` | Data is keyed by the manifest `id` whenever there is a manifest, so development and installed copies share it | | 9 | `fs/promises` lacked `stat`, `rename`, `readlink`; `fetch` lacked `arrayBuffer()` | `stat`, `lstat`, `rename`, `readlink`; `Response.arrayBuffer()` and `bytes()` | | 10 | `fetch` sent no `User-Agent` (GitHub API returned 403) | Default `User-Agent: gpui-shell/<version>` on fetch and WebSocket; a script's header wins | | 11 | `flex_shrink()` failed with "expected a number, got nil" | GPUI's `flex_shrink` takes an `f32` too, so the argument stays required; the error now names the method and lists `flex_shrink_0()` / `flex_shrink_1()` | | 12 | `line_height(n)` is a multiplier while other lengths are pixels | Kept as is: a unitless `line_height(1.4)` is the same as CSS's `line-height: 1.4` and Rust's `relative(1.4)`. The docs now say so and show `"18px"` for a fixed height | ### `process.env` The environment can hold secrets handed to the host, so `process.env` shows only: - the defaults in [`crates/shell/src/default-env`](https://github.com/longbridge/gpui-kit/blob/shell/bench-fixes/crates/shell/src/default-env), one name per line: user, machine, locale, preferred tools (including `PATH`), desktop session, proxies, and standard directories. A test refuses a duplicate, a lowercase spelling, or a name that looks like a credential; - plus the names listed in the new `capabilities.process.env`. Names match regardless of case, so `HTTP_PROXY` also shows `http_proxy`, under the spelling the environment uses. Paths (`os.homedir()`, `process.dataDir`, `import.meta.url`) need no grant: reading or writing there is still the `fs` grant's decision. ### `fs/promises` access rules `stat`, `lstat` and `readlink` need read access; `rename` needs write access at both ends. `stat` does not follow a link out of a granted root. `readlink` returns the stored target text as Node does, without following it. ### Getting Started: install first `website/shell/getting-started.md` (and zh-CN) now opens with installing `gpui-shell`, ahead of embedding the runtime in Rust: a Rust host still needs the binary for `check`, `types` and `agents`. It adds building the latest `main` from Git: ```bash cargo install --git https://github.com/longbridge/gpui-kit gpui-shell --locked ``` The page's commands use the installed `gpui-shell` instead of `cargo run -p gpui-shell --`, and `crates/shell/README.md` mentions the Git install too. ## Public API ### gpui-shell (Rust) - `Capabilities::env_variables(self, names: impl IntoIterator<Item = String>) -> Self`: environment variables `process.env` shows beyond the defaults. - `Policy::with_data_dir(self, path: impl Into<PathBuf>) -> Self`: the data directory `process.dataDir` reports. - `Policy::data_dir(&self) -> Option<&Path>`: reads it. - `StateDescriptor::with_input_events::<M: InputModeKind>(self) -> Self`: gives a registered state `on(event, handler)` with the `gpui-base` input events. - `StateDescriptor::has_input_events(&self) -> bool`: whether it does. - `RetainedStateStore::attach(&mut self, handle: u64, subscription: gpui::Subscription) -> anyhow::Result<()>`: keeps a subscription alive as long as the state. ### Manifest (`gpui-shell.json`) - `${homeDir}` placeholder in `capabilities.fs.read` / `write`. - `capabilities.process.env: string[]`: extra environment variables to expose. ### JavaScript / TypeScript - `os.homedir(): string` - `process.env: Record<string, string | undefined>` (read-only view) - `process.dataDir: string | undefined` - `import.meta.url: string` - `class TextEncoder { encoding; encode(input?): Uint8Array; encodeInto(source, dest): { read, written } }` - `class TextDecoder { constructor(label?, { fatal?, ignoreBOM? }); decode(input?, { stream? }): string }` - `fs/promises`: `stat(path): Promise<Stats>`, `lstat(path): Promise<Stats>`, `rename(from, to): Promise<void>`, `readlink(path): Promise<string>`, and `interface Stats` - `Response.arrayBuffer(): Promise<ArrayBuffer>`, `Response.bytes(): Promise<Uint8Array>` - `Element.on_pinch(handler: (event: PinchEvent, cx) => void)` and `interface PinchEvent { delta; phase; position; local_position?; bounds?; modifiers }` - `gpui-component`: `Type.new(...)` for every component and state export - `gpui-component` `InputState.on(event: "change" | "submit" | "focus" | "blur", handler)` ## Breaking Changes `gpui-shell run -d <dir>` with a manifest now stores data under the manifest `id`, the same place the installed copy uses, instead of `<dir>-<hash>`. Data written by earlier development runs stays in the old directory. `process.env` was absent before; scripts that feature-detected it with `typeof process.env === "undefined"` now see an object. `Capabilities`' read and write roots for a local run now include the manifest's roots rather than only the app and data directories, so `gpui-shell list` reports a wider grant for manifests that ask for one. ## Validation - `cargo test -p gpui-shell`: 890 unit, 116 integration and 4 doc tests pass. - `cargo clippy -p gpui-shell --all-targets -- --deny warnings` and `cargo fmt --check` are clean. - `gpui-shell check` passes for `examples/js_todolist` and `examples/js_dock`. - `cargo install --git https://github.com/longbridge/gpui-kit gpui-shell --locked` installs from `main` (verified into a scratch `--root`). - Not done: running the benchmark apps against this branch and removing their workarounds; pinch on real hardware (covered by a simulated `PinchEvent` test); `typos` (not installed locally). The code and docs in this PR are AI-generated (Claude Code), reviewed and directed by a human. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 10 天前 | |
shell: Ship gpui-shell as an installable runtime with run, new and packaging (#3464) Makes `gpui-shell` a runtime you install and ship applications with, rather than only a development host run through `cargo run`. Releases now publish a prebuilt `gpui-shell` binary, installed applications live under `~/.gpui-shell/apps`, and an application can be packaged as the binary plus its script directory. ## What changes **One package, one command.** `gpui-component-shell` is merged into `gpui-shell` as the default `components` feature (`gpui_shell::components`). Cargo forbids the cycle a feature on a separate crate would need, so the catalog moved in. Without the feature, the runtime still does not depend on `gpui-component`, and CI checks that build. **Command line.** It runs one application per process and has no interface of its own: ```text gpui-shell run <name> | -d <dir> [--watch] [--dev] # ~/.gpui-shell/apps/<name>, or a directory gpui-shell # the application packaged beside the executable gpui-shell new <dir> # scaffold a blank application gpui-shell # with nothing packaged beside it: the help gpui-shell list # installed applications and their requested capabilities gpui-shell check | types | agents ... ``` - An installed or packaged application keeps its storage under its manifest `id`; a directory run with `-d` stays keyed by its path. - With no arguments the binary runs `Contents/Resources/app` inside a macOS bundle, or `app/` beside itself. This is what lets a `.app` or a Linux package be the binary plus a directory of scripts. - `GPUI_SHELL_HOME` replaces `~/.gpui-shell`, both for applications and for the Git dependency cache. - `agents` prints, or writes as `AGENTS.md`, development instructions for coding agents. They are generated from the running binary, including its module and component list. - The window is brought forward on launch, and Cmd-Q / Alt-F4 quits. - The manifest gains an optional `window` field: `{ "width": 360, "height": 520 }` sets the opening size. **Release and install.** `release.yml` builds `gpui-shell` for aarch64/x86_64 macOS, x86_64 Linux and x86_64 Windows. Asset names carry no version, e.g. `gpui-shell-aarch64-apple-darwin.tar.gz` plus `.sha256`, so `releases/latest/download/<asset>` always resolves. `curl -fsSL https://gpui-kit.com/shell/install.sh | sh` installs it to `~/.local/bin`; the script source is `website/public/shell/install.sh`. **Catalog.** - New `TitleBar` binding. - Catalog windows use `TitleBar::window_options()`, so Sheets and notifications line up with the title bar. - `js_story`, `js_todolist` and `js_dock` now render a `TitleBar`. **Fixes.** - `localStorage.getItem` / `key` return `null` rather than `undefined` for a missing entry. This is what made `js_todolist` warn "storage unavailable". - OtpInput repaints on every keystroke. Its state was a plain entity, so its notify dirtied no view, and the cached content replayed until the pointer moved. - Importing `gpui-component` without a catalog now explains how to get one, instead of listing it as a built-in module. - The load-failure page's copy action is an icon button (copy, then check), so it can no longer overflow the panel. **Docs.** - Getting Started, en + zh-CN: installing, `run` / `new` / `list` / `agents`, and installed and packaged applications. - New Packaging page, en + zh-CN: macOS `.app` with `Info.plist`, `lipo`, signing and notarization; a Linux archive and `.desktop` entry; a Windows folder. ## Public API ### gpui-shell - Feature `components` (default): the `gpui-component` catalog. Optional dependency on `gpui-component`. - `pub mod components` (with `components`): - `pub fn init(cx: &mut App)`: initializes `gpui-component` and the runtime (was `gpui_component_shell::init`). - `pub fn components() -> Result<FrozenComponentRegistry, RegistryError>`: the frozen catalog (was `gpui_component_shell::components`). - `pub fn new_isolated_runtime() -> anyhow::Result<Rc<ShellRuntime>>`: an isolated runtime with the catalog (was `gpui_component_shell::new_isolated_runtime`). - `pub struct plugin::WindowSize` with `width(&self) -> f32` and `height(&self) -> f32`: a manifest's opening window size. - `PluginManifest::window(&self) -> Option<WindowSize>`: the manifest's `window`, if any. - `ManifestProblem::Window(String)`: an invalid `window` field. - `host::InvocationKind::{List, Agents, New}`: classifications for the new commands. - Manifest field `window: { width: number, height: number }`: logical pixels, positive, no other keys. - JavaScript: `window.start_window_move(): void` (GPUI `Window::start_window_move`), so a script-drawn title bar can drag the window. - JavaScript, `"gpui-component"`: `TitleBar` (`new TitleBar()`, with styles, `child`/`children` and `on_close_window(callback: (cx) => void)`). It is declared in the generated `gpui-kit.d.ts`. ## Validation - `cargo test -p gpui-shell --no-fail-fast`: 34 test binaries, 990 tests, no failures. - `cargo clippy -p gpui-shell --all-targets -- -D warnings`, `cargo fmt --check -p gpui-shell`, `cargo machete`, `typos`: clean. - `cargo check -p gpui-shell --no-default-features --features quickjs`: the runtime builds without the catalog. - `gpui-shell check -d` passes for `examples/js_todolist`, `examples/js_dock` and `examples/js_story`. - `npm --prefix crates/shell/tests/types test` and `node examples/js_story/fixtures/verify-coverage.mjs` pass. - Manually: `new` → `check` on a scaffolded application; a hand-built `Demo.app/Contents/{MacOS,Resources/app}` starts its bundled application with no arguments; OtpInput repainting on every keystroke was confirmed by eye in the running gallery. Not verified: - The release job and the install script have not run against a real release. - Bringing the window forward could not be confirmed from a background shell; macOS will not let it take focus from another app. It needs a check from a terminal launch. - Cmd-Q / Alt-F4 was not pressed in testing. - The manifest `window` size was unit-tested only, not opened end to end. - The TitleBar in `js_story`, `js_todolist` and `js_dock`, and whether a Sheet now opens flush under it, has not been looked at in a running window. ## AI-generated This change was written with Claude Code (one main session, two sub-agents for TitleBar and OtpInput, and a separate session for the manifest `window` field), reviewed and directed by the author. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 11 天前 | |
shell: Ship gpui-shell as an installable runtime with run, new and packaging (#3464) Makes `gpui-shell` a runtime you install and ship applications with, rather than only a development host run through `cargo run`. Releases now publish a prebuilt `gpui-shell` binary, installed applications live under `~/.gpui-shell/apps`, and an application can be packaged as the binary plus its script directory. ## What changes **One package, one command.** `gpui-component-shell` is merged into `gpui-shell` as the default `components` feature (`gpui_shell::components`). Cargo forbids the cycle a feature on a separate crate would need, so the catalog moved in. Without the feature, the runtime still does not depend on `gpui-component`, and CI checks that build. **Command line.** It runs one application per process and has no interface of its own: ```text gpui-shell run <name> | -d <dir> [--watch] [--dev] # ~/.gpui-shell/apps/<name>, or a directory gpui-shell # the application packaged beside the executable gpui-shell new <dir> # scaffold a blank application gpui-shell # with nothing packaged beside it: the help gpui-shell list # installed applications and their requested capabilities gpui-shell check | types | agents ... ``` - An installed or packaged application keeps its storage under its manifest `id`; a directory run with `-d` stays keyed by its path. - With no arguments the binary runs `Contents/Resources/app` inside a macOS bundle, or `app/` beside itself. This is what lets a `.app` or a Linux package be the binary plus a directory of scripts. - `GPUI_SHELL_HOME` replaces `~/.gpui-shell`, both for applications and for the Git dependency cache. - `agents` prints, or writes as `AGENTS.md`, development instructions for coding agents. They are generated from the running binary, including its module and component list. - The window is brought forward on launch, and Cmd-Q / Alt-F4 quits. - The manifest gains an optional `window` field: `{ "width": 360, "height": 520 }` sets the opening size. **Release and install.** `release.yml` builds `gpui-shell` for aarch64/x86_64 macOS, x86_64 Linux and x86_64 Windows. Asset names carry no version, e.g. `gpui-shell-aarch64-apple-darwin.tar.gz` plus `.sha256`, so `releases/latest/download/<asset>` always resolves. `curl -fsSL https://gpui-kit.com/shell/install.sh | sh` installs it to `~/.local/bin`; the script source is `website/public/shell/install.sh`. **Catalog.** - New `TitleBar` binding. - Catalog windows use `TitleBar::window_options()`, so Sheets and notifications line up with the title bar. - `js_story`, `js_todolist` and `js_dock` now render a `TitleBar`. **Fixes.** - `localStorage.getItem` / `key` return `null` rather than `undefined` for a missing entry. This is what made `js_todolist` warn "storage unavailable". - OtpInput repaints on every keystroke. Its state was a plain entity, so its notify dirtied no view, and the cached content replayed until the pointer moved. - Importing `gpui-component` without a catalog now explains how to get one, instead of listing it as a built-in module. - The load-failure page's copy action is an icon button (copy, then check), so it can no longer overflow the panel. **Docs.** - Getting Started, en + zh-CN: installing, `run` / `new` / `list` / `agents`, and installed and packaged applications. - New Packaging page, en + zh-CN: macOS `.app` with `Info.plist`, `lipo`, signing and notarization; a Linux archive and `.desktop` entry; a Windows folder. ## Public API ### gpui-shell - Feature `components` (default): the `gpui-component` catalog. Optional dependency on `gpui-component`. - `pub mod components` (with `components`): - `pub fn init(cx: &mut App)`: initializes `gpui-component` and the runtime (was `gpui_component_shell::init`). - `pub fn components() -> Result<FrozenComponentRegistry, RegistryError>`: the frozen catalog (was `gpui_component_shell::components`). - `pub fn new_isolated_runtime() -> anyhow::Result<Rc<ShellRuntime>>`: an isolated runtime with the catalog (was `gpui_component_shell::new_isolated_runtime`). - `pub struct plugin::WindowSize` with `width(&self) -> f32` and `height(&self) -> f32`: a manifest's opening window size. - `PluginManifest::window(&self) -> Option<WindowSize>`: the manifest's `window`, if any. - `ManifestProblem::Window(String)`: an invalid `window` field. - `host::InvocationKind::{List, Agents, New}`: classifications for the new commands. - Manifest field `window: { width: number, height: number }`: logical pixels, positive, no other keys. - JavaScript: `window.start_window_move(): void` (GPUI `Window::start_window_move`), so a script-drawn title bar can drag the window. - JavaScript, `"gpui-component"`: `TitleBar` (`new TitleBar()`, with styles, `child`/`children` and `on_close_window(callback: (cx) => void)`). It is declared in the generated `gpui-kit.d.ts`. ## Validation - `cargo test -p gpui-shell --no-fail-fast`: 34 test binaries, 990 tests, no failures. - `cargo clippy -p gpui-shell --all-targets -- -D warnings`, `cargo fmt --check -p gpui-shell`, `cargo machete`, `typos`: clean. - `cargo check -p gpui-shell --no-default-features --features quickjs`: the runtime builds without the catalog. - `gpui-shell check -d` passes for `examples/js_todolist`, `examples/js_dock` and `examples/js_story`. - `npm --prefix crates/shell/tests/types test` and `node examples/js_story/fixtures/verify-coverage.mjs` pass. - Manually: `new` → `check` on a scaffolded application; a hand-built `Demo.app/Contents/{MacOS,Resources/app}` starts its bundled application with no arguments; OtpInput repainting on every keystroke was confirmed by eye in the running gallery. Not verified: - The release job and the install script have not run against a real release. - Bringing the window forward could not be confirmed from a background shell; macOS will not let it take focus from another app. It needs a check from a terminal launch. - Cmd-Q / Alt-F4 was not pressed in testing. - The manifest `window` size was unit-tested only, not opened end to end. - The TitleBar in `js_story`, `js_todolist` and `js_dock`, and whether a Sheet now opens flush under it, has not been looked at in a running window. ## AI-generated This change was written with Claude Code (one main session, two sub-agents for TitleBar and OtpInput, and a separate session for the manifest `window` field), reviewed and directed by the author. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 11 天前 | |
shell: Fix more runtime gaps found by the benchmark apps (#3510) ## Summary A batch of gpui-shell runtime fixes and additions found while running the benchmark apps (omanta, disktree). Each fix is its own commit; later shell changes in this series land here too. ### Virtual list rows and error messages A row built in a virtual list's item renderer could not register handlers: `row.on_click(...)` threw `` `on_click` cannot be registered from a virtual list's item renderer… ``. That is the way anyone writes a clickable row (the omanta benchmark app hit it), and a row with several clickable parts had no way to work at all. **Row handlers** (`engine/quickjs/mod.rs`, `spec.rs`, `materialize/components/virtual_list.rs`) - The refusal existed because callbacks lived as long as the render's snapshot while rows are rebuilt every frame, so per-row handlers would pile up. This is the batch-scoped generation the module comment recorded as the way to lift it. - Each batch of rows now records into a callback generation of its own, owned by a `RenderSnapshot` of the batch. Every row drawn from it holds the snapshot in a listener; GPUI keeps a frame's listeners until the next frame replaces them, so the generation is retired exactly when nothing on screen can deliver an event to it. This is the same lease the interactive inline renderers already use. - A batch that registers nothing takes the old path and adds no wrapper. - `on_item_click` / `on_item_secondary_click` are unchanged and remain the cheaper choice for a whole-row click. - Layout code without a lifetime of its own (dock chrome, or a batch reached while another generation is recording) still refuses, with a message that no longer assumes it is a list. **Whole error messages** (`engine/quickjs/js_error.rs`) - rquickjs's `Exception::throw_type` and its siblings copy the message into a 256-byte buffer, so longer runtime messages lost their second half, usually the part saying what to do instead. The same refusal above was cut at `on_item_secondary_click((key, event`. - All 354 call sites now throw through the realm's own `TypeError` / `RangeError` / `Error` constructors, which take any length and capture the stack the same way. If the constructor cannot be reached, they fall back to rquickjs. - Displayed stacks drop the runtime prelude's `eval_script` frames. The TypeScript declarations (`typings.rs`) now describe row handlers as supported. ### Unbounded JavaScript heap The shell capped each QuickJS runtime's heap at 256 MiB. A GPUI application written in Rust never budgets its memory, and the cap only turned legitimate data sets into broken applications. The disktree benchmark scanning a home directory (~218k files) hit it: - QuickJS throws `null` when it cannot allocate even the `out of memory` error (`quickjs.c`: "out of memory: throw JS_NULL to avoid recursing"). The app's `catch (e)` received `null`, and the runtime reported "A script threw null". - A failed reload keeps the old application (and its data) alive, so every later reload ran out while loading modules. Only a restart recovered. **Heap** (`engine/quickjs/sandbox.rs`, `mod.rs`) - No heap ceiling. Memory is the operating system's to manage, as it is for a Rust GPUI app. An embedding host can still bound its own runtime. - When a heap does run out, the failure says so. That covers `InternalError: out of memory` with no ceiling (the system could not allocate), a host ceiling being hit, and a `null` thrown while a ceiling is ≥ 90 % used. The message names the limit when there is one and suggests keeping less data in script or using a host module. - `docs/gpui-shell.md` §19.3: the memory row, plus the runaway-execution row, which still listed the 50 ms / 500 ms interrupt budgets replaced in #3509. ### `cx.open_path` A script could not open a file with the user's default application. Running `xdg-open` through `process.run` fails on Linux, because commands start with a cleared environment (`process.rs` `env_clear()`). Without `HOME`, `XDG_*`, `WAYLAND_DISPLAY` and the D-Bus address, `xdg-open` cannot identify the desktop, falls through to text browsers, and fails. The omanta benchmark app's "open with default application" never worked on Linux because of this. - `cx.open_path(path)` hands the path to `App::open_with_system`. GPUI runs the platform opener with the graphical session's environment on Linux, Launch Services on macOS, and the shell on Windows. It is on both `Context` and `AsyncContext`, like `open_url`. - It carries the authority reading the path would. The path must be inside a `capabilities.fs.read` grant and must exist. The check goes through the granted directory handle (`Grant::system_path`), so a symlink leading out of the grant is refused the way a read would be. A denial names `capabilities.fs.read`. - The session environment is not handed to arbitrary commands: `process.run` is unchanged. - Documented in `website/shell/api.md` (en and zh-CN) and in `typings.rs`. ### `process.spawn` `process.run` waits for the command to end, stops it at 30 seconds (`Limits::default`), and has no stdin. AI command lines need more: `claude -p --output-format stream-json`, `codex exec --json`, and long-lived `codex app-server` / `claude --input-format stream-json`. They have to be read line by line while they run, may run for minutes, take input over stdin, and must be cancellable. **Rust** (`process.rs`, engine-neutral) - `spawn_streaming` starts the child with the same rules as `run_bounded`: host-PATH resolution, `env_clear()`, its own process group (a Job Object on Windows). - A reader thread per stream decodes UTF-8 incrementally, so a character split across reads comes out whole and invalid bytes become U+FFFD. In line mode it joins lines across reads, strips `\n`/`\r\n`, and flushes a last unterminated line at EOF. A line over 8 MiB stops the process. - A waiting thread enforces `timeoutMs` when given. After the child exits it kills the rest of the group, so a descendant holding the pipes cannot keep the exit pending or outlive the command. It joins both readers before sending `Exit`, which is why `exit` always comes after the last output. - stdin is fed from its own thread, so a slow reader never blocks the UI. **Scheduler** (`scheduler::stream`) - A new primitive: events produced off the main thread are delivered to one script callback as `dispatch(name, payload, cx)` on the UI thread, with the async `cx` timer handlers get. Events that arrived together go out in one host call. A handler that throws is reported without costing the events after it. - The child is registered as a task owned by the view that spawned it, with "kill the process group" as its cancellation. Releasing the view, reloading the application (`release_application_generation` → `cancel_application_tasks`) or closing the window therefore kills it, through the same paths that already cancel timers and spawned work. - The channel is polled every 10 ms, like actor completions, rather than awaited. A wake from an OS thread is not a foreground schedule GPUI's deterministic test executor accepts. **JavaScript** - `process.spawn` is a thin prelude over a native `__process_spawn`. Handlers registered with `on()` live in script, and Rust holds only the one dispatch function, in the task registry that already releases persisted functions before the VM goes away. - `cwd` uses the same check as `fs`: the read grant, through the granted directory handle. - `run(command, args, { cwd, timeoutMs })` keeps 30 seconds when no timeout is given. - Documented in `typings.rs` (`declare module "process"`), the app template `host/agents.md` ("State, events and asynchrony", with a JSONL example), and `website/shell/capabilities.md` (en and zh-CN). ### Docs: start as a standalone app, move to a Rust host later `website/shell/index.md` and `getting-started.md` (en and zh-CN) described running scripts on the `gpui-shell` command as a development convenience behind plugins. They now recommend starting an application as a gpui-shell standalone app and show how the same scripts move under a Rust program when it needs more, without rewriting any JavaScript: - **Keep the command, add Rust**: register a `HostModule`, then call `gpui_shell::host::main_with_brand_and_components(HostBrand::new(..), components)`. The manifest, grants, data directory (keyed by the manifest `id`), and `dev`/`check`/`types`/`agents` all stay the same. `bundle` is refused under another name, because its launchers install `gpui-shell` releases. - **Build the runtime yourself**: the existing host example, moved to the end of Getting Started. - Getting Started now puts the JavaScript sections first and the Rust host last. - The host example used `gpui_kit::shell`, `set_store_path`, and `Capabilities::store`, none of which exist. It now uses `gpui_shell::`, `set_bundle_id`, and `storage(true)`. Other pages still use `gpui_kit::shell::` and are not changed here. ## Public API **gpui-shell (JavaScript)** - `cx.open_path(path: string): void` opens a file or directory inside a `capabilities.fs.read` grant with the system's default application (`App::open_with_system`). - `process.spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess` starts a command and streams its output. - `interface SpawnOptions { cwd?: string; stdin?: "pipe" | "null"; timeoutMs?: number; lines?: boolean }`. `cwd` must be inside `capabilities.fs.read`. `stdin` defaults to `"null"`. There is no timeout by default. `lines` delivers line by line. - `interface ChildProcess { readonly pid: number; on("stdout" | "stderr", (text, cx) => void): ChildProcess; on("exit", (result: ExitResult, cx) => void): ChildProcess; write(text: string): void; end(): void; kill(): void }` is the running child. `kill()` stops its process group, and `exit` fires once. - `interface ExitResult { code: number | null; signal: string | null; error?: string }`. `error` is set when the runtime stopped the process. - `interface RunOptions { cwd?: string; timeoutMs?: number }` and `process.run(command, args?, options?: RunOptions)` add an optional third argument. Without it, `run` behaves as before (30 s). ## Validation - `cargo test -p gpui-shell` (all pass), `cargo clippy -p gpui-shell --all-targets -- --deny warnings`, `cargo fmt --check` - `a_row_registers_its_own_handler_without_accumulating` replaces the refusal test: a click reaches the handler the third row registered, `on_item_click` still fires, and twenty scroll steps leave the live callback count within two of where it started. The existing "handlers do not accumulate while scrolled" test still passes. - `a_long_message_survives_whole` checks a 400+ character `TypeError` arrives intact and is still `instanceof TypeError`. - Not run against omanta in a window. - `the_shell_sets_no_heap_ceiling` checks that a runtime the shell creates reports no limit. - `running_out_of_memory_is_explained_with_the_limit` checks that a 16 MiB runtime filled by a loop reports the exhaustion and the limit, that a `null` thrown on a nearly full heap does too, and that after GC a `null` is reported as a plain `null` again. - Not re-run against disktree on a large directory. - `open_path_is_confined_to_the_read_grant`: a file and the granted directory itself pass both checks and reach the host-call gate, a missing file reports that it cannot be opened, and a path outside the grant is refused with `capabilities.fs.read`. Not exercised in a running desktop session. - `process.spawn`, Rust level (`process::tests`): - `printf` writes split mid-line are joined, including a trailing line with no newline; - 中 and 😀 split across writes decode whole, and an invalid byte becomes U+FFFD; - `cat` round-trips two written lines and exits 0, and a write after `end()` is refused; - `kill()` called twice ends `sh` and its backgrounded `sleep` with one `exit` (SIGKILL, no error), and the descendant's pid is verified gone; - `timeoutMs: 100` stops `sleep 5` with a "timed out after 100 ms" error; - no timeout means no deadline; - one byte past 8 MiB without a newline is refused; - `run` honours `cwd` and a longer timeout. - `process.spawn`, JavaScript level (`tests::process`): - lines reach `on()` handlers with a `cx` and the exit fires once with `code` 3 and `signal: null`; - an ungranted executable, a `cwd` outside the read grant, a bad `stdin` and `write()` without a pipe all throw; - releasing the application generation the way `dev --watch` reloads kills the child and its descendant. - The 29 process tests pass on three consecutive runs. - The generated `gpui-kit.d.ts` type-checks a script using `spawn`, `on`, `write`/`end`/`kill`, `pid` and `run` options with `tsc --strict`. The file has the same 5 pre-existing errors before and after, none in the `process` module. - Not tested: window close (it uses the same view-release cancellation, covered elsewhere), Windows and macOS locally (CI covers them), and a real `claude` / `codex` session. - Docs: both Rust snippets were compiled as temporary examples in `crates/shell`. The wrapper binary was run end to end: `new`, `check` passes, `types` emits `declare module "native"`, `dev` logs `Hello, world` from a script importing the Rust function, `bundle` is refused, and `--version` prints the brand. `bun test tests/doc-sources.test.ts tests/markdown.test.ts` passes. The full site build was not run, because it needs the sibling `gpui-kit-showcases` checkout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 9 天前 | |
website: Keep versioned docs links inside their version (#3243) Documentation pages read under `/versions/main` or `/versions/<tag>` linked back to the default version through site-root links (`/docs/entity`, `/component/...`), which often 404 there because the default is the latest release. ## Links stay in their version - 124 site-root links in `docs`, `component`, `base`, `shell` (en and zh-CN) now link the `.md` file relatively. zh-CN pages that linked English Base pages now link the Chinese ones. - `remarkDocLinks` prefixes any remaining site-root docs link with the build's base, so released snapshots (v0.6.6 still has such links) stay inside `/versions/<tag>` too. - Redirect destinations (`/docs/dock` → `/component/dock`, …) and the zh-CN → en language switch kept dropping the version base; both are fixed. - The 404 page, which GitHub Pages serves for every missing path, looks for the same page in the other versions and offers it ("This page is not in the v0.6.6 (latest) documentation, but it exists in main."). It also now loads the site stylesheet; it was unstyled before. ## GPUI snapshot version in one place Pages write `{{gpui_pre_version}}` in prose, code and docs.rs links. It resolves from the `gpui` entry of the workspace `Cargo.toml`; `script/build-website-versions` passes each revision's own value. The `.md` endpoints and `llms-full.txt` expand it as well. ## Maturity labels `maturity: [preview | experimental | showcase-only | platform-dependent | stable]` in frontmatter renders mono labels under the title, linking to a new Maturity section on the docs home. Marked: Mobile and WebView (Experimental, Platform-dependent), WebAssembly (Showcase only: it currently serves the component showcases, not shipped applications), all GPUI Shell pages (Preview), Native Extensions, SystemNotification and TitleBar (Platform-dependent). Unmarked pages are Stable. ## Checks - `bun run test:docs` (sources, no build): no site-root docs links; relative links resolve, in the same locale when possible; no hand-written pinned `gpui-pre` version or versioned docs.rs gpui-pre URL; known variables only; en/zh-CN maturity match. - `bun run test:links`: every `<a>` and redirect in a build reaches a built page; `test:versioned-examples` repeats it at `/versions/test/`, where leaving the version fails. - Test Docs now also runs on `Cargo.toml` changes. Verified locally with a production-shaped build (v0.6.6 at `/`, this branch at `/versions/main`): all website tests pass, the v0.6.6 snapshot passes the link check at `/versions/v0.6.6/`, and the 404 fallback works for `/docs/entity` and `/zh-CN/docs/entity`. Publishing a release that contains the new GPUI guides is what makes `/docs/` show them; this PR does not change which version is the default. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> | 28 天前 | |
shell: Add `bundle` to package an application, and a `dev` command (#3487) Adds `gpui-shell bundle`, which turns an application directory into something a user can install or open on macOS, Linux and Windows, modeled on the installer folio-mail ships by hand. Also adds `gpui-shell dev` for the edit-and-reload loop. ## What changes **`bundle`.** One target per run, from any platform (`--platform macos|linux|windows`, default: the host). Output goes to `./dist`. Needs `gpui-shell.json`; names come from its `name`, `id` and `version`. Hidden files, `node_modules`, `gpui-kit.d.ts`, the root `jsconfig.json`/`AGENTS.md` and the output directory are left out. | Target | Output | How it runs | | --- | --- | --- | | macOS | `Name.app`, unsigned | The executable is a launcher script. On launch it finds a gpui-shell or downloads one. Signing and notarization are left to the author. | | Linux | `name-1.0.0-linux.tar.gz` and `install.sh` beside it | The archive holds the launcher, `app/` and an icon, and runs where it is unpacked. `install.sh` has the archive's SHA-256 baked in. It installs from the file beside it, from a path or URL argument, or from `--download-url`, so `curl …/install.sh \| sh` works. It installs into `~/.local/share/<id>` with a `~/.local/bin` link, a desktop entry and a hicolor icon. `--uninstall` keeps data and the shared runtime. | | Windows | `name-1.0.0-windows-x86_64.zip`, portable | `Name.exe` is the same version's release `gpui-shell.exe` with its PE subsystem switched to GUI, so no console window opens. `app\` sits beside it. When bundling on another platform, the executable is downloaded from the release, verified and cached under `<shell home>/cache/runtime`. | **No runtime on macOS and Linux.** These bundles are about 0.6–1 MB. Their launchers look for gpui-shell in `~/.local/bin`, then `PATH`, Homebrew and `~/.cargo/bin`, and take one at the bundling version or later. If none qualifies, they download that release, check its `.sha256`, and install it to `~/.local/bin/gpui-shell`, the same place `install.sh` uses. Messages go to the terminal, `notify-send` or `osascript`. **`run --packaged`.** The launchers use it to run a directory as a packaged application: - data is kept under the manifest `id`; - no `gpui-kit.d.ts` / `jsconfig.json` / dependency links are written into it (new `ShellRuntime::set_editor_files`); - the window is titled by the manifest `name`. The app beside the executable (`Application::Bundled`) gets the same treatment. Every window with a manifest now gets the manifest `id` as its `app_id`, so a Linux desktop matches it to `<id>.desktop`. **`dev`.** `gpui-shell dev [<name> | -d <dir>]` reloads on change and logs at debug level. `run --watch` still reloads. `--dev` (with `run` or `dev`) now only relaxes the sandbox, and only `run` and `dev` accept these flags. **Other.** - `gpui-shell new` adds `dist/` to `.gitignore`. - Docs, en + zh-CN: Packaging is rewritten around `bundle`, with the hand-assembled standalone layout kept as reference. Getting Started, Capabilities, Hosting, Overlays and the crate README are updated for `dev`. ## Public API ### gpui-shell - `ShellRuntime::set_editor_files(&self, enabled: bool)`: whether loading an application writes the editor files (`gpui-kit.d.ts`, `jsconfig.json`, dependency links) into it. On by default; off for packaged applications. - `host::InvocationKind::Bundle`: the classification for the new `bundle` command. - CLI: `bundle [<name> | --dir <dir>] [--platform <platform>] [--icon <png>] [--out <dir>] [--download-url <url>]`, `dev [<name> | --dir <dir>] [--dev]`, and `run … --packaged`. ## Breaking Changes `--dev` no longer implies `--watch` or a debug log. `dev` is the command that reloads. ```diff - gpui-shell run -d app --dev + gpui-shell dev -d app # reload + debug log + gpui-shell dev -d app --dev # … and the relaxed sandbox, as before ``` `InvocationKind` gains a variant, so an exhaustive `match` over it needs a `Bundle` arm. ```diff match gpui_shell::host::parse_invocation(arguments)? { InvocationKind::Run => …, + InvocationKind::Bundle => …, … } ``` ## Validation - `cargo test -p gpui-shell --no-fail-fast` passes. The lib has 872 passed and 1 ignored, and every other test binary passes too. - `cargo clippy -p gpui-shell --all-targets -- --deny warnings`, `cargo fmt --check`, `cargo machete`: clean. `typos` is not installed here and was not run. - Linux, end to end with folio-mail in a temporary `HOME`: - bundle; - `curl http://127.0.0.1:…/install.sh | sh` from a local HTTP server; - launch; - reinstall and `--uninstall`; - running the unpacked launcher directly. - Data lands under `gpui-shell/apps/<id>` and survives uninstall. - Launcher and installer scripts in temporary `HOME`s: - The download path was run against the real v0.7.3 release, including a tampered checksum, which is rejected. - The installed-runtime path and version comparison were tested with fake binaries. - `install.sh` was run from beside the archive, with a path argument, with a URL, piped through `sh`, with an archive whose SHA-256 doesn't match, and with refusals to replace links or foreign files. - Windows: bundled with the real v0.7.3 `gpui-shell.exe` downloaded from the release. The zip passes `testzip`, the exe's subsystem is 2 (GUI) with one byte changed, and the Unix mode is kept. - macOS: bundled on Linux. `Info.plist` parses, the icon is a valid `.icns`, and the launcher was walked through on Linux with fake `uname`/`osascript`/`curl`. Not verified: - Not run on a real Mac or Windows machine. On macOS, whether the menu bar and Dock show the app or `gpui-shell` after the launcher `exec`s is unknown. - The macOS and Linux launchers need a released gpui-shell with `--packaged`, so they work once the next version ships. Until then the runtime they accept (≥ 0.7.3) rejects the flag. The Windows portable zip works with v0.7.3 today, though 0.7.3 titles its window `app — gpui-shell`. ## AI-generated This change was written with Claude Code: one main session, with sub-agents for the launcher/installer scripts and the docs, directed and reviewed by the author. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 10 天前 | |
shell: Fix more runtime gaps found by the benchmark apps (#3510) ## Summary A batch of gpui-shell runtime fixes and additions found while running the benchmark apps (omanta, disktree). Each fix is its own commit; later shell changes in this series land here too. ### Virtual list rows and error messages A row built in a virtual list's item renderer could not register handlers: `row.on_click(...)` threw `` `on_click` cannot be registered from a virtual list's item renderer… ``. That is the way anyone writes a clickable row (the omanta benchmark app hit it), and a row with several clickable parts had no way to work at all. **Row handlers** (`engine/quickjs/mod.rs`, `spec.rs`, `materialize/components/virtual_list.rs`) - The refusal existed because callbacks lived as long as the render's snapshot while rows are rebuilt every frame, so per-row handlers would pile up. This is the batch-scoped generation the module comment recorded as the way to lift it. - Each batch of rows now records into a callback generation of its own, owned by a `RenderSnapshot` of the batch. Every row drawn from it holds the snapshot in a listener; GPUI keeps a frame's listeners until the next frame replaces them, so the generation is retired exactly when nothing on screen can deliver an event to it. This is the same lease the interactive inline renderers already use. - A batch that registers nothing takes the old path and adds no wrapper. - `on_item_click` / `on_item_secondary_click` are unchanged and remain the cheaper choice for a whole-row click. - Layout code without a lifetime of its own (dock chrome, or a batch reached while another generation is recording) still refuses, with a message that no longer assumes it is a list. **Whole error messages** (`engine/quickjs/js_error.rs`) - rquickjs's `Exception::throw_type` and its siblings copy the message into a 256-byte buffer, so longer runtime messages lost their second half, usually the part saying what to do instead. The same refusal above was cut at `on_item_secondary_click((key, event`. - All 354 call sites now throw through the realm's own `TypeError` / `RangeError` / `Error` constructors, which take any length and capture the stack the same way. If the constructor cannot be reached, they fall back to rquickjs. - Displayed stacks drop the runtime prelude's `eval_script` frames. The TypeScript declarations (`typings.rs`) now describe row handlers as supported. ### Unbounded JavaScript heap The shell capped each QuickJS runtime's heap at 256 MiB. A GPUI application written in Rust never budgets its memory, and the cap only turned legitimate data sets into broken applications. The disktree benchmark scanning a home directory (~218k files) hit it: - QuickJS throws `null` when it cannot allocate even the `out of memory` error (`quickjs.c`: "out of memory: throw JS_NULL to avoid recursing"). The app's `catch (e)` received `null`, and the runtime reported "A script threw null". - A failed reload keeps the old application (and its data) alive, so every later reload ran out while loading modules. Only a restart recovered. **Heap** (`engine/quickjs/sandbox.rs`, `mod.rs`) - No heap ceiling. Memory is the operating system's to manage, as it is for a Rust GPUI app. An embedding host can still bound its own runtime. - When a heap does run out, the failure says so. That covers `InternalError: out of memory` with no ceiling (the system could not allocate), a host ceiling being hit, and a `null` thrown while a ceiling is ≥ 90 % used. The message names the limit when there is one and suggests keeping less data in script or using a host module. - `docs/gpui-shell.md` §19.3: the memory row, plus the runaway-execution row, which still listed the 50 ms / 500 ms interrupt budgets replaced in #3509. ### `cx.open_path` A script could not open a file with the user's default application. Running `xdg-open` through `process.run` fails on Linux, because commands start with a cleared environment (`process.rs` `env_clear()`). Without `HOME`, `XDG_*`, `WAYLAND_DISPLAY` and the D-Bus address, `xdg-open` cannot identify the desktop, falls through to text browsers, and fails. The omanta benchmark app's "open with default application" never worked on Linux because of this. - `cx.open_path(path)` hands the path to `App::open_with_system`. GPUI runs the platform opener with the graphical session's environment on Linux, Launch Services on macOS, and the shell on Windows. It is on both `Context` and `AsyncContext`, like `open_url`. - It carries the authority reading the path would. The path must be inside a `capabilities.fs.read` grant and must exist. The check goes through the granted directory handle (`Grant::system_path`), so a symlink leading out of the grant is refused the way a read would be. A denial names `capabilities.fs.read`. - The session environment is not handed to arbitrary commands: `process.run` is unchanged. - Documented in `website/shell/api.md` (en and zh-CN) and in `typings.rs`. ### `process.spawn` `process.run` waits for the command to end, stops it at 30 seconds (`Limits::default`), and has no stdin. AI command lines need more: `claude -p --output-format stream-json`, `codex exec --json`, and long-lived `codex app-server` / `claude --input-format stream-json`. They have to be read line by line while they run, may run for minutes, take input over stdin, and must be cancellable. **Rust** (`process.rs`, engine-neutral) - `spawn_streaming` starts the child with the same rules as `run_bounded`: host-PATH resolution, `env_clear()`, its own process group (a Job Object on Windows). - A reader thread per stream decodes UTF-8 incrementally, so a character split across reads comes out whole and invalid bytes become U+FFFD. In line mode it joins lines across reads, strips `\n`/`\r\n`, and flushes a last unterminated line at EOF. A line over 8 MiB stops the process. - A waiting thread enforces `timeoutMs` when given. After the child exits it kills the rest of the group, so a descendant holding the pipes cannot keep the exit pending or outlive the command. It joins both readers before sending `Exit`, which is why `exit` always comes after the last output. - stdin is fed from its own thread, so a slow reader never blocks the UI. **Scheduler** (`scheduler::stream`) - A new primitive: events produced off the main thread are delivered to one script callback as `dispatch(name, payload, cx)` on the UI thread, with the async `cx` timer handlers get. Events that arrived together go out in one host call. A handler that throws is reported without costing the events after it. - The child is registered as a task owned by the view that spawned it, with "kill the process group" as its cancellation. Releasing the view, reloading the application (`release_application_generation` → `cancel_application_tasks`) or closing the window therefore kills it, through the same paths that already cancel timers and spawned work. - The channel is polled every 10 ms, like actor completions, rather than awaited. A wake from an OS thread is not a foreground schedule GPUI's deterministic test executor accepts. **JavaScript** - `process.spawn` is a thin prelude over a native `__process_spawn`. Handlers registered with `on()` live in script, and Rust holds only the one dispatch function, in the task registry that already releases persisted functions before the VM goes away. - `cwd` uses the same check as `fs`: the read grant, through the granted directory handle. - `run(command, args, { cwd, timeoutMs })` keeps 30 seconds when no timeout is given. - Documented in `typings.rs` (`declare module "process"`), the app template `host/agents.md` ("State, events and asynchrony", with a JSONL example), and `website/shell/capabilities.md` (en and zh-CN). ### Docs: start as a standalone app, move to a Rust host later `website/shell/index.md` and `getting-started.md` (en and zh-CN) described running scripts on the `gpui-shell` command as a development convenience behind plugins. They now recommend starting an application as a gpui-shell standalone app and show how the same scripts move under a Rust program when it needs more, without rewriting any JavaScript: - **Keep the command, add Rust**: register a `HostModule`, then call `gpui_shell::host::main_with_brand_and_components(HostBrand::new(..), components)`. The manifest, grants, data directory (keyed by the manifest `id`), and `dev`/`check`/`types`/`agents` all stay the same. `bundle` is refused under another name, because its launchers install `gpui-shell` releases. - **Build the runtime yourself**: the existing host example, moved to the end of Getting Started. - Getting Started now puts the JavaScript sections first and the Rust host last. - The host example used `gpui_kit::shell`, `set_store_path`, and `Capabilities::store`, none of which exist. It now uses `gpui_shell::`, `set_bundle_id`, and `storage(true)`. Other pages still use `gpui_kit::shell::` and are not changed here. ## Public API **gpui-shell (JavaScript)** - `cx.open_path(path: string): void` opens a file or directory inside a `capabilities.fs.read` grant with the system's default application (`App::open_with_system`). - `process.spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess` starts a command and streams its output. - `interface SpawnOptions { cwd?: string; stdin?: "pipe" | "null"; timeoutMs?: number; lines?: boolean }`. `cwd` must be inside `capabilities.fs.read`. `stdin` defaults to `"null"`. There is no timeout by default. `lines` delivers line by line. - `interface ChildProcess { readonly pid: number; on("stdout" | "stderr", (text, cx) => void): ChildProcess; on("exit", (result: ExitResult, cx) => void): ChildProcess; write(text: string): void; end(): void; kill(): void }` is the running child. `kill()` stops its process group, and `exit` fires once. - `interface ExitResult { code: number | null; signal: string | null; error?: string }`. `error` is set when the runtime stopped the process. - `interface RunOptions { cwd?: string; timeoutMs?: number }` and `process.run(command, args?, options?: RunOptions)` add an optional third argument. Without it, `run` behaves as before (30 s). ## Validation - `cargo test -p gpui-shell` (all pass), `cargo clippy -p gpui-shell --all-targets -- --deny warnings`, `cargo fmt --check` - `a_row_registers_its_own_handler_without_accumulating` replaces the refusal test: a click reaches the handler the third row registered, `on_item_click` still fires, and twenty scroll steps leave the live callback count within two of where it started. The existing "handlers do not accumulate while scrolled" test still passes. - `a_long_message_survives_whole` checks a 400+ character `TypeError` arrives intact and is still `instanceof TypeError`. - Not run against omanta in a window. - `the_shell_sets_no_heap_ceiling` checks that a runtime the shell creates reports no limit. - `running_out_of_memory_is_explained_with_the_limit` checks that a 16 MiB runtime filled by a loop reports the exhaustion and the limit, that a `null` thrown on a nearly full heap does too, and that after GC a `null` is reported as a plain `null` again. - Not re-run against disktree on a large directory. - `open_path_is_confined_to_the_read_grant`: a file and the granted directory itself pass both checks and reach the host-call gate, a missing file reports that it cannot be opened, and a path outside the grant is refused with `capabilities.fs.read`. Not exercised in a running desktop session. - `process.spawn`, Rust level (`process::tests`): - `printf` writes split mid-line are joined, including a trailing line with no newline; - 中 and 😀 split across writes decode whole, and an invalid byte becomes U+FFFD; - `cat` round-trips two written lines and exits 0, and a write after `end()` is refused; - `kill()` called twice ends `sh` and its backgrounded `sleep` with one `exit` (SIGKILL, no error), and the descendant's pid is verified gone; - `timeoutMs: 100` stops `sleep 5` with a "timed out after 100 ms" error; - no timeout means no deadline; - one byte past 8 MiB without a newline is refused; - `run` honours `cwd` and a longer timeout. - `process.spawn`, JavaScript level (`tests::process`): - lines reach `on()` handlers with a `cx` and the exit fires once with `code` 3 and `signal: null`; - an ungranted executable, a `cwd` outside the read grant, a bad `stdin` and `write()` without a pipe all throw; - releasing the application generation the way `dev --watch` reloads kills the child and its descendant. - The 29 process tests pass on three consecutive runs. - The generated `gpui-kit.d.ts` type-checks a script using `spawn`, `on`, `write`/`end`/`kill`, `pid` and `run` options with `tsc --strict`. The file has the same 5 pre-existing errors before and after, none in the `process` module. - Not tested: window close (it uses the same view-release cancellation, covered elsewhere), Windows and macOS locally (CI covers them), and a real `claude` / `codex` session. - Docs: both Rust snippets were compiled as temporary examples in `crates/shell`. The wrapper binary was run end to end: `new`, `check` passes, `types` emits `declare module "native"`, `dev` logs `Hello, world` from a script importing the Rust function, `bundle` is refused, and `--version` prints the brand. `bun test tests/doc-sources.test.ts tests/markdown.test.ts` passes. The full site build was not run, because it needs the sibling `gpui-kit-showcases` checkout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 9 天前 | |
shell: Fix more runtime gaps found by the benchmark apps (#3510) ## Summary A batch of gpui-shell runtime fixes and additions found while running the benchmark apps (omanta, disktree). Each fix is its own commit; later shell changes in this series land here too. ### Virtual list rows and error messages A row built in a virtual list's item renderer could not register handlers: `row.on_click(...)` threw `` `on_click` cannot be registered from a virtual list's item renderer… ``. That is the way anyone writes a clickable row (the omanta benchmark app hit it), and a row with several clickable parts had no way to work at all. **Row handlers** (`engine/quickjs/mod.rs`, `spec.rs`, `materialize/components/virtual_list.rs`) - The refusal existed because callbacks lived as long as the render's snapshot while rows are rebuilt every frame, so per-row handlers would pile up. This is the batch-scoped generation the module comment recorded as the way to lift it. - Each batch of rows now records into a callback generation of its own, owned by a `RenderSnapshot` of the batch. Every row drawn from it holds the snapshot in a listener; GPUI keeps a frame's listeners until the next frame replaces them, so the generation is retired exactly when nothing on screen can deliver an event to it. This is the same lease the interactive inline renderers already use. - A batch that registers nothing takes the old path and adds no wrapper. - `on_item_click` / `on_item_secondary_click` are unchanged and remain the cheaper choice for a whole-row click. - Layout code without a lifetime of its own (dock chrome, or a batch reached while another generation is recording) still refuses, with a message that no longer assumes it is a list. **Whole error messages** (`engine/quickjs/js_error.rs`) - rquickjs's `Exception::throw_type` and its siblings copy the message into a 256-byte buffer, so longer runtime messages lost their second half, usually the part saying what to do instead. The same refusal above was cut at `on_item_secondary_click((key, event`. - All 354 call sites now throw through the realm's own `TypeError` / `RangeError` / `Error` constructors, which take any length and capture the stack the same way. If the constructor cannot be reached, they fall back to rquickjs. - Displayed stacks drop the runtime prelude's `eval_script` frames. The TypeScript declarations (`typings.rs`) now describe row handlers as supported. ### Unbounded JavaScript heap The shell capped each QuickJS runtime's heap at 256 MiB. A GPUI application written in Rust never budgets its memory, and the cap only turned legitimate data sets into broken applications. The disktree benchmark scanning a home directory (~218k files) hit it: - QuickJS throws `null` when it cannot allocate even the `out of memory` error (`quickjs.c`: "out of memory: throw JS_NULL to avoid recursing"). The app's `catch (e)` received `null`, and the runtime reported "A script threw null". - A failed reload keeps the old application (and its data) alive, so every later reload ran out while loading modules. Only a restart recovered. **Heap** (`engine/quickjs/sandbox.rs`, `mod.rs`) - No heap ceiling. Memory is the operating system's to manage, as it is for a Rust GPUI app. An embedding host can still bound its own runtime. - When a heap does run out, the failure says so. That covers `InternalError: out of memory` with no ceiling (the system could not allocate), a host ceiling being hit, and a `null` thrown while a ceiling is ≥ 90 % used. The message names the limit when there is one and suggests keeping less data in script or using a host module. - `docs/gpui-shell.md` §19.3: the memory row, plus the runaway-execution row, which still listed the 50 ms / 500 ms interrupt budgets replaced in #3509. ### `cx.open_path` A script could not open a file with the user's default application. Running `xdg-open` through `process.run` fails on Linux, because commands start with a cleared environment (`process.rs` `env_clear()`). Without `HOME`, `XDG_*`, `WAYLAND_DISPLAY` and the D-Bus address, `xdg-open` cannot identify the desktop, falls through to text browsers, and fails. The omanta benchmark app's "open with default application" never worked on Linux because of this. - `cx.open_path(path)` hands the path to `App::open_with_system`. GPUI runs the platform opener with the graphical session's environment on Linux, Launch Services on macOS, and the shell on Windows. It is on both `Context` and `AsyncContext`, like `open_url`. - It carries the authority reading the path would. The path must be inside a `capabilities.fs.read` grant and must exist. The check goes through the granted directory handle (`Grant::system_path`), so a symlink leading out of the grant is refused the way a read would be. A denial names `capabilities.fs.read`. - The session environment is not handed to arbitrary commands: `process.run` is unchanged. - Documented in `website/shell/api.md` (en and zh-CN) and in `typings.rs`. ### `process.spawn` `process.run` waits for the command to end, stops it at 30 seconds (`Limits::default`), and has no stdin. AI command lines need more: `claude -p --output-format stream-json`, `codex exec --json`, and long-lived `codex app-server` / `claude --input-format stream-json`. They have to be read line by line while they run, may run for minutes, take input over stdin, and must be cancellable. **Rust** (`process.rs`, engine-neutral) - `spawn_streaming` starts the child with the same rules as `run_bounded`: host-PATH resolution, `env_clear()`, its own process group (a Job Object on Windows). - A reader thread per stream decodes UTF-8 incrementally, so a character split across reads comes out whole and invalid bytes become U+FFFD. In line mode it joins lines across reads, strips `\n`/`\r\n`, and flushes a last unterminated line at EOF. A line over 8 MiB stops the process. - A waiting thread enforces `timeoutMs` when given. After the child exits it kills the rest of the group, so a descendant holding the pipes cannot keep the exit pending or outlive the command. It joins both readers before sending `Exit`, which is why `exit` always comes after the last output. - stdin is fed from its own thread, so a slow reader never blocks the UI. **Scheduler** (`scheduler::stream`) - A new primitive: events produced off the main thread are delivered to one script callback as `dispatch(name, payload, cx)` on the UI thread, with the async `cx` timer handlers get. Events that arrived together go out in one host call. A handler that throws is reported without costing the events after it. - The child is registered as a task owned by the view that spawned it, with "kill the process group" as its cancellation. Releasing the view, reloading the application (`release_application_generation` → `cancel_application_tasks`) or closing the window therefore kills it, through the same paths that already cancel timers and spawned work. - The channel is polled every 10 ms, like actor completions, rather than awaited. A wake from an OS thread is not a foreground schedule GPUI's deterministic test executor accepts. **JavaScript** - `process.spawn` is a thin prelude over a native `__process_spawn`. Handlers registered with `on()` live in script, and Rust holds only the one dispatch function, in the task registry that already releases persisted functions before the VM goes away. - `cwd` uses the same check as `fs`: the read grant, through the granted directory handle. - `run(command, args, { cwd, timeoutMs })` keeps 30 seconds when no timeout is given. - Documented in `typings.rs` (`declare module "process"`), the app template `host/agents.md` ("State, events and asynchrony", with a JSONL example), and `website/shell/capabilities.md` (en and zh-CN). ### Docs: start as a standalone app, move to a Rust host later `website/shell/index.md` and `getting-started.md` (en and zh-CN) described running scripts on the `gpui-shell` command as a development convenience behind plugins. They now recommend starting an application as a gpui-shell standalone app and show how the same scripts move under a Rust program when it needs more, without rewriting any JavaScript: - **Keep the command, add Rust**: register a `HostModule`, then call `gpui_shell::host::main_with_brand_and_components(HostBrand::new(..), components)`. The manifest, grants, data directory (keyed by the manifest `id`), and `dev`/`check`/`types`/`agents` all stay the same. `bundle` is refused under another name, because its launchers install `gpui-shell` releases. - **Build the runtime yourself**: the existing host example, moved to the end of Getting Started. - Getting Started now puts the JavaScript sections first and the Rust host last. - The host example used `gpui_kit::shell`, `set_store_path`, and `Capabilities::store`, none of which exist. It now uses `gpui_shell::`, `set_bundle_id`, and `storage(true)`. Other pages still use `gpui_kit::shell::` and are not changed here. ## Public API **gpui-shell (JavaScript)** - `cx.open_path(path: string): void` opens a file or directory inside a `capabilities.fs.read` grant with the system's default application (`App::open_with_system`). - `process.spawn(command: string, args?: string[], options?: SpawnOptions): ChildProcess` starts a command and streams its output. - `interface SpawnOptions { cwd?: string; stdin?: "pipe" | "null"; timeoutMs?: number; lines?: boolean }`. `cwd` must be inside `capabilities.fs.read`. `stdin` defaults to `"null"`. There is no timeout by default. `lines` delivers line by line. - `interface ChildProcess { readonly pid: number; on("stdout" | "stderr", (text, cx) => void): ChildProcess; on("exit", (result: ExitResult, cx) => void): ChildProcess; write(text: string): void; end(): void; kill(): void }` is the running child. `kill()` stops its process group, and `exit` fires once. - `interface ExitResult { code: number | null; signal: string | null; error?: string }`. `error` is set when the runtime stopped the process. - `interface RunOptions { cwd?: string; timeoutMs?: number }` and `process.run(command, args?, options?: RunOptions)` add an optional third argument. Without it, `run` behaves as before (30 s). ## Validation - `cargo test -p gpui-shell` (all pass), `cargo clippy -p gpui-shell --all-targets -- --deny warnings`, `cargo fmt --check` - `a_row_registers_its_own_handler_without_accumulating` replaces the refusal test: a click reaches the handler the third row registered, `on_item_click` still fires, and twenty scroll steps leave the live callback count within two of where it started. The existing "handlers do not accumulate while scrolled" test still passes. - `a_long_message_survives_whole` checks a 400+ character `TypeError` arrives intact and is still `instanceof TypeError`. - Not run against omanta in a window. - `the_shell_sets_no_heap_ceiling` checks that a runtime the shell creates reports no limit. - `running_out_of_memory_is_explained_with_the_limit` checks that a 16 MiB runtime filled by a loop reports the exhaustion and the limit, that a `null` thrown on a nearly full heap does too, and that after GC a `null` is reported as a plain `null` again. - Not re-run against disktree on a large directory. - `open_path_is_confined_to_the_read_grant`: a file and the granted directory itself pass both checks and reach the host-call gate, a missing file reports that it cannot be opened, and a path outside the grant is refused with `capabilities.fs.read`. Not exercised in a running desktop session. - `process.spawn`, Rust level (`process::tests`): - `printf` writes split mid-line are joined, including a trailing line with no newline; - 中 and 😀 split across writes decode whole, and an invalid byte becomes U+FFFD; - `cat` round-trips two written lines and exits 0, and a write after `end()` is refused; - `kill()` called twice ends `sh` and its backgrounded `sleep` with one `exit` (SIGKILL, no error), and the descendant's pid is verified gone; - `timeoutMs: 100` stops `sleep 5` with a "timed out after 100 ms" error; - no timeout means no deadline; - one byte past 8 MiB without a newline is refused; - `run` honours `cwd` and a longer timeout. - `process.spawn`, JavaScript level (`tests::process`): - lines reach `on()` handlers with a `cx` and the exit fires once with `code` 3 and `signal: null`; - an ungranted executable, a `cwd` outside the read grant, a bad `stdin` and `write()` without a pipe all throw; - releasing the application generation the way `dev --watch` reloads kills the child and its descendant. - The 29 process tests pass on three consecutive runs. - The generated `gpui-kit.d.ts` type-checks a script using `spawn`, `on`, `write`/`end`/`kill`, `pid` and `run` options with `tsc --strict`. The file has the same 5 pre-existing errors before and after, none in the `process` module. - Not tested: window close (it uses the same view-release cancellation, covered elsewhere), Windows and macOS locally (CI covers them), and a real `claude` / `codex` session. - Docs: both Rust snippets were compiled as temporary examples in `crates/shell`. The wrapper binary was run end to end: `new`, `check` passes, `types` emits `declare module "native"`, `dev` logs `Hello, world` from a script importing the Rust function, `bundle` is refused, and `--version` prints the brand. `bun test tests/doc-sources.test.ts tests/markdown.test.ts` passes. The full site build was not run, because it needs the sibling `gpui-kit-showcases` checkout. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 9 天前 | |
shell: Add `bundle` to package an application, and a `dev` command (#3487) Adds `gpui-shell bundle`, which turns an application directory into something a user can install or open on macOS, Linux and Windows, modeled on the installer folio-mail ships by hand. Also adds `gpui-shell dev` for the edit-and-reload loop. ## What changes **`bundle`.** One target per run, from any platform (`--platform macos|linux|windows`, default: the host). Output goes to `./dist`. Needs `gpui-shell.json`; names come from its `name`, `id` and `version`. Hidden files, `node_modules`, `gpui-kit.d.ts`, the root `jsconfig.json`/`AGENTS.md` and the output directory are left out. | Target | Output | How it runs | | --- | --- | --- | | macOS | `Name.app`, unsigned | The executable is a launcher script. On launch it finds a gpui-shell or downloads one. Signing and notarization are left to the author. | | Linux | `name-1.0.0-linux.tar.gz` and `install.sh` beside it | The archive holds the launcher, `app/` and an icon, and runs where it is unpacked. `install.sh` has the archive's SHA-256 baked in. It installs from the file beside it, from a path or URL argument, or from `--download-url`, so `curl …/install.sh \| sh` works. It installs into `~/.local/share/<id>` with a `~/.local/bin` link, a desktop entry and a hicolor icon. `--uninstall` keeps data and the shared runtime. | | Windows | `name-1.0.0-windows-x86_64.zip`, portable | `Name.exe` is the same version's release `gpui-shell.exe` with its PE subsystem switched to GUI, so no console window opens. `app\` sits beside it. When bundling on another platform, the executable is downloaded from the release, verified and cached under `<shell home>/cache/runtime`. | **No runtime on macOS and Linux.** These bundles are about 0.6–1 MB. Their launchers look for gpui-shell in `~/.local/bin`, then `PATH`, Homebrew and `~/.cargo/bin`, and take one at the bundling version or later. If none qualifies, they download that release, check its `.sha256`, and install it to `~/.local/bin/gpui-shell`, the same place `install.sh` uses. Messages go to the terminal, `notify-send` or `osascript`. **`run --packaged`.** The launchers use it to run a directory as a packaged application: - data is kept under the manifest `id`; - no `gpui-kit.d.ts` / `jsconfig.json` / dependency links are written into it (new `ShellRuntime::set_editor_files`); - the window is titled by the manifest `name`. The app beside the executable (`Application::Bundled`) gets the same treatment. Every window with a manifest now gets the manifest `id` as its `app_id`, so a Linux desktop matches it to `<id>.desktop`. **`dev`.** `gpui-shell dev [<name> | -d <dir>]` reloads on change and logs at debug level. `run --watch` still reloads. `--dev` (with `run` or `dev`) now only relaxes the sandbox, and only `run` and `dev` accept these flags. **Other.** - `gpui-shell new` adds `dist/` to `.gitignore`. - Docs, en + zh-CN: Packaging is rewritten around `bundle`, with the hand-assembled standalone layout kept as reference. Getting Started, Capabilities, Hosting, Overlays and the crate README are updated for `dev`. ## Public API ### gpui-shell - `ShellRuntime::set_editor_files(&self, enabled: bool)`: whether loading an application writes the editor files (`gpui-kit.d.ts`, `jsconfig.json`, dependency links) into it. On by default; off for packaged applications. - `host::InvocationKind::Bundle`: the classification for the new `bundle` command. - CLI: `bundle [<name> | --dir <dir>] [--platform <platform>] [--icon <png>] [--out <dir>] [--download-url <url>]`, `dev [<name> | --dir <dir>] [--dev]`, and `run … --packaged`. ## Breaking Changes `--dev` no longer implies `--watch` or a debug log. `dev` is the command that reloads. ```diff - gpui-shell run -d app --dev + gpui-shell dev -d app # reload + debug log + gpui-shell dev -d app --dev # … and the relaxed sandbox, as before ``` `InvocationKind` gains a variant, so an exhaustive `match` over it needs a `Bundle` arm. ```diff match gpui_shell::host::parse_invocation(arguments)? { InvocationKind::Run => …, + InvocationKind::Bundle => …, … } ``` ## Validation - `cargo test -p gpui-shell --no-fail-fast` passes. The lib has 872 passed and 1 ignored, and every other test binary passes too. - `cargo clippy -p gpui-shell --all-targets -- --deny warnings`, `cargo fmt --check`, `cargo machete`: clean. `typos` is not installed here and was not run. - Linux, end to end with folio-mail in a temporary `HOME`: - bundle; - `curl http://127.0.0.1:…/install.sh | sh` from a local HTTP server; - launch; - reinstall and `--uninstall`; - running the unpacked launcher directly. - Data lands under `gpui-shell/apps/<id>` and survives uninstall. - Launcher and installer scripts in temporary `HOME`s: - The download path was run against the real v0.7.3 release, including a tampered checksum, which is rejected. - The installed-runtime path and version comparison were tested with fake binaries. - `install.sh` was run from beside the archive, with a path argument, with a URL, piped through `sh`, with an archive whose SHA-256 doesn't match, and with refusals to replace links or foreign files. - Windows: bundled with the real v0.7.3 `gpui-shell.exe` downloaded from the release. The zip passes `testzip`, the exe's subsystem is 2 (GUI) with one byte changed, and the Unix mode is kept. - macOS: bundled on Linux. `Info.plist` parses, the icon is a valid `.icns`, and the launcher was walked through on Linux with fake `uname`/`osascript`/`curl`. Not verified: - Not run on a real Mac or Windows machine. On macOS, whether the menu bar and Dock show the app or `gpui-shell` after the launcher `exec`s is unknown. - The macOS and Linux launchers need a released gpui-shell with `--packaged`, so they work once the next version ships. Until then the runtime they accept (≥ 0.7.3) rejects the flag. The Windows portable zip works with v0.7.3 today, though 0.7.3 titles its window `app — gpui-shell`. ## AI-generated This change was written with Claude Code: one main session, with sub-agents for the launcher/installer scripts and the docs, directed and reviewed by the author. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 10 天前 | |
website: Keep versioned docs links inside their version (#3243) Documentation pages read under `/versions/main` or `/versions/<tag>` linked back to the default version through site-root links (`/docs/entity`, `/component/...`), which often 404 there because the default is the latest release. ## Links stay in their version - 124 site-root links in `docs`, `component`, `base`, `shell` (en and zh-CN) now link the `.md` file relatively. zh-CN pages that linked English Base pages now link the Chinese ones. - `remarkDocLinks` prefixes any remaining site-root docs link with the build's base, so released snapshots (v0.6.6 still has such links) stay inside `/versions/<tag>` too. - Redirect destinations (`/docs/dock` → `/component/dock`, …) and the zh-CN → en language switch kept dropping the version base; both are fixed. - The 404 page, which GitHub Pages serves for every missing path, looks for the same page in the other versions and offers it ("This page is not in the v0.6.6 (latest) documentation, but it exists in main."). It also now loads the site stylesheet; it was unstyled before. ## GPUI snapshot version in one place Pages write `{{gpui_pre_version}}` in prose, code and docs.rs links. It resolves from the `gpui` entry of the workspace `Cargo.toml`; `script/build-website-versions` passes each revision's own value. The `.md` endpoints and `llms-full.txt` expand it as well. ## Maturity labels `maturity: [preview | experimental | showcase-only | platform-dependent | stable]` in frontmatter renders mono labels under the title, linking to a new Maturity section on the docs home. Marked: Mobile and WebView (Experimental, Platform-dependent), WebAssembly (Showcase only: it currently serves the component showcases, not shipped applications), all GPUI Shell pages (Preview), Native Extensions, SystemNotification and TitleBar (Platform-dependent). Unmarked pages are Stable. ## Checks - `bun run test:docs` (sources, no build): no site-root docs links; relative links resolve, in the same locale when possible; no hand-written pinned `gpui-pre` version or versioned docs.rs gpui-pre URL; known variables only; en/zh-CN maturity match. - `bun run test:links`: every `<a>` and redirect in a build reaches a built page; `test:versioned-examples` repeats it at `/versions/test/`, where leaving the version fails. - Test Docs now also runs on `Cargo.toml` changes. Verified locally with a production-shaped build (v0.6.6 at `/`, this branch at `/versions/main`): all website tests pass, the v0.6.6 snapshot passes the link check at `/versions/v0.6.6/`, and the 404 fallback works for `/docs/entity` and `/zh-CN/docs/entity`. Publishing a release that contains the new GPUI guides is what makes `/docs/` show them; this PR does not change which version is the default. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> | 28 天前 | |
shell: Let a script textarea submit on Enter (#3550) ## Description In a script's `TextareaState`, every Enter inserts a line break and then reports `submit` with `shift: false`. Base emits `InputEvent::PressEnter` for every Enter, including one it has just turned into a line break, and the shell maps that to `submit`. A message box that sends on Enter therefore reads the text with the new break in it: at the end when the caret was at the end, in the middle when it was not. Base's `InputBaseState::set_submit_on_enter` exists for this, but the script state did not expose it. `TextareaState` (from `gpui-base`) gains `set_submit_on_enter(submit)`, which passes through to base. With it on: - Enter inserts nothing and reports `submit` with `shift: false`. - Shift+Enter still inserts a line break, and is still reported as `submit`, with `shift: true`, because base emits it. The shell does not filter it, since a script may want it; the declaration and the docs say to return early when `event.shift` is set. The default stays off, so existing scripts behave as before. The single-line `InputState` does not need it: Enter there already only submits. The `gpui-component` catalog's `TextareaState` exposes only the shared text methods (no `set_rows` or `set_soft_wrap` either), so it is not changed here. ## Public API ### Script (`gpui-kit.d.ts`, `gpui-base` `TextareaState`) ```ts set_submit_on_enter(submit: boolean): void; ``` ## How to Test ```sh cargo test -p gpui-shell --lib -- a_textarea_that_submits_on_enter ``` `a_textarea_that_submits_on_enter_keeps_the_line_break_for_shift_enter` mounts a script whose textarea has `set_submit_on_enter(true)` and whose `submit` handler records the text it reads. It types `send` and presses Enter (the handler reads `send`), presses Shift+Enter (a break is inserted, and `submit` arrives with `shift: true`), then types `two` and presses Enter (the handler reads `send`, a break, `two`). The test sends base's `Enter` action rather than the key. When nothing handles a key, GPUI's test window types the key's character, and it gives Enter the character `\n`; the macOS, Windows and Linux windows type nothing for Enter in that case. Base passes a submitting Enter on to the parent, so with the key the test window would put the break back, which a real window does not. The test fails when the setting is not passed to base: the handler reads `send` followed by a break, with `shift: false`, which is the behavior before this change. AI-assisted: written with Claude Code and reviewed by me before submission. ## Checklist - [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and followed the guidelines. - [x] Reviewed the changes in this PR and confirmed AI generated code (If any) is accurate. - [ ] Passed `cargo run` for story tests related to the changes. (No story changes.) - [x] Tested macOS, Windows and Linux platforms performance (if the change is platform-specific). (Not platform-specific.) | 7 天前 | |
shell: Fix the runtime gaps found by the gpui-shell benchmark apps (#3505) ## Summary [gpui-shell-benchs](https://github.com/huacnlee/gpui-shell-benchs) builds real applications on gpui-shell 0.7.3 with AI coding agents, and its README records the runtime problems those agents hit and worked around. This PR fixes all twelve. | # | Problem | Fix | | - | ------- | --- | | 1 | Manifest `fs` roots outside the app were ignored | `local_capabilities` replaced the manifest's roots with the app and data directories; it now adds them. New `${homeDir}` placeholder. `$HOME`, `${HOME}`, `$USERPROFILE`, `%USERPROFILE%` and `~` are refused with the `${homeDir}` spelling to use, instead of silently becoming a relative directory | | 2 | No home directory, environment, data directory or `import.meta.url` | `os.homedir()`, `process.dataDir`, `import.meta.url`, and `process.env` (see below) | | 3 | No `TextEncoder` / `TextDecoder` | Both added as globals (UTF-8, `fatal`, `ignoreBOM`, `stream`) | | 4 | `window.open_dialog` / `window.push_toast` threw "needs a ShellRoot" | The CLI window's first view is a component `Root` with the `ShellRoot` inside it; `ShellRoot` is now found there too. This also fixes tooltips and the reload-failure toast in CLI windows. Calls from `init()` are still refused (the window has no root yet), now with an error that says so | | 5 | `gpui-component` needed `new Type(...)` while the guide said `Type.new(...)` | Every `gpui-component` export also accepts `Type.new(...)`; the old forms keep working | | 6 | Component `InputState` had no change or submit events | It gains `on(event, handler)` with the same events and payloads as `gpui-base`'s. A `gpui-base` state passed to a component `Input` is now refused instead of possibly binding an unrelated state | | 7 | No pinch gesture | `on_pinch(handler)`, forwarding GPUI's `PinchEvent` (all three desktop platforms emit it) | | 8 | `run -d` stored data under `<dir>-<hash>` | Data is keyed by the manifest `id` whenever there is a manifest, so development and installed copies share it | | 9 | `fs/promises` lacked `stat`, `rename`, `readlink`; `fetch` lacked `arrayBuffer()` | `stat`, `lstat`, `rename`, `readlink`; `Response.arrayBuffer()` and `bytes()` | | 10 | `fetch` sent no `User-Agent` (GitHub API returned 403) | Default `User-Agent: gpui-shell/<version>` on fetch and WebSocket; a script's header wins | | 11 | `flex_shrink()` failed with "expected a number, got nil" | GPUI's `flex_shrink` takes an `f32` too, so the argument stays required; the error now names the method and lists `flex_shrink_0()` / `flex_shrink_1()` | | 12 | `line_height(n)` is a multiplier while other lengths are pixels | Kept as is: a unitless `line_height(1.4)` is the same as CSS's `line-height: 1.4` and Rust's `relative(1.4)`. The docs now say so and show `"18px"` for a fixed height | ### `process.env` The environment can hold secrets handed to the host, so `process.env` shows only: - the defaults in [`crates/shell/src/default-env`](https://github.com/longbridge/gpui-kit/blob/shell/bench-fixes/crates/shell/src/default-env), one name per line: user, machine, locale, preferred tools (including `PATH`), desktop session, proxies, and standard directories. A test refuses a duplicate, a lowercase spelling, or a name that looks like a credential; - plus the names listed in the new `capabilities.process.env`. Names match regardless of case, so `HTTP_PROXY` also shows `http_proxy`, under the spelling the environment uses. Paths (`os.homedir()`, `process.dataDir`, `import.meta.url`) need no grant: reading or writing there is still the `fs` grant's decision. ### `fs/promises` access rules `stat`, `lstat` and `readlink` need read access; `rename` needs write access at both ends. `stat` does not follow a link out of a granted root. `readlink` returns the stored target text as Node does, without following it. ### Getting Started: install first `website/shell/getting-started.md` (and zh-CN) now opens with installing `gpui-shell`, ahead of embedding the runtime in Rust: a Rust host still needs the binary for `check`, `types` and `agents`. It adds building the latest `main` from Git: ```bash cargo install --git https://github.com/longbridge/gpui-kit gpui-shell --locked ``` The page's commands use the installed `gpui-shell` instead of `cargo run -p gpui-shell --`, and `crates/shell/README.md` mentions the Git install too. ## Public API ### gpui-shell (Rust) - `Capabilities::env_variables(self, names: impl IntoIterator<Item = String>) -> Self`: environment variables `process.env` shows beyond the defaults. - `Policy::with_data_dir(self, path: impl Into<PathBuf>) -> Self`: the data directory `process.dataDir` reports. - `Policy::data_dir(&self) -> Option<&Path>`: reads it. - `StateDescriptor::with_input_events::<M: InputModeKind>(self) -> Self`: gives a registered state `on(event, handler)` with the `gpui-base` input events. - `StateDescriptor::has_input_events(&self) -> bool`: whether it does. - `RetainedStateStore::attach(&mut self, handle: u64, subscription: gpui::Subscription) -> anyhow::Result<()>`: keeps a subscription alive as long as the state. ### Manifest (`gpui-shell.json`) - `${homeDir}` placeholder in `capabilities.fs.read` / `write`. - `capabilities.process.env: string[]`: extra environment variables to expose. ### JavaScript / TypeScript - `os.homedir(): string` - `process.env: Record<string, string | undefined>` (read-only view) - `process.dataDir: string | undefined` - `import.meta.url: string` - `class TextEncoder { encoding; encode(input?): Uint8Array; encodeInto(source, dest): { read, written } }` - `class TextDecoder { constructor(label?, { fatal?, ignoreBOM? }); decode(input?, { stream? }): string }` - `fs/promises`: `stat(path): Promise<Stats>`, `lstat(path): Promise<Stats>`, `rename(from, to): Promise<void>`, `readlink(path): Promise<string>`, and `interface Stats` - `Response.arrayBuffer(): Promise<ArrayBuffer>`, `Response.bytes(): Promise<Uint8Array>` - `Element.on_pinch(handler: (event: PinchEvent, cx) => void)` and `interface PinchEvent { delta; phase; position; local_position?; bounds?; modifiers }` - `gpui-component`: `Type.new(...)` for every component and state export - `gpui-component` `InputState.on(event: "change" | "submit" | "focus" | "blur", handler)` ## Breaking Changes `gpui-shell run -d <dir>` with a manifest now stores data under the manifest `id`, the same place the installed copy uses, instead of `<dir>-<hash>`. Data written by earlier development runs stays in the old directory. `process.env` was absent before; scripts that feature-detected it with `typeof process.env === "undefined"` now see an object. `Capabilities`' read and write roots for a local run now include the manifest's roots rather than only the app and data directories, so `gpui-shell list` reports a wider grant for manifests that ask for one. ## Validation - `cargo test -p gpui-shell`: 890 unit, 116 integration and 4 doc tests pass. - `cargo clippy -p gpui-shell --all-targets -- --deny warnings` and `cargo fmt --check` are clean. - `gpui-shell check` passes for `examples/js_todolist` and `examples/js_dock`. - `cargo install --git https://github.com/longbridge/gpui-kit gpui-shell --locked` installs from `main` (verified into a scratch `--root`). - Not done: running the benchmark apps against this branch and removing their workarounds; pinch on real hardware (covered by a simulated `PinchEvent` test); `typos` (not installed locally). The code and docs in this PR are AI-generated (Claude Code), reviewed and directed by a human. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> | 10 天前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 5 天前 | ||
| 9 天前 | ||
| 28 天前 | ||
| 11 天前 | ||
| 10 天前 | ||
| 11 天前 | ||
| 11 天前 | ||
| 9 天前 | ||
| 28 天前 | ||
| 10 天前 | ||
| 9 天前 | ||
| 9 天前 | ||
| 10 天前 | ||
| 28 天前 | ||
| 7 天前 | ||
| 10 天前 |