@vellumai/assistant 0.12.2-dev.202609191615.99620e2 → 0.12.2-dev.202609210132.d52247f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/desktop-browser-cli.md +18 -0
- package/package.json +1 -1
- package/scripts/smoke-desktop-window-theme.ts +2 -3
- package/src/desktop/desktop-browser-endpoint.ts +3 -0
- package/src/desktop/desktop-dependencies.ts +94 -35
- package/src/desktop/desktop-panel-config.test.ts +13 -3
- package/src/desktop/desktop-panel-config.ts +109 -15
- package/src/desktop/desktop-session-manager.test.ts +5 -2
- package/src/desktop/desktop-session-manager.ts +7 -10
- package/src/desktop/desktop-window-theme.ts +5 -25
- package/src/workspace/migrations/157-add-desktop-files-launcher.ts +94 -0
- package/src/workspace/migrations/__tests__/156-extract-workspace-mcp-json.test.ts +4 -10
- package/src/workspace/migrations/__tests__/157-add-desktop-files-launcher.test.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -16,6 +16,10 @@ flowchart LR
|
|
|
16
16
|
Chrome --> Stream[Existing desktop stream]
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
The Terminal dock launcher opens [WezTerm](https://wezterm.org/), with a plain dark native title bar, minimize/maximize/close controls, a new-tab button and split panes. Use Ctrl+Shift+T for a tab, Ctrl+Shift+D to split side by side, and Ctrl+Shift+E to split top and bottom. The desktop seeds an editable `data/desktop-panel/wezterm/wezterm.lua` configuration with software rendering and a dark theme. The managed launcher keeps its stable `data/desktop-panel/applications/xterm.desktop` path so existing dock pins remain valid. Setup installs an exact-version, SHA-256-verified WezTerm package for Linux x64 or ARM64, plus its rendering libraries.
|
|
20
|
+
|
|
21
|
+
Chrome and WezTerm use explicit desktop window classes shared with their launcher entries. The desktop session bus and Plank share `XDG_DATA_HOME` so the D-Bus-activated window matcher can resolve the generated launchers and group running windows under their pinned icons.
|
|
22
|
+
|
|
19
23
|
Chrome and its dock launcher share the managed profile and loopback debug port. For Chrome reopened from the dock, the profile singleton lock identifies a candidate PID, validated against the installed executable, profile and loopback debug arguments. Discovery checks `/proc` socket ownership against that browser PID, refuses redirects and validates the returned browser WebSocket endpoint. The connection stays within the container. No CDP endpoint or token is exposed to the renderer.
|
|
20
24
|
|
|
21
25
|
The desktop client bypasses personal-browser discovery, extension reconnect waits and backend fallback. It reuses the existing AX snapshot, DOM element resolution, mouse, keyboard, extraction and credential-fill implementations. Operation-scoped clients borrow the lease's connection; disposing one does not release the lease. `detach`, `close`, turn completion, cancellation, errors and idle expiry release control. Turn completion clears the activity indicator immediately and queues browser cleanup before the next automation session; it leaves Chrome and the current page open for the user. Only `tabs close` closes a Chrome tab.
|
|
@@ -54,3 +58,17 @@ Browser commands, user handoff, errors, and interruption require a fresh
|
|
|
54
58
|
observation before returning to native input. Browser commands and computer use
|
|
55
59
|
share session ownership, cancellation, and the user-help reservation. Use
|
|
56
60
|
browser commands for page-level operations and computer use for desktop UI.
|
|
61
|
+
|
|
62
|
+
## File explorer
|
|
63
|
+
|
|
64
|
+
Open **Files**, between Chrome and Terminal in the virtual desktop dock, to browse the assistant workspace in
|
|
65
|
+
Thunar, with a Workspace sidebar shortcut and a dark theme. It supports tabs,
|
|
66
|
+
split views, search, hidden files, drag and drop,
|
|
67
|
+
copy/paste, bulk renaming, and file properties. GVfs provides Trash support and
|
|
68
|
+
Tumbler provides thumbnails. Use Ctrl+L to enter a path, Ctrl+T for a new tab,
|
|
69
|
+
Ctrl+H to toggle hidden files, and F3 for split view.
|
|
70
|
+
|
|
71
|
+
Files manages the assistant's files. Files on your personal computer must first
|
|
72
|
+
be uploaded or otherwise copied into the assistant workspace. Existing desktops
|
|
73
|
+
receive the Files dock shortcut once during upgrade; removing or rearranging it
|
|
74
|
+
is preserved across restarts.
|
package/package.json
CHANGED
|
@@ -91,15 +91,14 @@ try {
|
|
|
91
91
|
`<bindings xmlns="http://openbox.org/3.4/rc">${mouse}${keyboard}</bindings>`,
|
|
92
92
|
);
|
|
93
93
|
const configDir = join(directory, "managed & theme");
|
|
94
|
-
const config = writeDesktopWindowTheme(configDir, home
|
|
94
|
+
const config = writeDesktopWindowTheme(configDir, home);
|
|
95
95
|
const expanded = run("xmllint", "--xinclude", config);
|
|
96
96
|
assert(
|
|
97
97
|
expanded.includes("NextWindow") && expanded.includes("Move"),
|
|
98
98
|
"Relative includes must retain the actual keyboard and mouse bindings",
|
|
99
99
|
);
|
|
100
100
|
assert.equal(await readFile(sourcePath, "utf8"), source);
|
|
101
|
-
writeDesktopWindowTheme(configDir, home
|
|
102
|
-
writeDesktopWindowTheme(configDir, home, "#53b7aa");
|
|
101
|
+
writeDesktopWindowTheme(configDir, home);
|
|
103
102
|
assert.equal(await readFile(sourcePath, "utf8"), source);
|
|
104
103
|
const blocked = join(directory, "blocked");
|
|
105
104
|
await writeFile(blocked, "not a directory");
|
|
@@ -4,6 +4,8 @@ import { join } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { shouldRestoreDesktopChromeSession } from "./desktop-chrome-session.js";
|
|
6
6
|
|
|
7
|
+
export const DESKTOP_CHROME_WINDOW_CLASS = "vellum-desktop-chrome";
|
|
8
|
+
|
|
7
9
|
export async function allocateDesktopDebugPort(): Promise<number> {
|
|
8
10
|
const server = createServer();
|
|
9
11
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -129,6 +131,7 @@ export function desktopChromeArguments(
|
|
|
129
131
|
return [
|
|
130
132
|
"--no-sandbox",
|
|
131
133
|
"--no-first-run",
|
|
134
|
+
`--class=${DESKTOP_CHROME_WINDOW_CLASS}`,
|
|
132
135
|
"--force-renderer-accessibility",
|
|
133
136
|
"--disable-dev-shm-usage",
|
|
134
137
|
...(shouldRestoreDesktopChromeSession(profileDir)
|
|
@@ -12,6 +12,17 @@ import { getLogger } from "../util/logger.js";
|
|
|
12
12
|
import { getExternalDir } from "../util/platform.js";
|
|
13
13
|
|
|
14
14
|
const log = getLogger("desktop-dependencies");
|
|
15
|
+
const WEZTERM_VERSION = "20240203-110809-5046fc22";
|
|
16
|
+
const WEZTERM_PACKAGES = {
|
|
17
|
+
x64: {
|
|
18
|
+
suffix: "Debian12.deb",
|
|
19
|
+
sha256: "d3a5c97093fbc0a87e8f9616e44efc4c9503cfc495fd1cfe4ffeff88578d15f8",
|
|
20
|
+
},
|
|
21
|
+
arm64: {
|
|
22
|
+
suffix: "Debian12.arm64.deb",
|
|
23
|
+
sha256: "351f8791c6e9561d687afa63834a2132de84800dad6e7c3d035cc6518d6c3744",
|
|
24
|
+
},
|
|
25
|
+
} as const;
|
|
15
26
|
const CHROME_VERSION = "153.0.8010.36-1";
|
|
16
27
|
const CHROME_PACKAGES = {
|
|
17
28
|
x64: {
|
|
@@ -32,17 +43,22 @@ const DESKTOP_BINARIES = {
|
|
|
32
43
|
panel: ["plank"],
|
|
33
44
|
sessionBus: ["dbus-daemon"],
|
|
34
45
|
clipboard: ["tigervncconfig", "vncconfig"],
|
|
35
|
-
terminal: ["
|
|
46
|
+
terminal: ["wezterm"],
|
|
47
|
+
fileManager: ["thunar"],
|
|
36
48
|
wallpaper: ["feh"],
|
|
37
49
|
input: ["xdotool"],
|
|
38
50
|
capture: ["scrot"],
|
|
39
51
|
} as const;
|
|
40
52
|
|
|
41
53
|
const DESKTOP_PACKAGES = [
|
|
54
|
+
"adwaita-icon-theme",
|
|
42
55
|
"at-spi2-core",
|
|
43
56
|
"dbus-x11",
|
|
44
57
|
"feh",
|
|
45
58
|
"gnome-mines",
|
|
59
|
+
"gvfs",
|
|
60
|
+
"thunar",
|
|
61
|
+
"tumbler",
|
|
46
62
|
"openbox",
|
|
47
63
|
"python3",
|
|
48
64
|
"python3-dbus",
|
|
@@ -54,11 +70,14 @@ const DESKTOP_PACKAGES = [
|
|
|
54
70
|
"xauth",
|
|
55
71
|
"xcompmgr",
|
|
56
72
|
"xfonts-base",
|
|
73
|
+
// Preserved Openbox menus and shortcuts can invoke xterm directly.
|
|
57
74
|
"xterm",
|
|
58
75
|
"xdotool",
|
|
59
76
|
"scrot",
|
|
60
77
|
"fonts-liberation",
|
|
61
78
|
"libgtk-3-0",
|
|
79
|
+
"libegl1",
|
|
80
|
+
"libgl1-mesa-dri",
|
|
62
81
|
"libvulkan1",
|
|
63
82
|
"libcurl4",
|
|
64
83
|
];
|
|
@@ -115,6 +134,12 @@ export class DesktopDependencyInstaller {
|
|
|
115
134
|
existsSync(desktopChromePath() + ".ready") &&
|
|
116
135
|
existsSync(desktopChromePath()) &&
|
|
117
136
|
existsSync("/usr/games/gnome-mines") &&
|
|
137
|
+
existsSync(
|
|
138
|
+
"/usr/share/dbus-1/services/org.gtk.vfs.Daemon.service",
|
|
139
|
+
) &&
|
|
140
|
+
existsSync(
|
|
141
|
+
"/usr/share/dbus-1/services/org.xfce.Tumbler.Thumbnailer1.service",
|
|
142
|
+
) &&
|
|
118
143
|
existsSync("/usr/share/fonts/X11/misc/fonts.dir") &&
|
|
119
144
|
existsSync("/usr/share/dbus-1/services/org.ayatana.bamf.service") &&
|
|
120
145
|
existsSync("/usr/share/dbus-1/services/org.a11y.Bus.service") &&
|
|
@@ -277,17 +302,36 @@ async function installDesktopComponents(
|
|
|
277
302
|
await rm(desktopChromePath() + ".ready", { force: true });
|
|
278
303
|
// Desktop binaries, X assets and the loader share the image root.
|
|
279
304
|
await run([...apt, "update"]);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
"-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
"
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
305
|
+
const terminal =
|
|
306
|
+
WEZTERM_PACKAGES[process.arch as keyof typeof WEZTERM_PACKAGES];
|
|
307
|
+
const terminalDownloadDir = await mkdtemp(
|
|
308
|
+
join(tmpdir(), "desktop-terminal-"),
|
|
309
|
+
);
|
|
310
|
+
try {
|
|
311
|
+
const deb = join(terminalDownloadDir, "wezterm.deb");
|
|
312
|
+
await downloadDesktopPackage(
|
|
313
|
+
`https://github.com/wezterm/wezterm/releases/download/${WEZTERM_VERSION}/wezterm-${WEZTERM_VERSION}.${terminal.suffix}`,
|
|
314
|
+
deb,
|
|
315
|
+
terminal.sha256,
|
|
316
|
+
caBundle,
|
|
317
|
+
);
|
|
318
|
+
await chmod(terminalDownloadDir, 0o755);
|
|
319
|
+
await chmod(deb, 0o644);
|
|
320
|
+
await run([
|
|
321
|
+
...apt,
|
|
322
|
+
"install",
|
|
323
|
+
"-y",
|
|
324
|
+
"--no-install-recommends",
|
|
325
|
+
"--no-upgrade",
|
|
326
|
+
"--no-remove",
|
|
327
|
+
"-o",
|
|
328
|
+
"DPkg::Lock::Timeout=120",
|
|
329
|
+
...DESKTOP_PACKAGES,
|
|
330
|
+
deb,
|
|
331
|
+
]);
|
|
332
|
+
} finally {
|
|
333
|
+
await rm(terminalDownloadDir, { recursive: true, force: true });
|
|
334
|
+
}
|
|
291
335
|
onStage("chrome");
|
|
292
336
|
if (!existsSync(desktopChromePath())) {
|
|
293
337
|
const downloadDir = await mkdtemp(join(tmpdir(), "desktop-chrome-"));
|
|
@@ -296,30 +340,12 @@ async function installDesktopComponents(
|
|
|
296
340
|
const staging = await mkdtemp(join(installRoot, ".install-"));
|
|
297
341
|
try {
|
|
298
342
|
const deb = join(downloadDir, "chrome.deb");
|
|
299
|
-
await
|
|
300
|
-
"curl",
|
|
301
|
-
...(caBundle ? ["--cacert", caBundle] : []),
|
|
302
|
-
"--fail",
|
|
303
|
-
"--location",
|
|
304
|
-
"--proto",
|
|
305
|
-
"=https",
|
|
306
|
-
"--proto-redir",
|
|
307
|
-
"=https",
|
|
308
|
-
"--retry",
|
|
309
|
-
"2",
|
|
310
|
-
"--max-time",
|
|
311
|
-
"300",
|
|
312
|
-
"--output",
|
|
313
|
-
deb,
|
|
343
|
+
await downloadDesktopPackage(
|
|
314
344
|
`https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_${chrome.arch}.deb`,
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}
|
|
320
|
-
if (hash.digest("hex") !== chrome.sha256) {
|
|
321
|
-
throw new Error("Chrome download checksum mismatch");
|
|
322
|
-
}
|
|
345
|
+
deb,
|
|
346
|
+
chrome.sha256,
|
|
347
|
+
caBundle,
|
|
348
|
+
);
|
|
323
349
|
await run(["dpkg-deb", "--extract", deb, staging]);
|
|
324
350
|
await rename(staging, join(installRoot, `chrome-${CHROME_VERSION}`));
|
|
325
351
|
} finally {
|
|
@@ -330,8 +356,41 @@ async function installDesktopComponents(
|
|
|
330
356
|
}
|
|
331
357
|
}
|
|
332
358
|
onStage("checking");
|
|
359
|
+
await run([resolveDesktopBinaries().terminal, "--version"]);
|
|
333
360
|
await run([desktopChromePath(), "--version"]);
|
|
334
361
|
await writeFile(desktopChromePath() + ".ready", "");
|
|
335
362
|
}
|
|
336
363
|
|
|
364
|
+
async function downloadDesktopPackage(
|
|
365
|
+
url: string,
|
|
366
|
+
destination: string,
|
|
367
|
+
sha256: string,
|
|
368
|
+
caBundle?: string,
|
|
369
|
+
): Promise<void> {
|
|
370
|
+
await run([
|
|
371
|
+
"curl",
|
|
372
|
+
...(caBundle ? ["--cacert", caBundle] : []),
|
|
373
|
+
"--fail",
|
|
374
|
+
"--location",
|
|
375
|
+
"--proto",
|
|
376
|
+
"=https",
|
|
377
|
+
"--proto-redir",
|
|
378
|
+
"=https",
|
|
379
|
+
"--retry",
|
|
380
|
+
"2",
|
|
381
|
+
"--max-time",
|
|
382
|
+
"300",
|
|
383
|
+
"--output",
|
|
384
|
+
destination,
|
|
385
|
+
url,
|
|
386
|
+
]);
|
|
387
|
+
const hash = createHash("sha256");
|
|
388
|
+
for await (const chunk of createReadStream(destination)) {
|
|
389
|
+
hash.update(chunk);
|
|
390
|
+
}
|
|
391
|
+
if (hash.digest("hex") !== sha256) {
|
|
392
|
+
throw new Error("Desktop package download checksum mismatch");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
337
396
|
export const desktopDependencyInstaller = new DesktopDependencyInstaller();
|
|
@@ -33,7 +33,9 @@ test("initializes the dock and preserves user changes across restarts", () => {
|
|
|
33
33
|
configDir,
|
|
34
34
|
chromiumPath: "/opt/chrome-v1/chrome",
|
|
35
35
|
chromiumProfileDir: profileDir,
|
|
36
|
-
terminalPath: "/usr/bin/
|
|
36
|
+
terminalPath: "/usr/bin/wezterm",
|
|
37
|
+
fileManagerPath: "/usr/bin/thunar",
|
|
38
|
+
workspaceDir: workspace,
|
|
37
39
|
};
|
|
38
40
|
writeDesktopPanelConfig(request);
|
|
39
41
|
const settingsPath = join(configDir, "glib-2.0", "settings", "keyfile");
|
|
@@ -59,6 +61,9 @@ test("initializes the dock and preserves user changes across restarts", () => {
|
|
|
59
61
|
rmSync(join(pinsDir, "mines.dockitem"));
|
|
60
62
|
const customSettings = settings.replace("icon-size=48", "icon-size=64");
|
|
61
63
|
writeFileSync(settingsPath, customSettings);
|
|
64
|
+
const terminalConfig = join(configDir, "wezterm", "wezterm.lua");
|
|
65
|
+
const customTerminalConfig = `${readFileSync(terminalConfig, "utf8")}\n-- Custom settings\n`;
|
|
66
|
+
writeFileSync(terminalConfig, customTerminalConfig);
|
|
62
67
|
writeDesktopPanelConfig({
|
|
63
68
|
...request,
|
|
64
69
|
chromiumPath: "/opt/chrome-v2/chrome",
|
|
@@ -66,6 +71,7 @@ test("initializes the dock and preserves user changes across restarts", () => {
|
|
|
66
71
|
expect(() => readFileSync(join(pinsDir, "chrome.dockitem"))).toThrow();
|
|
67
72
|
expect(() => readFileSync(join(pinsDir, "mines.dockitem"))).toThrow();
|
|
68
73
|
expect(readFileSync(settingsPath, "utf8")).toBe(customSettings);
|
|
74
|
+
expect(readFileSync(terminalConfig, "utf8")).toBe(customTerminalConfig);
|
|
69
75
|
expect(
|
|
70
76
|
readFileSync(
|
|
71
77
|
join(configDir, "applications", "google-chrome.desktop"),
|
|
@@ -89,7 +95,9 @@ test("initialization recovers partial pin creation", () => {
|
|
|
89
95
|
configDir,
|
|
90
96
|
chromiumPath: "/opt/chrome/chrome",
|
|
91
97
|
chromiumProfileDir: join(workspace, "data", "desktop-profile"),
|
|
92
|
-
terminalPath: "/usr/bin/
|
|
98
|
+
terminalPath: "/usr/bin/wezterm",
|
|
99
|
+
fileManagerPath: "/usr/bin/thunar",
|
|
100
|
+
workspaceDir: workspace,
|
|
93
101
|
});
|
|
94
102
|
expect(readFileSync(join(pinsDir, "chrome.dockitem"), "utf8")).toBe(
|
|
95
103
|
"existing pin",
|
|
@@ -111,7 +119,9 @@ test("a failed settings write leaves initialization retryable", () => {
|
|
|
111
119
|
configDir,
|
|
112
120
|
chromiumPath: "/opt/chrome/chrome",
|
|
113
121
|
chromiumProfileDir: join(workspace, "data", "desktop-profile"),
|
|
114
|
-
terminalPath: "/usr/bin/
|
|
122
|
+
terminalPath: "/usr/bin/wezterm",
|
|
123
|
+
fileManagerPath: "/usr/bin/thunar",
|
|
124
|
+
workspaceDir: workspace,
|
|
115
125
|
};
|
|
116
126
|
const write = fs.writeFileSync;
|
|
117
127
|
const interrupted = spyOn(fs, "writeFileSync").mockImplementation(
|
|
@@ -3,13 +3,21 @@ import {
|
|
|
3
3
|
existsSync,
|
|
4
4
|
linkSync,
|
|
5
5
|
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
6
8
|
rmSync,
|
|
7
9
|
writeFileSync,
|
|
8
10
|
} from "node:fs";
|
|
9
11
|
import { dirname, join } from "node:path";
|
|
10
12
|
import { pathToFileURL } from "node:url";
|
|
11
13
|
|
|
12
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
DESKTOP_CHROME_WINDOW_CLASS,
|
|
16
|
+
desktopChromeArguments,
|
|
17
|
+
} from "./desktop-browser-endpoint.js";
|
|
18
|
+
|
|
19
|
+
const TERMINAL_WINDOW_CLASS = "vellum-desktop-terminal";
|
|
20
|
+
const TERMINAL_HEADER_CONFIG = " window_decorations = 'TITLE|RESIZE',\n";
|
|
13
21
|
|
|
14
22
|
// Absolute icon paths work without an installed icon theme.
|
|
15
23
|
const TERMINAL_ICON_BASE64 = [
|
|
@@ -34,6 +42,8 @@ export interface DesktopPanelConfigRequest {
|
|
|
34
42
|
/** Profile the launcher shares with that browser, so it reuses the window. */
|
|
35
43
|
readonly chromiumProfileDir: string;
|
|
36
44
|
readonly terminalPath: string;
|
|
45
|
+
readonly fileManagerPath: string;
|
|
46
|
+
readonly workspaceDir: string;
|
|
37
47
|
readonly debugPort?: number;
|
|
38
48
|
}
|
|
39
49
|
|
|
@@ -50,41 +60,69 @@ export function writeDesktopPanelConfig(
|
|
|
50
60
|
"product_logo_64.png",
|
|
51
61
|
);
|
|
52
62
|
writeFileSync(terminalIcon, Buffer.from(TERMINAL_ICON_BASE64, "base64"));
|
|
63
|
+
const terminalConfig = join(configDir, "wezterm", "wezterm.lua");
|
|
64
|
+
mkdirSync(dirname(terminalConfig), { recursive: true });
|
|
65
|
+
const terminalConfigContents = `local wezterm = require 'wezterm'
|
|
66
|
+
return {
|
|
67
|
+
front_end = 'Software',
|
|
68
|
+
enable_wayland = false,
|
|
69
|
+
check_for_updates = false,
|
|
70
|
+
hide_tab_bar_if_only_one_tab = false,
|
|
71
|
+
${TERMINAL_HEADER_CONFIG} font_size = 11,
|
|
72
|
+
color_scheme = 'Catppuccin Mocha',
|
|
73
|
+
initial_cols = 110,
|
|
74
|
+
initial_rows = 28,
|
|
75
|
+
keys = {
|
|
76
|
+
{ key = 'd', mods = 'CTRL|SHIFT', action = wezterm.action.SplitHorizontal { domain = 'CurrentPaneDomain' } },
|
|
77
|
+
{ key = 'e', mods = 'CTRL|SHIFT', action = wezterm.action.SplitVertical { domain = 'CurrentPaneDomain' } },
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
`;
|
|
81
|
+
seedFile(terminalConfig, terminalConfigContents, [
|
|
82
|
+
terminalConfigContents.replace(TERMINAL_HEADER_CONFIG, ""),
|
|
83
|
+
...["", " integrated_title_buttons = { 'Maximize', 'Close' },\n"].map(
|
|
84
|
+
(buttons) =>
|
|
85
|
+
terminalConfigContents.replace(
|
|
86
|
+
TERMINAL_HEADER_CONFIG,
|
|
87
|
+
` window_decorations = 'INTEGRATED_BUTTONS|RESIZE',\n integrated_title_button_style = 'Gnome',\n${buttons}`,
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
]);
|
|
53
91
|
|
|
54
92
|
const applicationsDir = join(configDir, "applications");
|
|
55
93
|
mkdirSync(applicationsDir, { recursive: true });
|
|
56
94
|
const chromiumEntry = join(applicationsDir, "google-chrome.desktop");
|
|
95
|
+
// Saved dock pins reference this stable launcher path.
|
|
57
96
|
const terminalEntry = join(applicationsDir, "xterm.desktop");
|
|
97
|
+
const filesEntry = join(applicationsDir, "thunar.desktop");
|
|
58
98
|
const minesEntry = join(applicationsDir, "org.gnome.Mines.desktop");
|
|
59
99
|
writeFileSync(
|
|
60
100
|
chromiumEntry,
|
|
61
101
|
desktopEntry({
|
|
62
102
|
name: "Google Chrome",
|
|
63
|
-
|
|
64
|
-
windowClass: `google-chrome (${request.chromiumProfileDir})`,
|
|
103
|
+
windowClass: DESKTOP_CHROME_WINDOW_CLASS,
|
|
65
104
|
icon: browserIcon,
|
|
66
|
-
exec: [
|
|
105
|
+
exec: desktopCommand([
|
|
67
106
|
request.chromiumPath,
|
|
68
107
|
...desktopChromeArguments(
|
|
69
108
|
request.chromiumProfileDir,
|
|
70
109
|
request.debugPort,
|
|
71
110
|
),
|
|
72
|
-
]
|
|
73
|
-
.map(
|
|
74
|
-
(arg) => `"${arg.replace(/[\\"`$]/g, "\\$&").replace(/%/g, "%%")}"`,
|
|
75
|
-
)
|
|
76
|
-
.join(" "),
|
|
111
|
+
]),
|
|
77
112
|
}),
|
|
78
113
|
);
|
|
79
114
|
writeFileSync(
|
|
80
115
|
terminalEntry,
|
|
81
116
|
desktopEntry({
|
|
82
117
|
name: "Terminal",
|
|
83
|
-
windowClass:
|
|
118
|
+
windowClass: TERMINAL_WINDOW_CLASS,
|
|
84
119
|
icon: terminalIcon,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
120
|
+
exec: desktopCommand([
|
|
121
|
+
request.terminalPath,
|
|
122
|
+
"start",
|
|
123
|
+
"--new-tab",
|
|
124
|
+
`--class=${TERMINAL_WINDOW_CLASS}`,
|
|
125
|
+
]),
|
|
88
126
|
}),
|
|
89
127
|
);
|
|
90
128
|
|
|
@@ -98,6 +136,37 @@ export function writeDesktopPanelConfig(
|
|
|
98
136
|
}),
|
|
99
137
|
);
|
|
100
138
|
|
|
139
|
+
writeFileSync(
|
|
140
|
+
filesEntry,
|
|
141
|
+
desktopEntry({
|
|
142
|
+
name: "Files",
|
|
143
|
+
windowClass: "Thunar",
|
|
144
|
+
icon: "/usr/share/icons/Adwaita/scalable/places/folder.svg",
|
|
145
|
+
exec: desktopCommand([request.fileManagerPath, request.workspaceDir]),
|
|
146
|
+
}),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const gtkDir = join(configDir, "gtk-3.0");
|
|
150
|
+
const xfconfDir = join(configDir, "xfce4", "xfconf", "xfce-perchannel-xml");
|
|
151
|
+
mkdirSync(gtkDir, { recursive: true });
|
|
152
|
+
mkdirSync(xfconfDir, { recursive: true });
|
|
153
|
+
seedFile(
|
|
154
|
+
join(gtkDir, "settings.ini"),
|
|
155
|
+
"[Settings]\ngtk-theme-name=Adwaita\ngtk-icon-theme-name=Adwaita\ngtk-application-prefer-dark-theme=true\ngtk-font-name=Sans 11\n",
|
|
156
|
+
);
|
|
157
|
+
seedFile(
|
|
158
|
+
join(gtkDir, "bookmarks"),
|
|
159
|
+
`${pathToFileURL(request.workspaceDir).href} Workspace\n`,
|
|
160
|
+
);
|
|
161
|
+
seedFile(
|
|
162
|
+
join(xfconfDir, "thunar.xml"),
|
|
163
|
+
`<channel name="thunar" version="1.0">
|
|
164
|
+
<property name="last-window-width" type="int" value="1000"/>
|
|
165
|
+
<property name="last-window-height" type="int" value="680"/>
|
|
166
|
+
<property name="last-view" type="string" value="ThunarDetailsView"/>
|
|
167
|
+
</channel>\n`,
|
|
168
|
+
);
|
|
169
|
+
|
|
101
170
|
const launchersDir = join(configDir, "plank", "dock1", "launchers");
|
|
102
171
|
const settingsDir = join(configDir, "glib-2.0", "settings");
|
|
103
172
|
mkdirSync(launchersDir, { recursive: true });
|
|
@@ -107,13 +176,14 @@ export function writeDesktopPanelConfig(
|
|
|
107
176
|
return;
|
|
108
177
|
}
|
|
109
178
|
seedFile(join(launchersDir, "chrome.dockitem"), dockItem(chromiumEntry));
|
|
179
|
+
seedFile(join(launchersDir, "files.dockitem"), dockItem(filesEntry));
|
|
110
180
|
seedFile(join(launchersDir, "terminal.dockitem"), dockItem(terminalEntry));
|
|
111
181
|
seedFile(join(launchersDir, "mines.dockitem"), dockItem(minesEntry));
|
|
112
182
|
seedFile(
|
|
113
183
|
join(settingsDir, "keyfile"),
|
|
114
184
|
[
|
|
115
185
|
"[net/launchpad/plank/docks/dock1]",
|
|
116
|
-
"dock-items=['chrome.dockitem', 'terminal.dockitem', 'mines.dockitem']",
|
|
186
|
+
"dock-items=['chrome.dockitem', 'files.dockitem', 'terminal.dockitem', 'mines.dockitem']",
|
|
117
187
|
"icon-size=48",
|
|
118
188
|
"hide-mode='none'",
|
|
119
189
|
"theme='Matte'",
|
|
@@ -127,7 +197,25 @@ function dockItem(launcher: string): string {
|
|
|
127
197
|
return `[PlankDockItemPreferences]\nLauncher=${pathToFileURL(launcher).href}\n`;
|
|
128
198
|
}
|
|
129
199
|
|
|
130
|
-
function
|
|
200
|
+
function desktopCommand(args: string[]): string {
|
|
201
|
+
return args
|
|
202
|
+
.map((arg) => {
|
|
203
|
+
const quoted = `"${arg.replace(/[\\"`$]/g, "\\$&").replace(/%/g, "%%")}"`;
|
|
204
|
+
// Desktop values are unescaped before Exec arguments are parsed.
|
|
205
|
+
return quoted
|
|
206
|
+
.replace(/\\/g, "\\\\")
|
|
207
|
+
.replace(/\n/g, "\\n")
|
|
208
|
+
.replace(/\r/g, "\\r")
|
|
209
|
+
.replace(/\t/g, "\\t");
|
|
210
|
+
})
|
|
211
|
+
.join(" ");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function seedFile(
|
|
215
|
+
path: string,
|
|
216
|
+
contents: string,
|
|
217
|
+
previousContents: readonly string[] = [],
|
|
218
|
+
): void {
|
|
131
219
|
const temporaryPath = `${path}.${randomUUID()}.tmp`;
|
|
132
220
|
try {
|
|
133
221
|
writeFileSync(temporaryPath, contents, { flush: true });
|
|
@@ -137,6 +225,12 @@ function seedFile(path: string, contents: string): void {
|
|
|
137
225
|
if ((err as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
138
226
|
throw err;
|
|
139
227
|
}
|
|
228
|
+
if (
|
|
229
|
+
previousContents.length > 0 &&
|
|
230
|
+
previousContents.includes(readFileSync(path, "utf8"))
|
|
231
|
+
) {
|
|
232
|
+
renameSync(temporaryPath, path);
|
|
233
|
+
}
|
|
140
234
|
} finally {
|
|
141
235
|
rmSync(temporaryPath, { force: true });
|
|
142
236
|
}
|
|
@@ -219,6 +219,7 @@ describe("DesktopSessionManager process tree", () => {
|
|
|
219
219
|
expect(x[x.indexOf("-rfbport") + 1]).toBe(String(DESKTOP_VNC_PORT));
|
|
220
220
|
expect(x[x.indexOf("-geometry") + 1]).toBe("1600x900");
|
|
221
221
|
for (const role of [
|
|
222
|
+
"session-bus",
|
|
222
223
|
"window-manager",
|
|
223
224
|
"compositor",
|
|
224
225
|
"clipboard",
|
|
@@ -229,6 +230,8 @@ describe("DesktopSessionManager process tree", () => {
|
|
|
229
230
|
HOME: "/data",
|
|
230
231
|
LANG: "C.UTF-8",
|
|
231
232
|
DISPLAY: ":99",
|
|
233
|
+
XDG_CONFIG_HOME: panelConfigDir,
|
|
234
|
+
XDG_DATA_HOME: panelConfigDir,
|
|
232
235
|
DBUS_SESSION_BUS_ADDRESS: h.manager.accessibilityBusAddress,
|
|
233
236
|
NO_AT_BRIDGE: "0",
|
|
234
237
|
GTK_A11Y: "atspi",
|
|
@@ -280,7 +283,7 @@ describe("DesktopSessionManager process tree", () => {
|
|
|
280
283
|
"plank",
|
|
281
284
|
"tigervncconfig",
|
|
282
285
|
"vncconfig",
|
|
283
|
-
"
|
|
286
|
+
"wezterm",
|
|
284
287
|
],
|
|
285
288
|
});
|
|
286
289
|
await expect(h.manager.ensureDesktopRunning()).rejects.toThrow(
|
|
@@ -306,7 +309,7 @@ describe("DesktopSessionManager process tree", () => {
|
|
|
306
309
|
join(panelConfigDir, "applications", "xterm.desktop"),
|
|
307
310
|
"utf8",
|
|
308
311
|
);
|
|
309
|
-
expect(terminal).toContain(`Exec="/usr/bin/
|
|
312
|
+
expect(terminal).toContain(`Exec="/usr/bin/wezterm"`);
|
|
310
313
|
expect(terminal).toContain(`Icon=${join(panelConfigDir, "terminal.png")}`);
|
|
311
314
|
|
|
312
315
|
const bytes = readFileSync(join(panelConfigDir, "terminal.png"));
|
|
@@ -11,13 +11,11 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
11
11
|
import { createConnection } from "node:net";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
|
|
14
|
-
import { readAvatarState } from "../avatar/avatar-manifest.js";
|
|
15
|
-
import { resolveNotificationAccentHex } from "../avatar/notification-avatar.js";
|
|
16
14
|
import { getIsContainerized } from "../config/env-registry.js";
|
|
17
15
|
import { connectCdpWsTransport } from "../tools/browser/cdp-client/cdp-inspect/ws-transport.js";
|
|
18
16
|
import { terminateProcessTree } from "../util/host-process.js";
|
|
19
17
|
import { getLogger } from "../util/logger.js";
|
|
20
|
-
import { getDataDir } from "../util/platform.js";
|
|
18
|
+
import { getDataDir, getWorkspaceDir } from "../util/platform.js";
|
|
21
19
|
import { sleep } from "../util/retry.js";
|
|
22
20
|
import { DesktopBrowserClient } from "./desktop-browser-client.js";
|
|
23
21
|
import {
|
|
@@ -358,11 +356,7 @@ export class DesktopSessionManager {
|
|
|
358
356
|
((configDir) => {
|
|
359
357
|
let sourcePath: string | undefined;
|
|
360
358
|
try {
|
|
361
|
-
sourcePath = writeDesktopWindowTheme(
|
|
362
|
-
configDir,
|
|
363
|
-
this.sourceEnv.HOME,
|
|
364
|
-
resolveNotificationAccentHex(readAvatarState()),
|
|
365
|
-
);
|
|
359
|
+
sourcePath = writeDesktopWindowTheme(configDir, this.sourceEnv.HOME);
|
|
366
360
|
} catch (err) {
|
|
367
361
|
log.warn({ err }, "Desktop window theme could not be applied");
|
|
368
362
|
}
|
|
@@ -651,11 +645,11 @@ export class DesktopSessionManager {
|
|
|
651
645
|
chromiumProfileDir: this.profileDir,
|
|
652
646
|
debugPort: this.debugPort,
|
|
653
647
|
terminalPath: binaries.terminal,
|
|
648
|
+
fileManagerPath: binaries.fileManager,
|
|
649
|
+
workspaceDir: getWorkspaceDir(),
|
|
654
650
|
});
|
|
655
651
|
this.launch("panel", [binaries.panel], {
|
|
656
652
|
...env,
|
|
657
|
-
XDG_CONFIG_HOME: this.panelConfigDir,
|
|
658
|
-
XDG_DATA_HOME: this.panelConfigDir,
|
|
659
653
|
GSETTINGS_BACKEND: "keyfile",
|
|
660
654
|
});
|
|
661
655
|
} catch (err) {
|
|
@@ -882,6 +876,9 @@ export class DesktopSessionManager {
|
|
|
882
876
|
}
|
|
883
877
|
}
|
|
884
878
|
env.DISPLAY = DESKTOP_DISPLAY;
|
|
879
|
+
// D-Bus services share app preferences and launchers.
|
|
880
|
+
env.XDG_CONFIG_HOME = this.panelConfigDir;
|
|
881
|
+
env.XDG_DATA_HOME = this.panelConfigDir;
|
|
885
882
|
env.DBUS_SESSION_BUS_ADDRESS = this.accessibilityBusAddress;
|
|
886
883
|
env.NO_AT_BRIDGE = "0";
|
|
887
884
|
env.GTK_A11Y = "atspi";
|
|
@@ -13,24 +13,7 @@ const BUTTON_MASKS = {
|
|
|
13
13
|
iconify: [0, 0, 0, 0, 0x3ff, 0x3ff, 0, 0, 0, 0],
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
function windowTheme(
|
|
17
|
-
const tint = (base: string, amount: number): string => {
|
|
18
|
-
if (!accentHex || !/^#[\da-f]{6}$/i.test(accentHex)) {
|
|
19
|
-
return base;
|
|
20
|
-
}
|
|
21
|
-
return (
|
|
22
|
-
"#" +
|
|
23
|
-
[1, 3, 5]
|
|
24
|
-
.map((offset) => {
|
|
25
|
-
const from = parseInt(base.slice(offset, offset + 2), 16);
|
|
26
|
-
const to = parseInt(accentHex.slice(offset, offset + 2), 16);
|
|
27
|
-
return Math.round(from + (to - from) * amount)
|
|
28
|
-
.toString(16)
|
|
29
|
-
.padStart(2, "0");
|
|
30
|
-
})
|
|
31
|
-
.join("")
|
|
32
|
-
);
|
|
33
|
-
};
|
|
16
|
+
function windowTheme(): string {
|
|
34
17
|
return `
|
|
35
18
|
border.width: 1
|
|
36
19
|
padding.width: 8
|
|
@@ -47,17 +30,15 @@ window.*.grip.bg: Flat Solid
|
|
|
47
30
|
window.*.button.*.bg: Parentrelative
|
|
48
31
|
window.*.button.*.hover.bg: Flat Solid
|
|
49
32
|
window.*.button.*.pressed.bg: Flat Solid
|
|
50
|
-
window.active.title.bg:
|
|
51
|
-
window.active.title.bg.color: ${tint("#292930", 0.28)}
|
|
52
|
-
window.active.title.bg.colorTo: #292930
|
|
33
|
+
window.active.title.bg.color: #292930
|
|
53
34
|
window.active.border.color: #4a4a54
|
|
54
35
|
window.active.title.separator.color: #202026
|
|
55
36
|
window.active.label.text.color: #f0f0f4
|
|
56
37
|
window.active.handle.bg.color: #292930
|
|
57
38
|
window.active.grip.bg.color: #292930
|
|
58
39
|
window.active.button.*.image.color: #d4d4dc
|
|
59
|
-
window.active.button.*.hover.bg.color:
|
|
60
|
-
window.active.button.*.pressed.bg.color:
|
|
40
|
+
window.active.button.*.hover.bg.color: #45454f
|
|
41
|
+
window.active.button.*.pressed.bg.color: #555561
|
|
61
42
|
window.active.button.*.disabled.image.color: #62626e
|
|
62
43
|
window.active.button.close.hover.bg.color: #b64150
|
|
63
44
|
window.active.button.close.pressed.bg.color: #963442
|
|
@@ -97,7 +78,6 @@ osd.label.text.color: #f0f0f4
|
|
|
97
78
|
export function writeDesktopWindowTheme(
|
|
98
79
|
configDir: string,
|
|
99
80
|
home?: string,
|
|
100
|
-
accentHex: string | null = null,
|
|
101
81
|
): string {
|
|
102
82
|
let source: string | undefined;
|
|
103
83
|
let sourcePath = "";
|
|
@@ -134,7 +114,7 @@ export function writeDesktopWindowTheme(
|
|
|
134
114
|
mkdirSync(join(themeDir, "openbox-3"), { recursive: true });
|
|
135
115
|
writeFileSync(
|
|
136
116
|
join(themeDir, "openbox-3", "themerc"),
|
|
137
|
-
windowTheme(
|
|
117
|
+
windowTheme().trimStart(),
|
|
138
118
|
);
|
|
139
119
|
for (const [name, rows] of Object.entries(BUTTON_MASKS)) {
|
|
140
120
|
const bytes = rows.flatMap((row) => [row & 0xff, row >> 8]);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
linkSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
|
|
12
|
+
import type { WorkspaceMigration } from "./types.js";
|
|
13
|
+
|
|
14
|
+
export const addDesktopFilesLauncherMigration: WorkspaceMigration = {
|
|
15
|
+
id: "157-add-desktop-files-launcher",
|
|
16
|
+
description: "Add Files to existing virtual desktop docks",
|
|
17
|
+
retryFailedCheckpoint: true,
|
|
18
|
+
run(workspaceDir): void {
|
|
19
|
+
const configDir = join(workspaceDir, "data", "desktop-panel");
|
|
20
|
+
const settingsPath = join(configDir, "glib-2.0", "settings", "keyfile");
|
|
21
|
+
let settings: string;
|
|
22
|
+
try {
|
|
23
|
+
settings = readFileSync(settingsPath, "utf8");
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
throw err;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const section =
|
|
32
|
+
/(?:^|\n)\[net\/launchpad\/plank\/docks\/dock1\]\r?\n([^]*?)(?=\n\[|$)/;
|
|
33
|
+
const match = settings.match(section);
|
|
34
|
+
if (!match) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const pins = /^(dock-items\s*=\s*)(?:@as\s+)?\[([^\r\n]*)\](\r?)$/m;
|
|
38
|
+
const existing = match[1]!.match(pins);
|
|
39
|
+
if (existing && !existing[2]!.includes("'files.dockitem'")) {
|
|
40
|
+
const items = existing[2]!.trim();
|
|
41
|
+
let updatedItems: string;
|
|
42
|
+
if (items.includes("'chrome.dockitem'")) {
|
|
43
|
+
updatedItems = items.replace(
|
|
44
|
+
"'chrome.dockitem'",
|
|
45
|
+
"'chrome.dockitem', 'files.dockitem'",
|
|
46
|
+
);
|
|
47
|
+
} else if (items.includes("'terminal.dockitem'")) {
|
|
48
|
+
updatedItems = items.replace(
|
|
49
|
+
"'terminal.dockitem'",
|
|
50
|
+
"'files.dockitem', 'terminal.dockitem'",
|
|
51
|
+
);
|
|
52
|
+
} else {
|
|
53
|
+
updatedItems = `${items}${items ? ", " : ""}'files.dockitem'`;
|
|
54
|
+
}
|
|
55
|
+
const updated = match[0].replace(
|
|
56
|
+
pins,
|
|
57
|
+
() => `${existing[1]}[${updatedItems}]${existing[3]}`,
|
|
58
|
+
);
|
|
59
|
+
settings = settings.replace(section, () => updated);
|
|
60
|
+
} else if (!existing) {
|
|
61
|
+
settings = settings.replace(
|
|
62
|
+
section,
|
|
63
|
+
() =>
|
|
64
|
+
`${match[0]}${match[0].endsWith("\n") ? "" : "\n"}dock-items=['files.dockitem']\n`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const launchersDir = join(configDir, "plank", "dock1", "launchers");
|
|
69
|
+
mkdirSync(launchersDir, { recursive: true });
|
|
70
|
+
const launcher = pathToFileURL(
|
|
71
|
+
join(configDir, "applications", "thunar.desktop"),
|
|
72
|
+
).href;
|
|
73
|
+
const pinPath = join(launchersDir, "files.dockitem");
|
|
74
|
+
const pinTemporaryPath = `${pinPath}.migration.tmp`;
|
|
75
|
+
try {
|
|
76
|
+
writeFileSync(
|
|
77
|
+
pinTemporaryPath,
|
|
78
|
+
`[PlankDockItemPreferences]\nLauncher=${launcher}\n`,
|
|
79
|
+
{ flush: true },
|
|
80
|
+
);
|
|
81
|
+
linkSync(pinTemporaryPath, pinPath);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if ((err as NodeJS.ErrnoException).code !== "EEXIST") {
|
|
84
|
+
throw err;
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
rmSync(pinTemporaryPath, { force: true });
|
|
88
|
+
}
|
|
89
|
+
const temporaryPath = `${settingsPath}.files-migration.tmp`;
|
|
90
|
+
writeFileSync(temporaryPath, settings, { flush: true });
|
|
91
|
+
renameSync(temporaryPath, settingsPath);
|
|
92
|
+
},
|
|
93
|
+
down(): void {},
|
|
94
|
+
};
|
|
@@ -3,9 +3,12 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { describe, expect, test } from "bun:test";
|
|
5
5
|
|
|
6
|
-
import { extractWorkspaceMcpJsonMigration } from "../156-extract-workspace-mcp-json.js";
|
|
7
6
|
import { WORKSPACE_MIGRATIONS } from "../registry.js";
|
|
8
7
|
|
|
8
|
+
const extractWorkspaceMcpJsonMigration = WORKSPACE_MIGRATIONS.find(
|
|
9
|
+
(migration) => migration.id === "156-extract-workspace-mcp-json",
|
|
10
|
+
)!;
|
|
11
|
+
|
|
9
12
|
const MIGRATION_156_SCHEMA_URL =
|
|
10
13
|
"https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
11
14
|
|
|
@@ -23,15 +26,6 @@ function readJson(dir: string, name: string): Record<string, unknown> {
|
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
describe("156-extract-workspace-mcp-json", () => {
|
|
26
|
-
test("has the next migration id and is registered last", () => {
|
|
27
|
-
expect(extractWorkspaceMcpJsonMigration.id).toBe(
|
|
28
|
-
"156-extract-workspace-mcp-json",
|
|
29
|
-
);
|
|
30
|
-
expect(WORKSPACE_MIGRATIONS.at(-1)?.id).toBe(
|
|
31
|
-
"156-extract-workspace-mcp-json",
|
|
32
|
-
);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
29
|
test("moves servers into a spec-pure mcp.json and drops mcp from config", () => {
|
|
36
30
|
const dir = workspaceWith({
|
|
37
31
|
llm: { activeProfile: "balanced" },
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
existsSync,
|
|
3
|
+
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { afterEach, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import { writeDesktopPanelConfig } from "../../../desktop/desktop-panel-config.js";
|
|
14
|
+
import { WORKSPACE_MIGRATIONS } from "../registry.js";
|
|
15
|
+
|
|
16
|
+
const migration = WORKSPACE_MIGRATIONS.find(
|
|
17
|
+
(entry) => entry.id === "157-add-desktop-files-launcher",
|
|
18
|
+
)!;
|
|
19
|
+
|
|
20
|
+
const workspaces: string[] = [];
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
for (const workspace of workspaces.splice(0)) {
|
|
23
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function setup() {
|
|
28
|
+
const workspace = mkdtempSync(join(tmpdir(), "desktop-files-migration-"));
|
|
29
|
+
workspaces.push(workspace);
|
|
30
|
+
const configDir = join(workspace, "data", "desktop-panel");
|
|
31
|
+
const settingsPath = join(configDir, "glib-2.0", "settings", "keyfile");
|
|
32
|
+
return { workspace, configDir, settingsPath };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test("upgrades a customized dock idempotently and respects later unpinning", () => {
|
|
36
|
+
const { workspace, configDir, settingsPath } = setup();
|
|
37
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
38
|
+
const original =
|
|
39
|
+
"[unrelated]\nvalue=keep\n\n[net/launchpad/plank/docks/dock1]\ndock-items=['chrome.dockitem', 'terminal.dockitem', 'custom.dockitem']\nicon-size=64\n\n[another]\ndock-items=['other.dockitem']\n";
|
|
40
|
+
writeFileSync(settingsPath, original);
|
|
41
|
+
migration.run(workspace);
|
|
42
|
+
const upgraded = readFileSync(settingsPath, "utf8");
|
|
43
|
+
expect(upgraded).toBe(
|
|
44
|
+
original.replace(
|
|
45
|
+
"'chrome.dockitem', 'terminal.dockitem'",
|
|
46
|
+
"'chrome.dockitem', 'files.dockitem', 'terminal.dockitem'",
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
migration.run(workspace);
|
|
50
|
+
expect(readFileSync(settingsPath, "utf8")).toBe(upgraded);
|
|
51
|
+
const pin = join(configDir, "plank", "dock1", "launchers", "files.dockitem");
|
|
52
|
+
expect(readFileSync(pin, "utf8")).toContain("/applications/thunar.desktop");
|
|
53
|
+
rmSync(pin);
|
|
54
|
+
writeFileSync(settingsPath, original);
|
|
55
|
+
writeDesktopPanelConfig({
|
|
56
|
+
configDir,
|
|
57
|
+
chromiumPath: "/opt/chrome/chrome",
|
|
58
|
+
chromiumProfileDir: join(workspace, "profile"),
|
|
59
|
+
terminalPath: "/usr/bin/wezterm",
|
|
60
|
+
fileManagerPath: "/usr/bin/thunar",
|
|
61
|
+
workspaceDir: workspace,
|
|
62
|
+
});
|
|
63
|
+
expect(existsSync(pin)).toBe(false);
|
|
64
|
+
expect(readFileSync(settingsPath, "utf8")).toBe(original);
|
|
65
|
+
expect(existsSync(join(configDir, "applications", "thunar.desktop"))).toBe(
|
|
66
|
+
true,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test.each(["[]", "@as []"])(
|
|
71
|
+
"upgrades an empty dock (%s) and recovers an existing pin",
|
|
72
|
+
(items) => {
|
|
73
|
+
const { workspace, configDir, settingsPath } = setup();
|
|
74
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
75
|
+
writeFileSync(
|
|
76
|
+
settingsPath,
|
|
77
|
+
`[net/launchpad/plank/docks/dock1]\ndock-items=${items}\n`,
|
|
78
|
+
);
|
|
79
|
+
const pin = join(
|
|
80
|
+
configDir,
|
|
81
|
+
"plank",
|
|
82
|
+
"dock1",
|
|
83
|
+
"launchers",
|
|
84
|
+
"files.dockitem",
|
|
85
|
+
);
|
|
86
|
+
mkdirSync(dirname(pin), { recursive: true });
|
|
87
|
+
writeFileSync(pin, "user launcher");
|
|
88
|
+
migration.run(workspace);
|
|
89
|
+
expect(readFileSync(settingsPath, "utf8")).toContain(
|
|
90
|
+
"dock-items=['files.dockitem']",
|
|
91
|
+
);
|
|
92
|
+
expect(readFileSync(pin, "utf8")).toBe("user launcher");
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
test("does not initialize a desktop for workspaces without one", () => {
|
|
97
|
+
const { workspace, configDir } = setup();
|
|
98
|
+
migration.run(workspace);
|
|
99
|
+
expect(existsSync(configDir)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("adds an implicit dock pin list inside the correct settings section", () => {
|
|
103
|
+
const { workspace, settingsPath } = setup();
|
|
104
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
105
|
+
writeFileSync(
|
|
106
|
+
settingsPath,
|
|
107
|
+
"[unrelated]\nvalue=keep\n[net/launchpad/plank/docks/dock1]\nicon-size=64\n[next]\nvalue=also-keep\n",
|
|
108
|
+
);
|
|
109
|
+
migration.run(workspace);
|
|
110
|
+
expect(readFileSync(settingsPath, "utf8")).toBe(
|
|
111
|
+
"[unrelated]\nvalue=keep\n[net/launchpad/plank/docks/dock1]\nicon-size=64\ndock-items=['files.dockitem']\n\n[next]\nvalue=also-keep\n",
|
|
112
|
+
);
|
|
113
|
+
});
|
|
@@ -154,6 +154,7 @@ import { stripMcpPolicyFieldsMigration } from "./153-strip-mcp-policy-fields.js"
|
|
|
154
154
|
import { repairRetiredCodexGpt54ModelIdsMigration } from "./154-repair-retired-codex-gpt-5-4-model-ids.js";
|
|
155
155
|
import { moveFrontModelConfigToVoiceMigration } from "./155-move-front-model-config-to-voice.js";
|
|
156
156
|
import { extractWorkspaceMcpJsonMigration } from "./156-extract-workspace-mcp-json.js";
|
|
157
|
+
import { addDesktopFilesLauncherMigration } from "./157-add-desktop-files-launcher.js";
|
|
157
158
|
import { migrateToWorkspaceVolumeMigration } from "./migrate-to-workspace-volume.js";
|
|
158
159
|
import type { WorkspaceMigration } from "./types.js";
|
|
159
160
|
|
|
@@ -323,4 +324,5 @@ export const WORKSPACE_MIGRATIONS: WorkspaceMigration[] = [
|
|
|
323
324
|
repairRetiredCodexGpt54ModelIdsMigration,
|
|
324
325
|
moveFrontModelConfigToVoiceMigration,
|
|
325
326
|
extractWorkspaceMcpJsonMigration,
|
|
327
|
+
addDesktopFilesLauncherMigration,
|
|
326
328
|
];
|