Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,21 @@ builds still require the [Tauri prerequisites](https://v2.tauri.app/start/prereq
See [contributing](docs/contributing.md) for exact pins, registry settings,
and the pinned pnpm package's Intel Mac limitation.

Browser servers prefer port 1430 and automatically use the next open port, so
`just web` can run from multiple worktrees. `just desktop` requires port 1430
because its native window uses that fixed development URL.
Both commands forward arguments to their development tool (Vite or Tauri).
Select a port (default: 1430) with `just web --port 1431` or
`just desktop --port 1432`. Browser servers prefer the requested port and
use the next open port automatically; desktop requires the exact port to be free and keeps
Vite and the native window on the same URL. To run multiple desktop copies,
use a different port in each terminal/worktree:

```sh
just desktop --port 1430
# In another terminal/worktree:
just desktop --port 1431
```

Ports do not isolate account credentials or native plugin data. For separate
plugin profiles, use the existing `BUZZODZ_PROFILE` setting described below.
Without live opt-in they run the shell without relay identity access.
`just iterate` applies formatting and runs fast checks plus the frontend build.
`just scan` adds tests and native checks. [PR CI](.github/workflows/ci.yml) runs
Expand Down Expand Up @@ -65,7 +77,8 @@ in the non-live shell/fixture state.
```

Open the Local URL printed by `just web`; parallel worktrees may use a port
above 1430. Stop the process using 1430 before starting `just desktop`.
above the requested port. If 1430 is busy, use `just desktop --port 1431`
(or another free port) instead of stopping the other copy.

The broker reads the existing Keychain credential only after validating the
public pin, refuses mismatches and never falls back to another credential. If it
Expand Down
26 changes: 20 additions & 6 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,20 @@ The pinned pnpm Hermit package supports Apple Silicon macOS but marks Intel macO
on an unsupported platform; resolve that tooling gap first. Other platforms still
need their own validation.

- `just web`: install locked dependencies and start Vite on port 1430 or the
next available port, allowing parallel browser development across worktrees.
- `just desktop`: install locked dependencies and start Tauri, which starts Vite.
- `just web [args...]`: install locked dependencies and forward arguments to Vite,
e.g. `just web --port 1431 --host 127.0.0.1`. Vite uses the requested port
(default: 1430) or the next available port, allowing parallel browser development.
- `just desktop [args...]`: install locked dependencies and forward arguments to
Tauri, e.g. `just desktop --port 1431 --no-watch`. The desktop adapter consumes
`--port N` or `--port=N` to set both Vite's port and Tauri's development URL;
Tauri's own `--port` is for its static-file server, not Vite. Without this flag,
the existing Tauri configuration is unchanged (port 1430). Desktop requires the
exact port to be free; an occupied port fails rather than opening another copy's
server. Other arguments, including runner/application arguments after `--`, pass
through unchanged. Port configuration is prepended so Tauri parses it even with
implicit runner arguments. Explicit `--config` arguments merge afterward and can
override it; keep their development URL and frontend command consistent. Use `--`
before runner/application arguments if they contain their own `--port` flag.
- To pause notifications in your local dev server, set `BUZZ_DEV_NOTIFICATIONS=0`
in `.env.local` and restart the server. Only `0` pauses alerts and permission
requests; removing the setting restores normal behavior. Saved preferences are
Expand All @@ -70,9 +81,12 @@ isolated test buses, never use the desktop session bus or display real banners.
Installs run on every invocation to account for branch and lockfile changes.
pnpm reuses its shared package cache; no node_modules directory needs to be copied
into a new worktree. Native dependencies are fetched by Cargo as needed. Initial
downloads and native compilation can take time. Desktop dev requires port 1430
for its fixed native development URL. Browser dev prints its selected URL and can
use a later port when 1430 is occupied.
downloads and native compilation can take time. For parallel copies, run
`just desktop --port 1430` and `just desktop --port 1431` in separate
terminals/worktrees, or choose other free ports. Ports must be integers from 1 to 65535. Browser dev
prints its selected URL and can use a later port when the requested port is
occupied. Port selection does not isolate credentials or native plugin data;
use the existing `BUZZODZ_PROFILE` setting for separate plugin profiles.
Both run the development broker with your identity when the public
`BUZZ_DEV_VIEWER` pin is configured in `.env.local`, and start without live
identity otherwise; see [the setup and Keychain requirements](../README.md#relay-channels).
Expand Down
14 changes: 8 additions & 6 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ default:
install:
pnpm install --frozen-lockfile

# Run the shared frontend in a browser.
web: install
pnpm dev
# Run the shared frontend in a browser; forward Vite arguments (e.g. --port 1431).
[positional-arguments]
web *args: install
pnpm dev "$@"

# Run the shared frontend in Tauri (requires native toolchain).
desktop: install
pnpm tauri dev
# Run Tauri; --port selects Vite's port, other arguments pass through to Tauri.
[positional-arguments]
desktop *args: install
node scripts/desktop-dev.mjs "$@"

# Run buzzodz; forward arguments unchanged (Rust required).
[positional-arguments]
Expand Down
46 changes: 46 additions & 0 deletions scripts/desktop-dev.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { spawnSync } from "node:child_process";

const args = process.argv.slice(2);
const forwarded = [];
let port;
let index = 0;
for (; index < args.length && args[index] !== "--"; index++) {
const arg = args[index];
if (arg === "--port" || arg.startsWith("--port=")) {
const value =
arg === "--port" ? args[++index] : arg.slice("--port=".length);
if (
!/^[0-9]+$/.test(value ?? "") ||
Number(value) < 1 ||
Number(value) > 65535
) {
console.error("--port must be an integer between 1 and 65535.");
process.exit(1);
}
port = Number(value);
} else {
forwarded.push(arg);
}
}

if (port !== undefined) {
// Tauri's own --port controls its static-file server, not our Vite server.
// Prepend: Tauri treats everything after a bare positional as runner args.
forwarded.unshift(
"--config",
JSON.stringify({
build: {
devUrl: `http://localhost:${port}`,
beforeDevCommand: `pnpm dev:desktop --port ${port}`,
},
}),
);
}
// Runner/application arguments after -- belong to Tauri, including any --port.
forwarded.push(...args.slice(index));
const result = spawnSync("pnpm", ["tauri", "dev", ...forwarded], {
stdio: "inherit",
});
if (result.error) console.error(result.error.message);
if (result.signal) process.kill(process.pid, result.signal);
process.exit(result.status ?? 1);
73 changes: 67 additions & 6 deletions src-tauri/src/terminal/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,26 @@ fn context() -> TerminalContext {
relay_url: "wss://buzz.example.com".into(),
}
}
fn shell() -> CommandBuilder {
// portable-pty 0.9 uses non-reentrant getpwuid when constructing a command
// without inherited SHELL, and when spawning without explicit HOME/SHELL.
// Initialize once, then clone a complete fixture environment so parallel
// tests never share those password-database buffers or inherit user config.
static SHELL: std::sync::OnceLock<CommandBuilder> = std::sync::OnceLock::new();
SHELL
.get_or_init(|| {
let mut cmd = CommandBuilder::new("/bin/sh");
cmd.env_clear();
cmd.env("PATH", "/usr/bin:/bin");
cmd.env("HOME", "/");
cmd.env("SHELL", "/bin/sh");
cmd
})
.clone()
}
fn script(script: &str) -> CommandBuilder {
let mut cmd = CommandBuilder::new("/bin/sh");
let mut cmd = shell();
cmd.args(["-c", script]);
cmd.env_clear();
cmd.env("PATH", "/usr/bin:/bin");
cmd
}
fn read_all(state: &Terminals, owner: &str, id: &str) -> Vec<u8> {
Expand All @@ -37,6 +52,54 @@ fn read_all(state: &Terminals, owner: &str, id: &str) -> Vec<u8> {
}
}

#[test]
fn parallel_fixture_environment_probe() {
if std::env::var("BUZZ_TERMINAL_FIXTURE_PROBE").as_deref() != Ok("1") {
return;
}
let barrier = std::sync::Barrier::new(8);
std::thread::scope(|scope| {
for _ in 0..8 {
let barrier = &barrier;
scope.spawn(move || {
barrier.wait();
let cmd = script("printf '%s|%s|%s' \"$HOME\" \"$SHELL\" \"$PWD\"");
assert_eq!(cmd.get_env("HOME"), Some(std::ffi::OsStr::new("/")));
assert_eq!(cmd.get_env("SHELL"), Some(std::ffi::OsStr::new("/bin/sh")));
let state = Terminals::default();
let owner = state.create_owner().unwrap();
let id = state.spawn_with(&owner, |_| Ok(cmd), 80, 24).unwrap();
let output = read_all(&state, &owner, &id);
state.close_owner(&owner).unwrap();
assert_eq!(output, b"/|/bin/sh|/");
});
}
});
}

#[test]
fn parallel_fixtures_work_without_parent_home_or_shell() {
// A fresh process exercises first-use construction without mutating the
// environment shared by the other tests or pre-initializing the template.
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"terminal::tests::parallel_fixture_environment_probe",
"--nocapture",
])
.env("BUZZ_TERMINAL_FIXTURE_PROBE", "1")
.env_remove("HOME")
.env_remove("SHELL")
.output()
.unwrap();
assert!(
output.status.success(),
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}

#[test]
fn production_environment_probe() {
if std::env::var("BUZZ_TERMINAL_PROBE").as_deref() != Ok("1") {
Expand Down Expand Up @@ -322,10 +385,8 @@ fn foreground_job_receives_ctrl_c_and_close_stops_job_control_group() {
.spawn_with(
&owner,
|_| {
let mut cmd = CommandBuilder::new("/bin/sh");
let mut cmd = shell();
cmd.arg("-i");
cmd.env_clear();
cmd.env("PATH", "/usr/bin:/bin");
Ok(cmd)
},
80,
Expand Down
5 changes: 5 additions & 0 deletions tests/browser/agent-activity.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ for (const mode of ["light", "dark"]) {
path: testInfo.outputPath(`activity-entry-${mode}-${width}.png`),
});
await page.keyboard.press("Escape");
// Escape starts Base UI's asynchronous unmount. The closing portal still
// has its wide-screen position and can overflow the next narrow viewport.
await expect(
page.getByRole("tooltip", { includeHidden: true }),
).toHaveCount(0);
}

await entry.click();
Expand Down
Loading
Loading