Workspace vs Document Assistant: Why You Need Both

Workspace vs Document Assistant: Why You Need Both

One of the hardest things about AI chat in a document app is keeping the conversation going.

You're editing a doc. You ask the AI to rewrite a section. It does. You refine. It adjusts. Then you close the tab. Next time you open that doc, you're in a different chat thread — or worse, no thread at all. The context is gone. The back-and-forth that got you to the right result? Scattered across different conversations.

That loss hurts. Some of my best work comes from iterating in place, not starting fresh each time.

So we built two distinct assistants: workspace chat and document chat. Each has a role. And the document-level conversation becomes context for the universal workspace assistant when you need to zoom out. Here's the architecture and how they connect.

The problem with one chat to rule them all

When everything lives in a single conversation surface:

  • Context dilutes. Questions about "that doc I was working on" mix with "add this to my todo" and "summarize this PDF." The model has to hold too much in its head.
  • Continuity breaks. If each chat is ephemeral or scoped to "session," you lose the thread. You can't say "remember when we simplified that intro?" because that chat is gone.
  • Scope is ambiguous. Does "edit this" mean the current paragraph, the current doc, or something in a different doc? The assistant has to guess.

The alternative — one global chat — doesn't solve it. It just shifts the problem: now the model has to infer which document you mean, and you have no way to build a long-running conversation tied to a specific piece of work.

Architecture overview

flowchart TB subgraph Client WA["Workspace Chat Input"] DC["Document Chat Input"] ED["Editor with live doc content"] end subgraph "Routing" R{"Has documentId and doc-scoped context?"} end subgraph "Document Chat API" DAPI["Document-scoped route"] DBC[(Messages per document)] end subgraph "Workspace Chat API" WAPI["Workspace route"] TD["User tags documents or folders"] end subgraph "Context injection" EB["editorBlocksContext - live doc snapshot"] SD["Server fetches searchable_content from DB"] end DC --> R WA --> R R -->|"Yes - document context"| DAPI R -->|"No - workspace context"| WAPI DAPI --> EB DAPI --> DBC ED --> EB TD --> WAPI WAPI --> SD SD --> WAPI

Document chat: conversation tied to the document

Document chat is scoped to a single document. When you open a doc and use the chat panel, every message lives in that doc's conversation.

That means:

  • Continuity. "Make it shorter" refers to the section you were just editing. "Add a bullet list here" has clear placement. The model sees the doc content and the full history of your conversation about it.
  • Persistence. Close the doc, come back later — the chat is still there. No "start over" moment.
  • Focus. The assistant only has to reason about one document. Simpler prompts, clearer intent, fewer mistakes.

Document chat uses a separate API route and requires a documentId. The editor passes a live snapshot of the document — editorBlocksContext — so the assistant always sees the current state. When you select text and send it to chat, that selection becomes an inline context pill, keeping everything anchored to the doc.

Workspace chat: the bird's-eye view

Workspace chat is different. It's for cross-document work: planning, todos, "which doc was I thinking about?," organizing, and questions that span multiple notes.

The workspace assistant doesn't need to see every doc's raw content. It needs to know what docs exist, what they're about, and — when relevant — what's in them. So how does it get that information?

How workspace chat gets document context

Workspace chat gets document content through tagging. The user tags specific documents or folders in the chat input. The client sends those IDs; the server fetches the content and injects it into the prompt.

1. Client sends tagged document IDs

When the user adds a document or folder to the chat context, the client includes taggedDocumentIds and optionally taggedFolders in the request body:

// Client: include tagged docs when sending to workspace chat
const requestPayload = {
  messages: [...],
  body: {
    contextType: 'workspace',  // signals workspace context, no documentId required
    taggedDocumentIds: ['doc-uuid-1', 'doc-uuid-2'],
    taggedFolders: [{ id: 'folder-uuid', name: 'Research' }],
  },
};

2. Server determines context type

The API route distinguishes workspace vs document context up front. Workspace requests don't require a documentId; document-scoped requests do. Only document-scoped messages get persisted to the per-document messages table.

// Server: route by context type
const isWorkspaceContext =
  !documentId ||
  contextType === 'workspace';
 
const isDocumentContext = !!documentId && !isWorkspaceContext;
 
// Workspace: skip persisting to document messages table
// Document: persist to messages for that documentId

3. Server fetches document content and injects into the prompt

For workspace chat, when the user has tagged documents, the server fetches searchable_content (a text representation of each document) from the database and builds a context string. That string gets pushed into the system prompt so the model can see the content without the client having to send it:

// Server: fetch tagged doc content and inject as system context
if (taggedDocumentIds?.length > 0) {
  const { data: docs } = await supabase
    .from('documents')
    .select('id, name, searchable_content')
    .in('id', taggedDocumentIds);
 
  const contentString = docs
    .map(doc => `[Content from document: ${doc.name}]\n${doc.searchable_content || ''}`)
    .join('\n---\n');
 
  messagesForAI.push({
    role: 'system',
    content: `IMPORTANT CONTEXT FROM TAGGED DOCUMENTS:\n${contentString}`,
  });
}

Tagged folders work the same way: the server fetches all documents in those folders and injects their searchable_content. The workspace assistant then has access to the full text of whatever the user tagged — without document chat needing to be involved. The document content lives in the DB; workspace chat pulls it on demand.

How document chat differs

Document chat receives live editor state from the client — editorBlocksContext — which is a structured snapshot of the current document blocks. The server doesn't fetch from the DB for the primary doc; the client sends the freshest state. That keeps document chat in sync with what the user is seeing, including unsaved changes.

Workspace chat, by contrast, pulls from the database. Documents are stored with searchable_content (typically a flattened text version for search and context injection). So workspace chat gets "what's been saved"; document chat gets "what's on screen right now."

Data flow summary

Document chatWorkspace chat
Context sourceeditorBlocksContext from client (live)searchable_content from DB (tagged docs)
Requires documentIdYesNo
Messages persistedPer documentNot to document table
Primary useEdit, refine, iterate on one docCross-doc questions, todos, planning

What we learned

The main insight: continuity is a feature. Users don't want to re-explain their doc every time. They want to pick up where they left off.

Document chat gives that. Workspace chat gives the overview. The connection between them: document content is stored (with searchable_content); when the user tags a doc in workspace chat, the server fetches that content and injects it. Document chat's context stays in the document; workspace chat's context comes from tagging. Together they're richer than either alone — document-level iteration stays focused, and the workspace can pull in whatever documents the user needs for the bigger picture.