@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.2-dev.202609210049.b9f03ca",
3
+ "version": "0.12.2-dev.202609210132.d52247f",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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) => `"${arg.replace(/[\\"`$]/g, "\\$&").replace(/%/g, "%%")}"`)
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-activated window matching must see the same launchers as Plank.
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
  ];