Files
AgentHub/server/lib/desktop-env.js
T
nntrivi2001 f2f1cd9b6c fix: per-user Today, native paste and drop on a local machine
- Today's spend in the status bar and the Claude Code statusline is the
  caller's own (`GET /api/pricing/cost?mine=1`, scope.ownSessionWhere), not
  every user's sum for an admin or analyst.
- A screenshot pasted into a local claude pane is sent as Claude Code's own
  image-paste key: Alt+V on Windows (Ctrl+V did nothing there), ^V elsewhere,
  and only when the machine can read its clipboard (wl-paste/xclip + a
  display on Linux), else it is uploaded (GET /api/run/native-paste {key}).
- A file manager's files pasted or dropped on a local browser go in by their
  own paths, read from the OS clipboard or the file manager's selection
  (server/lib/file-manager.js, POST /api/run/clipboard-files and
  /selected-files): no upload, no size limit. A copied image file is pasted
  by path, text that comes with a picture of it (spreadsheet cells) is
  pasted as text, Windows paths go in as `C:/…` mentions.
- Tab titles for Snapshot, Artifacts, Files and Administration.
- An npm tool AgentHub installed under ~/.local is updated there, not by its
  own updater into npm's global prefix.
2026-10-26 11:18:05 +07:00

86 lines
3.1 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;
}
/**
* Whether a CLI in a pane started now can read this desktop's clipboard — what
* a native image paste (`^V`, the terminal's `lib/nativePaste.ts`) relies on.
* On Linux Claude Code shells out to `wl-paste` (Wayland) or `xclip` (X11);
* neither ships with a stock Ubuntu, and with neither — or with no display to
* reach — the `^V` pastes nothing at all, so the browser must upload instead.
* macOS and Windows read their clipboard with built-in tools.
*/
function canReadClipboard({
platform = process.platform,
env = null,
which = (name) => require("./ai-tools").which(name),
} = {}) {
if (platform !== "linux") return true;
const e = env || desktopEnv();
return !!((e.WAYLAND_DISPLAY && which("wl-paste")) || (e.DISPLAY && which("xclip")));
}
module.exports = { desktopEnv, canReadClipboard, FORWARDED, __setExecImpl };