Files
AgentHub/server/lib/desktop-env.js
T
nntrivi2001 f77049c238 feat(run,ui): make the lane console usable — terminal focus, live-exit recovery, auto permission mode, model aliases
Terminal (issue #33):
- `convertEol` is gone from xterm. It rewrote every bare LF as CR+LF, so a
  tmux pane that scrolled (tmux emits a bare LF and keeps drawing at the
  column it was on) had the rest of that draw land at column 0, stamping a
  line's first characters into the left margin of the rows below — the torn
  left edge that only showed up once content scrolled.
- The attach carries the client's real size (`?cols=&rows=`, `sizeFromQuery`
  in `server/websocket.js`, bounded because it reaches a TIOCSWINSZ ioctl).
  `attach()` spawned `tmux attach-session` at a hardcoded 80x24 on every
  attach, and tmux sizes the window to the newest client, so `claude` redrew
  its whole UI at 80 columns until the first `resize` message landed.
- The terminal takes the keyboard on mount, so typing reaches tmux without
  clicking the pane first.
- A pane that ends (double Ctrl+C, `exit`, a crash) reports itself through a
  new `onExit` prop and `LaneConsolePane` runs `newRun()`, so the console
  returns to the setup form instead of leaving a dead pane behind a
  "New run" button.

Pane environment:
- `server/lib/desktop-env.js` forwards the graphical session's DISPLAY /
  WAYLAND_DISPLAY / XAUTHORITY / XDG_RUNTIME_DIR (from
  `systemctl --user show-environment`, this process's own values winning)
  into the pane. The server normally runs as a systemd user service with none
  of them, so `claude` there could not read the clipboard and answered "no
  image found in clipboard" on every image paste.

Run setup:
- `auto` is a permission mode and is now the default everywhere — allowlists,
  the `--permission-mode` fallback, the select, and the pane's initial state.
- The model picker sends the CLI's aliases (`fable`/`opus`/`sonnet`/`haiku`)
  instead of pinned ids. `claude --model` resolves an alias to the latest
  model itself, so a release like Fable 5.1 needs no edit here; pricing is
  unaffected because it keys off the resolved id the transcript reports.
- **Create/update readme file** moved from the Add-lane modal into the lane's
  Tools menu: creating a lane and writing its docs are separate decisions,
  and the second is worth re-running long after the first.
- The Workspace's no-lane state is a real banner; the lane strip (add tile
  included) renders only once a lane exists.

Refs #32, #33.

Tests: npm run test:server (1828 pass), client vitest (558 pass).
2026-09-21 17:03:27 +07:00

68 lines
2.3 KiB
JavaScript

/**
* @file desktop-env.js
* @description The graphical session's own environment variables, for panes
* the dashboard spawns. The server usually runs as a systemd *user* service,
* which has no `DISPLAY`/`WAYLAND_DISPLAY` of its own — so neither does any
* tmux pane it starts, and a `claude` running in one cannot reach the X or
* Wayland clipboard: pasting an image answers "no image found in clipboard"
* while the same CLI in a normal terminal pastes fine. `systemctl --user
* show-environment` is the canonical local source for those values (the
* desktop session imports them there at login), with this process's own
* environment preferred when it already has them — a checkout started from a
* terminal inherits the real ones.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { execFileSync } = require("node:child_process");
/** The only names forwarded. A pane gets a clipboard, not the whole session. */
const FORWARDED = ["DISPLAY", "WAYLAND_DISPLAY", "XAUTHORITY", "XDG_RUNTIME_DIR"];
let execImpl = (args) =>
execFileSync("systemctl", args, {
encoding: "utf8",
timeout: 2000,
stdio: ["ignore", "pipe", "ignore"],
});
/** Test seam — same pattern as tmux.js. */
function __setExecImpl(fn) {
execImpl = fn;
}
/** `KEY=value` lines from `systemctl --user show-environment`, as a map. */
function systemdUserEnv() {
let out;
try {
out = execImpl(["--user", "show-environment"]);
} catch {
// No systemd user manager (macOS, a container, a plain login shell) — the
// process environment below is the only source then.
return {};
}
const env = {};
for (const line of String(out).split("\n")) {
const eq = line.indexOf("=");
if (eq > 0) env[line.slice(0, eq)] = line.slice(eq + 1);
}
return env;
}
/**
* The forwarded variables that actually have a usable value right now.
* A value carrying a newline is dropped rather than passed on: these end up in
* a `tmux -e NAME=value` argv, and a line break there is not a value tmux can
* represent.
*/
function desktopEnv() {
const fromSystemd = systemdUserEnv();
const out = {};
for (const name of FORWARDED) {
const value = process.env[name] || fromSystemd[name];
if (typeof value === "string" && value && !/[\r\n\0]/.test(value)) out[name] = value;
}
return out;
}
module.exports = { desktopEnv, FORWARDED, __setExecImpl };