${escape(message.title)}
${message.text ? `${escape(message.text)}
` : ''} -${state.detail ? `${escape(state.detail)}` : ''}
+${detail ? `${escape(detail)}` : ''}
diff --git a/README.md b/README.md index 5470170..1b67d82 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,72 @@ # Agent Guild for VS Code -Watch your AI coding agents come alive in a 3D world, in a VS Code tab. Claude Code, Codex CLI, Antigravity CLI, Grok Build, Docker Agent and your own shell each get a hall, and every session is a character at work. +Run Claude Code, Codex CLI, Antigravity CLI, Grok Build and Docker Agent side by side in real terminals, and watch every session come alive as a character in a 3D world, in a VS Code tab or right beside your code. Each tool runs on your own account, just as it does in any terminal.  -## The Yard +## Get started + +1. Install this extension. +2. Click **Agent Guild** in the status bar, or open **Agent Guild** in the Explorer. +3. If Agent Guild isn't installed yet, choose **Install**. It runs `npm install -g @oddessentials/agent-guild` in a terminal and connects when it finishes. + +Press **New** on a tool's card to start a session, and sign in to the tool there the first time. + +## Your coding agents, side by side + +- Real terminals that keep running when you close the tab or VS Code. The tab comes back the next time VS Code starts. +- Rate-limit meters for Claude Code and Codex CLI, so you know how much headroom is left. +- Each session's helper agents and model, live on its card. +- Two sessions at once with **Split**, and earlier conversations back with **Resume**. + +| | Install from its card | Helper agents | Rate-limit meter | Resume | Several accounts | Model benchmarks | +| --- | :-: | :-: | :-: | :-: | :-: | :-: | +| Claude Code | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Codex CLI | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Antigravity CLI | Shows the command | | | ✓ | | ✓ | +| Grok Build | ✓ | With **Agent reporting** | | ✓ | ✓ | ✓ | +| Docker Agent | ✓ | ✓ | | ✓ | | | + +The **Shell** card runs your own shell, plus tmux (macOS and Linux) and herdr sessions that keep running when Agent Guild stops. Helper agents need a recent version of each tool: [setup per tool](https://github.com/oddessentials/agent-guild/blob/main/docs/agent-reporting.md). + +## Watch them work: the Yard - Every session is a character. It works at its tool's hall while its terminal is busy, and walks off to rest when it goes quiet. There is nothing to set up. -- Helper agents appear beside their character: Claude Code's, Codex CLI's and Docker Agent's on their own, Grok Build's once you turn on **Agent reporting** on its card. -- Halls glow while their sessions work, and the roster lists every tool and session. +- Helper agents gather beside their character, and halls glow while their sessions work. - Double-click a character to open its real terminal. - Six worlds: The Guild Yard, Operations Campus, Orbital Station, The Living Grove, Goblinville Works and Gnomeland Village.  -To open it, run **Agent Guild: Open the Yard** from the Command Palette. **Agent Guild: Open** then keeps opening the Yard, until you choose **Cards** under **Settings → View** in the tab. Pick a world under **Settings → Skin**. - -[Try the Yard in your browser](https://oddessentials.github.io/agent-guild/?view=yard) (simulated, nothing installed), or watch the trailer: +Run **Agent Guild: Open the Yard**, and pick a world under **Settings → Skin**. [Try the Yard in your browser](https://oddessentials.github.io/agent-guild/?view=yard) (simulated, nothing installed), or watch the trailer: [](https://www.youtube.com/watch?v=Zttp-vs_6z0) -## The tab +## The Yard beside your code + +Keep your agents in view while you work. Open **Agent Guild** in the Explorer, or drag it to the bottom panel. + + + +_Shown with demo sessions._ + +- It shows the Yard in the world and theme you chose in the tab. +- Double-click a character to open its terminal in the Agent Guild tab. +- It draws nothing while it's collapsed or hidden. + +## The Agent Guild tab -- **Agent Guild: Open** in the Command Palette, or **Agent Guild** in the status bar. -- If no session manager is running, one starts. If one is running, the tab connects to it. +- Opens from the status bar or **Agent Guild: Open**, and starts Agent Guild if it isn't running. - The same sessions stay open in your browser at the same time. -- Closing the tab or VS Code leaves your sessions running. The tab comes back the next time VS Code starts. - Links open in your default browser.  -## Everything else Agent Guild does +## More in Agent Guild -Agent Guild runs your coding tools side by side, in real terminals that keep running when the page closes. - -- One card per tool: install and update it, start sessions, and see its helper agents and the model in use. -- Two terminals side by side with **Split**. -- Rate-limit meters for Claude Code and Codex CLI. -- Resume a tool's earlier sessions, and see what it remembers and which instruction files it reads. -- Several accounts per tool, and model grades and benchmarks. -- GitHub issues, pull requests and workflow runs for the working folder. -- tmux and herdr sessions that keep running when the manager stops. +- See what Claude Code, Codex CLI and Grok Build remember, and which instruction files your tools read in the working folder. +- Grades and benchmarks for your tools' models. +- GitHub issues, pull requests, workflow runs and branches for your working folder, once you sign in to GitHub.  @@ -48,24 +74,55 @@ Agent Guild runs your coding tools side by side, in real terminals that keep run  +## Commands + +| Command | What it does | +| --- | --- | +| **Agent Guild: Open**, or **Agent Guild** in the status bar | Opens the tab, in the view you last chose | +| **Agent Guild: Open the Yard** | Opens the tab straight into the Yard | +| **Agent Guild** in the Explorer | The Yard beside your code | + +There are no settings to configure. + ## Requirements -- Node.js 22 or newer. -- Agent Guild 1.2.0 or newer: `npm install -g @oddessentials/agent-guild`. If it is missing, the tab offers **Install**, which runs that command in a terminal. If it is older, the tab says so and opens it in your browser, where **Update** is. **Agent Guild: Open the Yard** needs 1.3.0 or newer; with 1.2, choose **Settings → View → Yard** in the tab. - VS Code 1.85 or newer, or an editor built on it. -- For the Yard: WebGL, and a one-time 255 MB download of its art the first time it opens. Everything else keeps working meanwhile, and after that it works offline. Without WebGL, the tab offers **Show Cards**. +- Node.js 22 or newer. +- Agent Guild, installed with `npm install -g @oddessentials/agent-guild` or **Install** in the tab. +- For the Yard: WebGL, and a one-time download of about 255 MB of art the first time it opens. After that it works offline. -The extension has no settings. +| Feature | Agent Guild | +| --- | --- | +| The tab | 1.2.0 or newer | +| **Agent Guild: Open the Yard** | 1.3.0 or newer | +| The Yard beside your code | 1.5.0 or newer | + +With an older Agent Guild, the tab or the view says which update it needs. From 1.0.0 the version follows semantic versioning: a new major version only when the extension stops supporting a VS Code, Agent Guild or Node.js version listed here. +## Troubleshooting + +| You see | What to do | +| --- | --- | +| Agent Guild is not installed | Choose **Install**. The tab connects when it finishes. | +| Agent Guild is not running | Choose **Open Agent Guild**, which starts it or offers **Install**. | +| Agent Guild needs Node.js | Install Node.js 22 or newer, then choose **Try again**. | +| Agent Guild did not start | Run `agent-guild doctor` in a terminal to see why. | +| Agent Guild needs an update | Choose **Open in browser**, then **Update** there. | +| Agent Guild stopped responding | Choose **Retry** once it's running again, or **Open Agent Guild** to start it. | + ## Known limits -- While the Agent Guild tab has keyboard focus, VS Code's own shortcuts, such as the Command Palette, go to the page. Click outside the tab to use them. -- Dictate is not offered in the tab. Use it from the browser. -- In a remote window (SSH, WSL, a container), the tab shows the Agent Guild on your own computer, not on the remote one. +- While the Agent Guild tab or view has keyboard focus, VS Code's own shortcuts, such as the Command Palette, go to the page. Click outside it to use them. +- **Dictate** works in the browser, not in VS Code. +- In a remote window (SSH, WSL, a container), the tab and the view show the Agent Guild on your own computer, not on the remote one. - vscode.dev and other editors in a browser are not supported. +## Privacy + +The extension talks only to the Agent Guild on your own computer, and collects no telemetry. Neither does Agent Guild: [its privacy section](https://github.com/oddessentials/agent-guild#privacy) lists what it fetches, such as version checks and usage meters, and none of it carries your code or prompts. + ## Agent Guild: World [Agent Guild: World](https://agentguild.oddessentials.ai/), coming soon to Steam, shows the same sessions as a crew at work in a living 3D world, on Windows, macOS and Linux. It runs on Agent Guild, which stays free. @@ -84,4 +141,6 @@ Pull requests are squash-merged, and their titles are Conventional Commits. Merg ## License -[PolyForm Shield 1.0.0](LICENSE), as Agent Guild. +Free to use, and source-available under [PolyForm Shield 1.0.0](LICENSE), like Agent Guild. + +Claude Code, Codex, Antigravity, Grok and Docker are trademarks of Anthropic, OpenAI, Google, xAI and Docker. Agent Guild is not affiliated with or endorsed by them. diff --git a/media/view.png b/media/view.png new file mode 100644 index 0000000..43789c3 Binary files /dev/null and b/media/view.png differ diff --git a/package.json b/package.json index 346a265..67a5184 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,9 @@ "agent visualization", "animated characters", "subagents", - "three.js" + "three.js", + "companion", + "sidebar" ], "extensionKind": [ "ui" @@ -65,9 +67,20 @@ "main": "src/extension.js", "activationEvents": [ "onStartupFinished", - "onWebviewPanel:agentGuild.page" + "onWebviewPanel:agentGuild.page", + "onView:agentGuild.yard" ], "contributes": { + "views": { + "explorer": [ + { + "type": "webview", + "id": "agentGuild.yard", + "name": "Agent Guild", + "visibility": "collapsed" + } + ] + }, "commands": [ { "command": "agentGuild.open", diff --git a/src/extension.js b/src/extension.js index 0a9ba75..ae73b85 100644 --- a/src/extension.js +++ b/src/extension.js @@ -1,12 +1,19 @@ -// Agent Guild in an editor tab. The session manager owns the sessions; closing the tab or VS Code leaves them running. +// Agent Guild in an editor tab, and its Yard in a small view. The session manager owns the sessions; closing either or VS Code leaves them running. const vscode = require('vscode'); const { Panel, VIEW_TYPE } = require('./panel'); +const { YardView, VIEW_ID } = require('./yard'); function activate(context) { - const ready = new vscode.EventEmitter(); - const open = () => Panel.open(ready); - const openYard = () => Panel.open(ready, 'yard'); + const events = { + ready: new vscode.EventEmitter(), + viewReady: new vscode.EventEmitter(), + look: new vscode.EventEmitter(), + connected: new vscode.EventEmitter(), + }; + const open = () => Panel.open(events); + const openYard = () => Panel.open(events, 'yard'); + const yard = new YardView(context.globalState, events); const button = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); button.text = '$(organization) Agent Guild'; @@ -15,16 +22,17 @@ function activate(context) { button.show(); context.subscriptions.push( - ready, + ...Object.values(events), button, vscode.commands.registerCommand('agentGuild.open', open), vscode.commands.registerCommand('agentGuild.openYard', openYard), vscode.window.registerWebviewPanelSerializer(VIEW_TYPE, { - deserializeWebviewPanel: async (panel) => Panel.revive(panel, ready), + deserializeWebviewPanel: async (panel) => Panel.revive(panel, events), }), + vscode.window.registerWebviewViewProvider(VIEW_ID, yard), ); - // For the extension's own tests: open the tab and hear when the page reached its manager. - return { open, openYard, onReady: ready.event }; + // For the extension's own tests: open the tab and the view, and hear when their pages reached the manager. + return { open, openYard, yard, onReady: events.ready.event, onViewReady: events.viewReady.event }; } module.exports = { activate, deactivate() {} }; diff --git a/src/manager.js b/src/manager.js index 30398a4..5b7e376 100644 --- a/src/manager.js +++ b/src/manager.js @@ -9,6 +9,8 @@ const { spawn } = require('child_process'); const MIN_VERSION = '1.2.0'; /** The first manager whose page opens the view VS Code asks for. */ const YARD_VERSION = '1.3.0'; +/** The first manager whose page can fill a small view with the Yard. */ +const VIEW_VERSION = '1.5.0'; const PACKAGE = '@oddessentials/agent-guild'; /** The manager's per-user data folder, by the same rules as agent-guild's src/manager/config.mjs. */ @@ -109,6 +111,29 @@ function pageSrc({ url, token, version }, view) { return `${url}/#${hash}`; } +/** The page's address in the small view, in the tab's last look. */ +function compactSrc({ url, token }, look) { + const hash = new URLSearchParams({ token, layout: 'compact' }); + if (look) { + hash.set('skin', look.skin); + hash.set('theme', look.theme); + } + return `${url}/#${hash}`; +} + +/** The skin and theme a page reported, or null. */ +function readLook(value) { + const skin = value?.skin; + const theme = value?.theme; + if (typeof skin !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(skin) || (theme !== 'light' && theme !== 'dark')) return null; + return { skin, theme }; +} + +/** A session id from a page, or null. */ +function sessionId(value) { + return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value) ? value : null; +} + /** The absolute http(s) URL `href` names, or null. */ function webUrl(href) { try { @@ -119,4 +144,6 @@ function webUrl(href) { } } -module.exports = { MIN_VERSION, YARD_VERSION, PACKAGE, dataDir, readManager, health, newEnough, locate, startFailure, pageSrc, webUrl }; +module.exports = { + MIN_VERSION, YARD_VERSION, VIEW_VERSION, PACKAGE, dataDir, readManager, health, newEnough, locate, startFailure, pageSrc, compactSrc, readLook, sessionId, webUrl, +}; diff --git a/src/panel.js b/src/panel.js index bd9c020..8456efb 100644 --- a/src/panel.js +++ b/src/panel.js @@ -2,7 +2,7 @@ const vscode = require('vscode'); const crypto = require('crypto'); -const { MIN_VERSION, YARD_VERSION, PACKAGE, health, locate, newEnough, pageSrc, webUrl } = require('./manager'); +const { MIN_VERSION, YARD_VERSION, PACKAGE, health, locate, newEnough, pageSrc, readLook, webUrl } = require('./manager'); const VIEW_TYPE = 'agentGuild.page'; const NODE_URL = 'https://nodejs.org/en/download'; @@ -11,34 +11,36 @@ class Panel { /** The one open tab, if any. */ static current = null; - /** Bring the tab forward, connecting first if the manager went away or moved, and show `view` if one is asked for. */ - static async open(ready, view) { + /** Bring the tab forward, connecting first if the manager went away or moved, and show `view` or open `session` if asked. */ + static async open(events, view, session) { if (Panel.current) { Panel.current.panel.reveal(); - return Panel.current.request(view); + return Panel.current.request(view, session); } const panel = vscode.window.createWebviewPanel(VIEW_TYPE, 'Agent Guild', vscode.ViewColumn.Active, { enableScripts: true, retainContextWhenHidden: true, }); - const tab = new Panel(panel, ready); + const tab = new Panel(panel, events); tab.view = view; + tab.session = session; return tab.connect(); } /** A tab VS Code brought back after a restart. */ - static revive(panel, ready) { - return new Panel(panel, ready).connect(); + static revive(panel, events) { + return new Panel(panel, events).connect(); } - constructor(panel, ready) { + constructor(panel, events) { Panel.current?.panel.dispose(); Panel.current = this; this.panel = panel; - this.ready = ready; + this.events = events; this.state = null; this.connecting = null; this.view = null; + this.session = null; panel.iconPath = vscode.Uri.joinPath(vscode.Uri.file(__dirname), '..', 'media', 'icon.png'); panel.webview.options = { enableScripts: true }; panel.onDidDispose(() => { if (Panel.current === this) Panel.current = null; }); @@ -66,12 +68,16 @@ class Panel { return this.connect(); } - /** A view asked for while the tab is open: the page is loaded again only if its manager moved. */ - async request(view) { + /** A view or session asked for while the tab is open: the page is loaded again only if its manager moved. */ + async request(view, session) { this.view = view; + if (session) this.session = session; await this.refresh(); - const asked = this.state?.state === 'ready' && this.take(); + if (this.state?.state !== 'ready') return; + const asked = this.take(); if (asked) this.panel.webview.postMessage({ kind: 'view', view: asked }); + const id = this.takeSession(); + if (id) this.panel.webview.postMessage({ kind: 'session', id }); } /** The asked-for view, once. A blocked tab keeps it until the page can show it. */ @@ -83,15 +89,36 @@ class Panel { return null; } + /** The asked-for session, once. A blocked tab keeps it until the page can open it. */ + takeSession() { + const id = this.session; + this.session = null; + return id; + } + show(state) { this.state = state; const nonce = crypto.randomBytes(16).toString('hex'); - this.panel.webview.html = state.state === 'ready' ? pageHtml(pageSrc(state, this.take()), state.url, nonce) : messageHtml(state, nonce); + if (state.state !== 'ready') { + this.panel.webview.html = messageHtml(state, nonce); + return; + } + this.panel.webview.html = pageHtml(pageSrc(state, this.take()), state.url, nonce, this.takeSession()); + this.events.connected.fire(state); } receive(message) { + // A frame that finished loading may show the browser's error page: keep it only if its manager still answers. + if (message?.kind === 'frame') { + this.refresh(); + return; + } if (message?.kind === 'page') { - if (message.type === 'ready') this.ready.fire(message.version); + if (message.type === 'ready') this.events.ready.fire(message.version); + if (message.type === 'look') { + const look = readLook(message); + if (look) this.events.look.fire(look); + } if (message.type === 'open') { const url = webUrl(message.url); if (url) vscode.env.openExternal(vscode.Uri.parse(url, true)); @@ -138,7 +165,7 @@ class Panel { const escape = (text) => String(text).replace(/[&<>"']/g, (c) => `${c.charCodeAt(0)};`); -function pageHtml(src, url, nonce) { +function pageHtml(src, url, nonce, session) { return `
@@ -147,26 +174,28 @@ function pageHtml(src, url, nonce) { const vscode = acquireVsCodeApi(); const frame = document.getElementById('page'); const origin = ${JSON.stringify(url)}; -// The page listens for a view once it has loaded. +// The page listens for requests once it has loaded. let loaded = false; -let waiting = null; -const showView = (view) => frame.contentWindow.postMessage({ source: 'agent-guild-vscode', type: 'view', view }, origin); +const waiting = ${JSON.stringify(session ? [{ type: 'session', id: session }] : [])}; +const send = (message) => frame.contentWindow.postMessage({ source: 'agent-guild-vscode', ...message }, origin); +const deliver = (message) => (loaded ? send(message) : waiting.push(message)); frame.addEventListener('load', () => { loaded = true; - if (waiting) showView(waiting); - waiting = null; + waiting.splice(0).forEach(send); + vscode.postMessage({ kind: 'frame', type: 'load', page: '${nonce}' }); }); // Only the framed page's own messages are passed on. addEventListener('message', (event) => { if (event.source !== frame.contentWindow) { - const view = event.data?.kind === 'view' && event.data.view; - if (view === 'cards' || view === 'yard') loaded ? showView(view) : (waiting = view); + const data = event.data; + if (data?.kind === 'view' && (data.view === 'cards' || data.view === 'yard')) deliver({ type: 'view', view: data.view }); + if (data?.kind === 'session' && typeof data.id === 'string') deliver({ type: 'session', id: data.id }); return; } if (event.origin !== origin) return; const data = event.data; if (!data || data.source !== 'agent-guild') return; - vscode.postMessage({ kind: 'page', type: data.type, url: data.url, version: data.version }); + vscode.postMessage({ kind: 'page', page: '${nonce}', type: data.type, url: data.url, version: data.version, skin: data.skin, theme: data.theme, session: data.session }); }); `; } @@ -198,6 +227,11 @@ function messageHtml(state, nonce) { actions: [['browser', 'Open in browser'], ['retry', 'Try again']], } : MESSAGES[state.state]; + return noticeHtml(message, nonce, state.detail); +} + +/** A notice whose buttons post `{ kind: 'action', action }`. */ +function noticeHtml(message, nonce, detail) { const buttons = message.actions.map(([action, label], i) => ``).join(''); return ` @@ -214,7 +248,7 @@ button:focus-visible{outline:1px solid var(--vscode-focusBorder);outline-offset:${escape(message.text)}
` : ''} -${state.detail ? `${escape(state.detail)}` : ''}
+${detail ? `${escape(detail)}` : ''}