@vellumai/assistant 0.12.2-dev.202609210049.b9f03ca → 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 +14 -0
- package/package.json +1 -1
- package/src/desktop/desktop-dependencies.ts +11 -0
- package/src/desktop/desktop-panel-config.test.ts +6 -0
- package/src/desktop/desktop-panel-config.ts +45 -2
- package/src/desktop/desktop-session-manager.test.ts +1 -0
- package/src/desktop/desktop-session-manager.ts +5 -4
- 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
|
@@ -58,3 +58,17 @@ Browser commands, user handoff, errors, and interruption require a fresh
|
|
|
58
58
|
observation before returning to native input. Browser commands and computer use
|
|
59
59
|
share session ownership, cancellation, and the user-help reservation. Use
|
|
60
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
|
@@ -44,16 +44,21 @@ const DESKTOP_BINARIES = {
|
|
|
44
44
|
sessionBus: ["dbus-daemon"],
|
|
45
45
|
clipboard: ["tigervncconfig", "vncconfig"],
|
|
46
46
|
terminal: ["wezterm"],
|
|
47
|
+
fileManager: ["thunar"],
|
|
47
48
|
wallpaper: ["feh"],
|
|
48
49
|
input: ["xdotool"],
|
|
49
50
|
capture: ["scrot"],
|
|
50
51
|
} as const;
|
|
51
52
|
|
|
52
53
|
const DESKTOP_PACKAGES = [
|
|
54
|
+
"adwaita-icon-theme",
|
|
53
55
|
"at-spi2-core",
|
|
54
56
|
"dbus-x11",
|
|
55
57
|
"feh",
|
|
56
58
|
"gnome-mines",
|
|
59
|
+
"gvfs",
|
|
60
|
+
"thunar",
|
|
61
|
+
"tumbler",
|
|
57
62
|
"openbox",
|
|
58
63
|
"python3",
|
|
59
64
|
"python3-dbus",
|
|
@@ -129,6 +134,12 @@ export class DesktopDependencyInstaller {
|
|
|
129
134
|
existsSync(desktopChromePath() + ".ready") &&
|
|
130
135
|
existsSync(desktopChromePath()) &&
|
|
131
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
|
+
) &&
|
|
132
143
|
existsSync("/usr/share/fonts/X11/misc/fonts.dir") &&
|
|
133
144
|
existsSync("/usr/share/dbus-1/services/org.ayatana.bamf.service") &&
|
|
134
145
|
existsSync("/usr/share/dbus-1/services/org.a11y.Bus.service") &&
|
|
@@ -34,6 +34,8 @@ test("initializes the dock and preserves user changes across restarts", () => {
|
|
|
34
34
|
chromiumPath: "/opt/chrome-v1/chrome",
|
|
35
35
|
chromiumProfileDir: profileDir,
|
|
36
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");
|
|
@@ -94,6 +96,8 @@ test("initialization recovers partial pin creation", () => {
|
|
|
94
96
|
chromiumPath: "/opt/chrome/chrome",
|
|
95
97
|
chromiumProfileDir: join(workspace, "data", "desktop-profile"),
|
|
96
98
|
terminalPath: "/usr/bin/wezterm",
|
|
99
|
+
fileManagerPath: "/usr/bin/thunar",
|
|
100
|
+
workspaceDir: workspace,
|
|
97
101
|
});
|
|
98
102
|
expect(readFileSync(join(pinsDir, "chrome.dockitem"), "utf8")).toBe(
|
|
99
103
|
"existing pin",
|
|
@@ -116,6 +120,8 @@ test("a failed settings write leaves initialization retryable", () => {
|
|
|
116
120
|
chromiumPath: "/opt/chrome/chrome",
|
|
117
121
|
chromiumProfileDir: join(workspace, "data", "desktop-profile"),
|
|
118
122
|
terminalPath: "/usr/bin/wezterm",
|
|
123
|
+
fileManagerPath: "/usr/bin/thunar",
|
|
124
|
+
workspaceDir: workspace,
|
|
119
125
|
};
|
|
120
126
|
const write = fs.writeFileSync;
|
|
121
127
|
const interrupted = spyOn(fs, "writeFileSync").mockImplementation(
|
|
@@ -42,6 +42,8 @@ export interface DesktopPanelConfigRequest {
|
|
|
42
42
|
/** Profile the launcher shares with that browser, so it reuses the window. */
|
|
43
43
|
readonly chromiumProfileDir: string;
|
|
44
44
|
readonly terminalPath: string;
|
|
45
|
+
readonly fileManagerPath: string;
|
|
46
|
+
readonly workspaceDir: string;
|
|
45
47
|
readonly debugPort?: number;
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -92,6 +94,7 @@ ${TERMINAL_HEADER_CONFIG} font_size = 11,
|
|
|
92
94
|
const chromiumEntry = join(applicationsDir, "google-chrome.desktop");
|
|
93
95
|
// Saved dock pins reference this stable launcher path.
|
|
94
96
|
const terminalEntry = join(applicationsDir, "xterm.desktop");
|
|
97
|
+
const filesEntry = join(applicationsDir, "thunar.desktop");
|
|
95
98
|
const minesEntry = join(applicationsDir, "org.gnome.Mines.desktop");
|
|
96
99
|
writeFileSync(
|
|
97
100
|
chromiumEntry,
|
|
@@ -133,6 +136,37 @@ ${TERMINAL_HEADER_CONFIG} font_size = 11,
|
|
|
133
136
|
}),
|
|
134
137
|
);
|
|
135
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
|
+
|
|
136
170
|
const launchersDir = join(configDir, "plank", "dock1", "launchers");
|
|
137
171
|
const settingsDir = join(configDir, "glib-2.0", "settings");
|
|
138
172
|
mkdirSync(launchersDir, { recursive: true });
|
|
@@ -142,13 +176,14 @@ ${TERMINAL_HEADER_CONFIG} font_size = 11,
|
|
|
142
176
|
return;
|
|
143
177
|
}
|
|
144
178
|
seedFile(join(launchersDir, "chrome.dockitem"), dockItem(chromiumEntry));
|
|
179
|
+
seedFile(join(launchersDir, "files.dockitem"), dockItem(filesEntry));
|
|
145
180
|
seedFile(join(launchersDir, "terminal.dockitem"), dockItem(terminalEntry));
|
|
146
181
|
seedFile(join(launchersDir, "mines.dockitem"), dockItem(minesEntry));
|
|
147
182
|
seedFile(
|
|
148
183
|
join(settingsDir, "keyfile"),
|
|
149
184
|
[
|
|
150
185
|
"[net/launchpad/plank/docks/dock1]",
|
|
151
|
-
"dock-items=['chrome.dockitem', 'terminal.dockitem', 'mines.dockitem']",
|
|
186
|
+
"dock-items=['chrome.dockitem', 'files.dockitem', 'terminal.dockitem', 'mines.dockitem']",
|
|
152
187
|
"icon-size=48",
|
|
153
188
|
"hide-mode='none'",
|
|
154
189
|
"theme='Matte'",
|
|
@@ -164,7 +199,15 @@ function dockItem(launcher: string): string {
|
|
|
164
199
|
|
|
165
200
|
function desktopCommand(args: string[]): string {
|
|
166
201
|
return args
|
|
167
|
-
.map((arg) =>
|
|
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
|
+
})
|
|
168
211
|
.join(" ");
|
|
169
212
|
}
|
|
170
213
|
|
|
@@ -230,6 +230,7 @@ describe("DesktopSessionManager process tree", () => {
|
|
|
230
230
|
HOME: "/data",
|
|
231
231
|
LANG: "C.UTF-8",
|
|
232
232
|
DISPLAY: ":99",
|
|
233
|
+
XDG_CONFIG_HOME: panelConfigDir,
|
|
233
234
|
XDG_DATA_HOME: panelConfigDir,
|
|
234
235
|
DBUS_SESSION_BUS_ADDRESS: h.manager.accessibilityBusAddress,
|
|
235
236
|
NO_AT_BRIDGE: "0",
|
|
@@ -15,7 +15,7 @@ import { getIsContainerized } from "../config/env-registry.js";
|
|
|
15
15
|
import { connectCdpWsTransport } from "../tools/browser/cdp-client/cdp-inspect/ws-transport.js";
|
|
16
16
|
import { terminateProcessTree } from "../util/host-process.js";
|
|
17
17
|
import { getLogger } from "../util/logger.js";
|
|
18
|
-
import { getDataDir } from "../util/platform.js";
|
|
18
|
+
import { getDataDir, getWorkspaceDir } from "../util/platform.js";
|
|
19
19
|
import { sleep } from "../util/retry.js";
|
|
20
20
|
import { DesktopBrowserClient } from "./desktop-browser-client.js";
|
|
21
21
|
import {
|
|
@@ -645,11 +645,11 @@ export class DesktopSessionManager {
|
|
|
645
645
|
chromiumProfileDir: this.profileDir,
|
|
646
646
|
debugPort: this.debugPort,
|
|
647
647
|
terminalPath: binaries.terminal,
|
|
648
|
+
fileManagerPath: binaries.fileManager,
|
|
649
|
+
workspaceDir: getWorkspaceDir(),
|
|
648
650
|
});
|
|
649
651
|
this.launch("panel", [binaries.panel], {
|
|
650
652
|
...env,
|
|
651
|
-
XDG_CONFIG_HOME: this.panelConfigDir,
|
|
652
|
-
XDG_DATA_HOME: this.panelConfigDir,
|
|
653
653
|
GSETTINGS_BACKEND: "keyfile",
|
|
654
654
|
});
|
|
655
655
|
} catch (err) {
|
|
@@ -876,7 +876,8 @@ export class DesktopSessionManager {
|
|
|
876
876
|
}
|
|
877
877
|
}
|
|
878
878
|
env.DISPLAY = DESKTOP_DISPLAY;
|
|
879
|
-
// D-Bus
|
|
879
|
+
// D-Bus services share app preferences and launchers.
|
|
880
|
+
env.XDG_CONFIG_HOME = this.panelConfigDir;
|
|
880
881
|
env.XDG_DATA_HOME = this.panelConfigDir;
|
|
881
882
|
env.DBUS_SESSION_BUS_ADDRESS = this.accessibilityBusAddress;
|
|
882
883
|
env.NO_AT_BRIDGE = "0";
|
|
@@ -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
|
];
|