f2f1cd9b6c
- 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.
125 lines
4.4 KiB
JavaScript
125 lines
4.4 KiB
JavaScript
/**
|
|
* @file file-manager.test.js
|
|
* @description server/lib/file-manager.js: the files a file manager's Copy
|
|
* put on the OS clipboard (Windows GetFileDropList, macOS NSPasteboard, Linux
|
|
* text/uri-list / gnome-copied-files) and the ones selected in it — what a
|
|
* drag carries (Windows Shell.Application, macOS Finder) — kept only when
|
|
* they exist here, with their sizes; a missing reader is no files.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
const { describe, it, before, after, afterEach } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const {
|
|
clipboardFiles,
|
|
selectedFiles,
|
|
uriListPaths,
|
|
__setExecImpl,
|
|
} = require("../lib/file-manager");
|
|
|
|
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), "fm-files-"));
|
|
const big = path.join(TMP, "big file.mp4");
|
|
const dir = path.join(TMP, "folder");
|
|
|
|
before(() => {
|
|
fs.writeFileSync(big, Buffer.alloc(1234));
|
|
fs.mkdirSync(dir);
|
|
});
|
|
after(() => fs.rmSync(TMP, { recursive: true, force: true }));
|
|
afterEach(() => __setExecImpl(async () => ""));
|
|
|
|
describe("uriListPaths", () => {
|
|
it("decodes file URIs and skips a gnome list's verb, comments and other hosts", () => {
|
|
assert.deepEqual(
|
|
uriListPaths("copy\nfile:///home/u/a%20b.png\r\n# c\nfile://other/x\nhttps://x/y\n"),
|
|
["/home/u/a b.png"]
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("clipboardFiles", () => {
|
|
it("reads Windows' file drop list with powershell -Sta", async () => {
|
|
let seen;
|
|
__setExecImpl(async (cmd, args) => {
|
|
seen = [cmd, args];
|
|
return `${big}\r\n${dir}\r\n${path.join(TMP, "gone.txt")}\r\n`;
|
|
});
|
|
assert.deepEqual(await clipboardFiles("win32"), [
|
|
{ path: big, size: 1234, dir: false },
|
|
{ path: dir, size: null, dir: true },
|
|
]);
|
|
assert.equal(seen[0], "powershell");
|
|
assert.ok(seen[1].includes("-Sta"));
|
|
assert.match(seen[1].at(-1), /GetFileDropList/);
|
|
});
|
|
|
|
it("reads macOS file URLs through JXA", async () => {
|
|
let seen;
|
|
__setExecImpl(async (cmd, args) => {
|
|
seen = [cmd, args];
|
|
return `${big}\n`;
|
|
});
|
|
assert.deepEqual(await clipboardFiles("darwin"), [{ path: big, size: 1234, dir: false }]);
|
|
assert.equal(seen[0], "osascript");
|
|
assert.deepEqual(seen[1].slice(0, 2), ["-l", "JavaScript"]);
|
|
});
|
|
|
|
it("reads Linux text/uri-list with the desktop's display", async () => {
|
|
const saved = process.env.DISPLAY;
|
|
process.env.DISPLAY = ":0";
|
|
try {
|
|
const calls = [];
|
|
__setExecImpl(async (cmd, args, env) => {
|
|
calls.push([cmd, args.join(" "), env.DISPLAY]);
|
|
return cmd === "xclip" ? `file://${encodeURI(big)}\n` : "";
|
|
});
|
|
assert.deepEqual(await clipboardFiles("linux"), [{ path: big, size: 1234, dir: false }]);
|
|
assert.deepEqual(calls.at(-1), ["xclip", "-selection clipboard -t text/uri-list -o", ":0"]);
|
|
} finally {
|
|
if (saved === undefined) delete process.env.DISPLAY;
|
|
else process.env.DISPLAY = saved;
|
|
}
|
|
});
|
|
|
|
it("answers no files when the reader is missing or prints relative paths", async () => {
|
|
__setExecImpl(async () => "");
|
|
assert.deepEqual(await clipboardFiles("win32"), []);
|
|
__setExecImpl(async () => "relative/x.txt\n");
|
|
assert.deepEqual(await clipboardFiles("darwin"), []);
|
|
});
|
|
|
|
it("reads Explorer's selection (and the desktop's) for a drop, once per path", async () => {
|
|
let seen;
|
|
__setExecImpl(async (cmd, args) => {
|
|
seen = [cmd, args];
|
|
return `${big}\r\n${big}\r\n`;
|
|
});
|
|
assert.deepEqual(await selectedFiles("win32"), [{ path: big, size: 1234, dir: false }]);
|
|
assert.equal(seen[0], "powershell");
|
|
assert.match(seen[1].at(-1), /Shell\.Application[\s\S]*SelectedItems[\s\S]*FindWindowSW/);
|
|
});
|
|
|
|
it("reads Finder's selection on macOS, a folder with its trailing slash", async () => {
|
|
let seen;
|
|
__setExecImpl(async (cmd, args) => {
|
|
seen = [cmd, args];
|
|
return `${dir}/\n`;
|
|
});
|
|
assert.deepEqual(await selectedFiles("darwin"), [{ path: `${dir}/`, size: null, dir: true }]);
|
|
assert.equal(seen[0], "osascript");
|
|
assert.ok(seen[1].includes("repeat with a in (selection as alias list)"));
|
|
});
|
|
|
|
it("has no selection to read on Linux", async () => {
|
|
let called = false;
|
|
__setExecImpl(async () => {
|
|
called = true;
|
|
return big;
|
|
});
|
|
assert.deepEqual(await selectedFiles("linux"), []);
|
|
assert.equal(called, false);
|
|
});
|
|
});
|