diff --git a/AGENTS.md b/AGENTS.md index 81e6921..292c8fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,6 +55,26 @@ direction. - Discord deletes the stored token when it thinks devtools opened; keep Vesktop's devtools detector patch (`src/renderer/patches/devtoolsFixes.ts`) so this cannot cause random logouts. +## Headless run (verified 2026-09-11) + +The real client has been run with no visible window and successfully driven end-to-end: + +```sh +Xvfb :99 -screen 0 1280x720x24 & +DISPLAY=:99 VESKSLOP_HEADLESS=1 \ +VESKSLOP_TOKEN_FILE=~/.config/veskslop/token \ +electron . --no-sandbox --disable-gpu --disable-dev-shm-usage +``` + +- A display server is still required; Xvfb works. +- `VESKSLOP_HEADLESS=1` skips the first-launch tour so the main window is created directly. +- The token file is seeded before the client boots (verified: logged in with the puppet account). +- Outbound sends must pass `waitForChannelReady = false` (`sendMessage(channelId, data, false)` in + `src/renderer/ipcCommands.ts`). Otherwise the client's MessageQueue waits forever for channels + that are never opened in a headless session. +- `message:read` is cache-only; channels the client never opened return zero messages. A + fetch-based read (through the client) is still to be added. + ## How to proceed **Current milestone — puppeting bridge (testable without Electron).** @@ -101,7 +121,7 @@ MCP clients (agents) ──Streamable HTTP──▶ veskSlop control daemon ## Open questions -- Headless Electron strategy: Xvfb vs offscreen rendering, and what to strip to lighten the client. +- What to strip to lighten the client once the daemon path is stable. - Voice/video: what the client's native stack can be driven to do without UI interaction. - Scope and auth model of the future remote proxy client and web debug UI. - How much of the Vesktop Electron tree gets deleted once the daemon is self-sufficient. diff --git a/poc/README.md b/poc/README.md index 6d5520e..ed931de 100644 --- a/poc/README.md +++ b/poc/README.md @@ -79,6 +79,10 @@ sanctioned live-send target, user **zzib** (`TEST_DM_USER_ID` overrides). - QR login works end-to-end; the token in `.token` came from a phone scan. - MCP bridge path: `initialize`, `tools/list`, `get_state`, `send_message`, `read_messages`, `send_dm` all round-trip correctly through the mock. +- **Live end-to-end (2026-09-11)**: a real Vesktop client was run headless under Xvfb with the + token seeded from `~/.config/veskslop/token`. `get_state` returned the real account and 7 guilds, + and `send_dm` delivered a real DM to zzib (`sent: true`). Cache-only `read_messages` on that + never-opened DM returned zero messages, as expected. - Pacing observed live: consecutive commands reached the mock **592 ms**, **1862 ms** and **1439 ms** apart — inside the 500–2800 ms window. diff --git a/src/main/main.ts b/src/main/main.ts index 163450b..0a31ba6 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -129,10 +129,10 @@ if (!app.requestSingleInstanceLock({ IS_DEV })) { } async function bootstrap() { - if (!Object.hasOwn(State.store, "firstLaunch")) { - createFirstLaunchTour(); - } else { + if (process.env.VESKSLOP_HEADLESS === "1" || Object.hasOwn(State.store, "firstLaunch")) { createWindows(); + } else { + createFirstLaunchTour(); } } diff --git a/src/renderer/ipcCommands.ts b/src/renderer/ipcCommands.ts index dba003f..44d7646 100644 --- a/src/renderer/ipcCommands.ts +++ b/src/renderer/ipcCommands.ts @@ -76,7 +76,7 @@ onIpcCommand(IpcCommands.MESSAGE_SEND, async (data: { channelId?: string; conten throw new Error("message:send requires { channelId, content }"); } - await sendMessage(data.channelId, { content: data.content }); + await sendMessage(data.channelId, { content: data.content }, false); return { channelId: data.channelId, sent: true }; }); @@ -115,6 +115,6 @@ onIpcCommand(IpcCommands.DM_SEND, async (data: { userId?: string; content?: stri if (!channelId) throw new Error(`No DM channel for user ${data.userId} yet`); - await sendMessage(channelId, { content: data.content }); + await sendMessage(channelId, { content: data.content }, false); return { userId: data.userId, channelId, sent: true }; });