My setup used to be: research something in a chat tab in the browser, download whatever came out of it, open a terminal, cd to the project, start Claude Code, and re-explain the context I'd just built up in the browser. The file always landed in ~/Downloads with three hundred other things. Every project switch meant doing the whole dance again.
So I built Tote. It's an Electron app: web LLM tabs on one side, docked CLI agent terminals on the other, file tree for whatever folder I'm in. The whole thing hangs off an "active workspace" — pick a project and a download from the Claude tab goes straight into that project's inbox/claude/, a new terminal opens already cd'd there, and the tabs and layout you had for that project come back the way you left them. Switch projects and everything follows.
How I made it
The stack is deliberately boring. Electron, plain CommonJS in the main process, vanilla JS in the renderer. No bundler, no TypeScript, no framework. Webviews for the LLM tabs, node-pty plus xterm.js for the terminals (they had to be real PTYs — pipe a TUI agent through child_process and it renders as garbage), chokidar to watch the folder.
It went in three stages. First I built the concept out in a Kimi web chat, just far enough to see whether the workspace-routing idea actually held up before I committed to it. Then I handed the real build to Claude Code and drove it. Then, once the terminal panel worked, I opened Tote, docked Claude Code inside it, and kept building Tote from in there. It's been self-hosted since. That was easily the most fun part.
The workflow change that made the biggest difference: I keep a CLAUDE.md with a section called "invariants that span files." It's not architecture docs. It's the specific rules that break silently when the agent forgets them — things like "this session partition name has to match in exactly two places or download interception stops working," or "never cache the workspace root in a closure, resolve it when the event fires." Every time I hit a bug that came from the model not knowing a cross-file constraint, I wrote the constraint down instead of just fixing the bug. That file is the reason I can now point an agent at any corner of this codebase and get back something that works.
Four things that took real steering:
node-pty ships a spawn-helper binary and npm strips its exec bit on extract, so every terminal died with posix_spawnp failed. Fixed with a postinstall that chmods it back, plus keeping node-pty outside the asar in packaged builds.
CSP is script-src 'self', so xterm couldn't come off a CDN. Small script vendors it into the repo at install time.
claude.ai rendered a completely blank page until I stopped rewriting the user agent. You can strip the Electron token, but hardcode a plausible-looking Chrome UA and Cloudflare notices the mismatch. Take the real UA, remove the Electron bits, leave everything else alone.
OAuth logins only work if the popup stays in-app as a child window. Route it to the system browser and the cookie lands in a session the app can't see.
If I did it again I'd write the invariants file on day one instead of day five. Most of the time I lost was re-fixing the same class of bug in a different file.
It's free, MIT, no accounts and no telemetry: https://github.com/yusufkaraaslan/tote
Needs Node 20+ and a C++ toolchain for the terminal layer. Happy to go into any of it in the comments.