# Manus — Submitting Tasks, Awaiting Completion, Sharing Results Manus (`manus.im`) is a general-purpose agent that runs long-form tasks in a cloud sandbox. The web UI is a Next.js SPA; the runtime API is a separate host (`api.manus.im`) speaking Connect RPC. You must be signed in before the harness can drive it — hit the auth wall and stop, don't type credentials. ## URL patterns - App home / new-task composer: `https://manus.im/app` - Running or completed task: `https://manus.im/app/` — `` is a 22-char base62-ish opaque id (character set includes letters, digits, and `-`/`_`). The same id is the session id on the API side. - Public share page: `https://manus.im/share/` — only reachable after a `ShareSession` call (clicking *Share*). - Library / past tasks: listed in the left sidebar; the list is paged via `session.v1.SessionService/ListSessions`. The title of a task page is auto-summarized from the prompt (it shortens, reflows, and sometimes paraphrases). Do **not** match on the original prompt text when identifying a task in the sidebar — use the taskId in the URL as the durable handle. ## Path 1: Private API — `api.manus.im` Connect RPC All API calls go to `https://api.manus.im/` with the Connect RPC shape `/.v./`. Requests are POST with `content-type: application/json`; the Connect JS client encodes request JSON as a `Uint8Array` before calling `fetch`, so a naive fetch-hook may log bodies as `<>` — the wire content is still JSON matching the content-type (both directions). Auth is a session cookie set by `manus.im`, so in-browser calls Just Work if you're logged in — `fetch("/session.v1.SessionService/...")` from the Manus origin is fine, but cross-origin from a scraper is not. Services observed on the wire: | Service | Known methods | Purpose | | --- | --- | --- | | `session.v1.SessionService` | `ListSessions`, `GetSession`, `ShareSession`, `UpdateReadPosition` | Tasks (called "sessions" on the API). `GetSession` is the poll target for status. `ShareSession` flips public access and returns the share url. | | `orchestrator.v1.OrchestratorService` | `GetSession` | Runtime/plan state for an in-progress task. | | `cloud_pc.v1.CloudPCService` | `List` | Attached Cloud PC instances (sandbox VMs). | | `desktop.v1.DesktopService` | `GetDesktopDevices` | Enumerates desktop agent connections. | | `user.v1.UserService` | `WebdevUsageInfo`, `SetUserClientConfig`, `GetHelpCenterToken`, `PickEmailUsers` | User settings + quota. | | `user.v1.UserPublicService` | `GetGlobalSettings` | Public feature flags. | | `team.v1.TeamService` | `ListTeam` | Team membership. | To poll a task to completion without scraping the DOM: ```python # Browser fetch from inside the page — auth cookies are attached automatically status = js(r""" (async()=>{ const r = await fetch('https://api.manus.im/session.v1.SessionService/GetSession', { method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify({sessionId: location.pathname.split('/').pop()}) }); return {status: r.status, body: await r.text()}; })() """) ``` (Exact field names in the request payload are not documented publicly — inspect the outgoing request with the fetch-hook snippet below the first time you run against a new Manus build.) There is **no plain REST/JSON convenience layer** — every method is Connect. `http_get` against API routes will not work; you have to either drive the browser or speak Connect yourself. If you need a pure-HTTP client, open a PR with the Connect wire format once you reverse-engineer the request shapes. ## Path 2: Browser DOM submission ```bash browser-harness <<'PY' new_tab("https://manus.im/app") wait_for_load() wait(1.5) # SPA still hydrating; composer is a TipTap editor that mounts late # Locate the editor and click into it — the ProseMirror div is the contenteditable. # The composer mounts late; retry briefly if it isn't in the DOM yet. for _ in range(10): rect = js(r""" (()=>{const ce=document.querySelector('div.ProseMirror[contenteditable="true"]'); if(!ce) return null; const r=ce.getBoundingClientRect();return {x:r.x+r.width/2|0,y:r.y+r.height/2|0}})() """) if rect: break wait(0.5) assert rect, "ProseMirror composer never mounted — page probably failed to hydrate" click(rect["x"], rect["y"]) wait(0.3) type_text("Research the top 5 espresso machines under $500 and summarize tradeoffs") wait(0.4) # Submit via Enter — the editor has enterkeyhint="enter" and binds Enter to submit. # (Click the send button at the bottom-right of the composer if Enter is being blocked # by an autocomplete / slash-menu.) press_key("Enter") wait(3) print(page_info()) # url will be /app/ PY ``` ### Composer selectors The input is a **TipTap/ProseMirror** contenteditable, not a `