| 155 | // registry wins. |
| 156 | |
| 157 | export class AssistantDirectory extends Agent<Env, DirectoryState> { |
| 158 | initialState: DirectoryState = { chats: [] }; |
| 159 | |
| 160 | /** |
| 161 | * Shared workspace for every chat under this directory. Backed by the |
| 162 | * directory's own SQLite so all of a user's files live in one place — |
| 163 | * a `hello.txt` written in chat A shows up verbatim in chat B. |
| 164 | * |
| 165 | * Children (`MyAssistant` facets) see this workspace through the |
| 166 | * `SharedWorkspace` proxy below, which forwards each call to |
| 167 | * `readFile` / `writeFile` / etc. here. See `SharedWorkspace`. |
| 168 | * |
| 169 | * The `onChange` hook fires on every mutation (create/update/delete) |
| 170 | * regardless of which chat's tool caused it. We rebroadcast to every |
| 171 | * client connected to this directory — that's every browser tab the |
| 172 | * user has open — so live UI like the file browser refreshes across |
| 173 | * chats and tabs without polling. See `_broadcastWorkspaceChange`. |
| 174 | * |
| 175 | * Security note: this means any tool running inside any chat has |
| 176 | * read-write access to every file this user owns. That's the point — |
| 177 | * a multi-chat assistant should remember what it did in previous |
| 178 | * chats — but extensions declared with `workspace: "read-write"` |
| 179 | * inherit the same reach. If you fork this example for a |
| 180 | * less-trusted extension surface, add gating here. |
| 181 | */ |
| 182 | workspace = new Workspace({ |
| 183 | sql: this.ctx.storage.sql, |
| 184 | name: () => this.name, |
| 185 | onChange: (event) => this._broadcastWorkspaceChange(event) |
| 186 | // r2: this.env.R2 — uncomment to spill large files to R2. |
| 187 | }); |
| 188 | |
| 189 | /** |
| 190 | * Fan-out: push workspace change events to every client connected to |
| 191 | * this directory. Each chat pane's `useAgent` connection to the |
| 192 | * directory (via `useChats()`) receives these; the client side |
| 193 | * treats them as signals to refresh workspace-backed UI. |
| 194 | * |
| 195 | * Deliberately a best-effort `broadcast` (not `setState`), so file |
| 196 | * churn doesn't trigger full `DirectoryState` re-broadcasts on every |
| 197 | * write. Does NOT notify sibling child facets — no tool in this |
| 198 | * example reacts server-side to another chat's writes. Add a |
| 199 | * parent → child RPC here if that use case shows up. |
| 200 | */ |
| 201 | private _broadcastWorkspaceChange(event: WorkspaceChangeEvent): void { |
| 202 | this.broadcast(JSON.stringify({ type: "workspace-change", event })); |
| 203 | } |
| 204 | |
| 205 | onStart() { |
| 206 | this.sql`CREATE TABLE IF NOT EXISTS chat_meta ( |
| 207 | id TEXT PRIMARY KEY, |
| 208 | title TEXT NOT NULL, |
| 209 | updated_at INTEGER NOT NULL, |
| 210 | last_message_preview TEXT |
| 211 | )`; |
| 212 | this._refreshState(); |
| 213 | |
| 214 | // The directory owns cross-chat scheduled work. Facets can't |
nothing calls this directly
no test coverage detected