DOC20_R4_3.md
Current Specs/DOC20/DOC20_R4_3.md
# DOC20 — Project, Browser, Notes, and Document Viewer Addendum
**Version:** R4.3
**Date:** 2026-04-09
**Status:** Consolidated implementation-ready revision — R4.3 supersedes all prior versions on all subjects
**Scope:** Unified Workspace shell, Browser, projects, DOC7 project integration, notes, To-Do system, Calendar module, Floating Palette, agent awareness, AI wiring contract, data schemas, command contracts, Document Viewer, Embedded Web Browser, Session System, Icon System, Split View, Notification System, Skills & Connectors page
**Intent:** Implementation-complete spec for Q/EC/OpenClaw coordination — a coding agent can build from this document without guessing
**Applies to:** Q Dashboard, EC durable state, DOC7 integration surfaces, project-scoped work, note editing and export, document viewing and review, unified tab system, navigation architecture, floating palette, to-do lists, calendar events
---
## CHANGELOG
| Date | Inputs | Areas Updated | Summary |
|---|---|---|---|
| 2026-04-09 | Skills & Connectors page design (proposal V1, mockup iteration, OpenClaw ecosystem research) | §6.29 (new — 30+ subsections), §6.18.2 (content registry), §8 (commands), §12 (cross-doc) | R4.3: Skills & Connectors Page — new Q content surface for unified capability management. 6 tabs: My Capabilities (unified browse of all capability types with CapabilityCardSchema read-model, 7-section detail drawer with confidence history / usage timeline / provenance / linked entities, inline edit for learned procedures via BundleEditOperation), Connectors & Accounts (MCP server management, OAuth account connections, multi-account, health monitoring), OpenClaw Tools (ClawHub catalog with 13,700+ skills, plugin management, install/update/uninstall), Expose Elnor (reverse MCP server with exposure scope / knowledge scope / action permissions / audit trail), Learn & Teach (DOC3 learning flow surfaces), Hub (Phase 2 placeholder). Backend split: DOC16 Entry 16.7 owns connector lifecycle / credential management / reverse MCP config; DOC11 owns gateway tool catalog routes and reverse MCP runtime; DOC24 owns capability registry queries and MCP registration; DOC72 owns procedure graph queries. Action matrix defines per-type CRUD (learned procedures: full edit/promote/archive/export; native skills: edit/disable/uninstall; MCP tools: read-only with connector link; system tools: read-only with settings link). Usage data from ProcedureExecutionOutcomeEvent records. |
| 2026-03-12 | Design discussion, DOC7, existing project specs | All | Initial draft R1 |
| 2026-03-13 | Design review, DOC10 R11, DOC15 R7.1, DOC18 R2, Ableton Live browser reference, notes architecture discussion | §3-§6, §10 | R1.1 comprehensive revision |
| 2026-03-14 | Design iteration session (Claude), mockup artifacts | §3 Browser, §4 Projects, §6 Comments/Tracked Changes, new §6.15 AI Wiring | R1.2: visual design pass — single-line browser, Home tab, highlight-to-comment, Review dropdown, author-colored changes, AI payload schemas, multiple overlays, archive/delete semantics |
| 2026-03-15 | Implementation completion redline (ChatGPT), folders feature design, self-audit | All sections | R1.3: implementation-complete — exact TypeScript schemas for all data contracts, browser state model + query contract, change-set tracked changes, comment anchor remapping, note storage layout + autosave, project schema + creation flow + membership table, Places/linked-folder wiring, Folders scope, command envelopes, deep-link routing, acceptance criteria additions |
| 2026-03-16 | Multi-session mockup iteration (Claude + Will), Browser V4, Notes V5, Document Viewer V6 | §3 Browser, §6 Notes, §6.16 Document Viewer, §7-§8 Contracts, §9-§13 | R1.4: Artifact Viewer → Document Viewer (any file type). Tabbed right panel (Comments + Send to Agent). Two-tier Send to Agent (Respond in Chat / Send with Instructions + result formats including Respond in Comments). Select-comments scope. Per-comment send icon. Folder overlay toggle. Browser: scope deselection, 5 sort options, 14 type chips, collections filter, archive/delete, footer. Notes: search/sort, archive/delete, deep subfolders, user-colored tracked changes, +Folder/+Note buttons, Send to Agent drawer with chat selector. Full comment interaction wiring (Reply/Edit/Resolve/Reopen/Delete). Immutable versioning model. Reference ID in all send payloads. Responsive toolbar collapse. |
| 2026-04-05 | Multi-session Q Unified Workspace mockup evolution (V4→V7.3, 9 font variants), 37-item instruction document, 44-item change audit | §0, §1, §3.4, §3.6, §6.15.10, §6.19.9, §6.20 (new — 28 subsections), §7, §8, §9, §13 | R3: Q Unified Workspace Shell — the workspace evolves from a "three-view" (notes, documents, web) to the primary Q app interface. New §6.20 defines: unified tab system supporting 8 content types (note, doc, web, clips, chat, room, task, utility) with type-specific colors and icons; Nav tab as 4th browser mode replacing the left sidebar menu (conversations, activity, pages, open tabs); transient utility tabs with blue outline border, leftmost insertion, one-at-a-time replacement, persist-on-navigate (no auto-close), right-click Pin; duplicate tab prevention; left nav rail simplified to 6 items (Q logo, browser toggle, bell with badge, settings, chat column toggle); right chat column (320px, independent of Ask panel, at app right edge); Ask Agent panel: context card, Include checkboxes, attachment button, inline agent response with "Continue in full chat →"; Chats management page (search, filters, star toggle); session system (auto-create on launch, "Clips: {M.D}-{N}" naming, close on quit/idle/manual); expanded [+] dropdown with Create/Open/Saved Groups sections. Browser: Folders as scope (not overlay), resizable splitter (150px default, 400px max, 6px handle), Panel and Forum type chips added (17 total), scope chips reduced to 5 (Folders button separate). Notes browser: folders-at-top + flat-all-notes-below layout, independent sort dropdowns, single-line note rows, draggable folders, folder rename/delete. Bookmarks: full Chrome-like CRUD with favicon squares. Clips unified with notes (same renderer, toolbar, blocks). IBM Plex Sans as working font. Web tab favicons. Tab bar design tokens codified. |
| 2026-04-03 | Q Browser v2 design session — tabs, privacy, credentials, downloads, reader mode, clippings, context pinning, DOC72 signal architecture | §6.19 (major expansion, 14 new subsections) | R2.3: Q Browser Full Feature Spec — tabs and tab groups, URL-as-search-bar, cookie/privacy controls with domain exclusion list, incognito mode, downloads manager, credential vault with macOS Keychain encryption and Elnor permission model, personal info autofill, Chrome extension loading, native ad blocking, reader mode, Send to Chat, clippings-to-note, pin-as-context (bucket or session), screenshot/full-page PDF capture, PDF interception to Document Viewer, custom context menus, zoom controls, DOC72 signal emission (BrowserPageVisitSchema + M365PageMetadataSchema) with exclusion-list filtering, WebBrowserReadSkill for Elnor awareness, browser settings page, profile export/import. Bookmark folders. Audio handling. All Chrome-equivalent behaviors for standard browser operations. |
| 2026-03-28 | Embedded browser concept exploration, Electron feasibility analysis, mockup (Q_BROWSER_VIEW_V1.jsx) | §6.19 (new section) | R2.2: Embedded Workspace Browser — §6.19 added (11 subsections, ~220 lines). Full web browser within Q using Electron `<webview>`. Reuses existing interaction surfaces (bubble menu, comment rail, Ask Elnor panel, Save as Artifact/Note). Content extraction via mozilla/readability for Elnor context fusion. Electron-dependent — hidden in web app mode. WebBookmark and WebPageSave commands. Bookmarks, passwords, tabs, agent-driven browsing explicitly deferred to v2. Dual capability model: web app (default) + Electron desktop app (optional). |
| 2026-03-19 | 5-reviewer red-team cycle + self-audit + E2E wiring audit | All sections | R2: Contract Closure — full enum canonicalization (context_bucket→bucket, generated_artifact→artifact across all tables/maps/routes). AttachmentSchema split to blob+ref. 20+ missing commands added (ModulePreset CRUD, FeedRefresh, DocumentSelectionAI, NoteCommentReanchor, TrackedChangeOverlapResolve, NoteAIPreviewAccept/Reject, NoteAICancel, NoteTemplateSave, ProjectDuplicate, PlaceRename, TodayNoteResolve, Recently Deleted restore/permanent-delete). InlineThread dispatch clarified (compound command). Agent feed response format contract. move_to_folder phantom fixed. RendererCapabilities reconciled with docx entry. Context menu routing table rewritten with exact command names. Export format matrix. DnD state machine. ArtifactVersionSchema + read contract. Iframe sandbox. Tracked-change flush-first rule. DOC20Settings command. 8 E2E gaps closed. |
| 2026-03-19 | Feed architecture redesign, +Module toolbar, block spacing, mockup V5–V5.2 iteration | §6.2A.3 ActivityFeed, §6.2A.6 Block insertion, §6.2C ModulePreset, §6.2B Today note, §6.3.1 Storage, §7.6A–B Schemas | R1.8: Feed Architecture + Module Presets — ActivityFeed blocks rewritten from cron/subscription model to lazy-evaluation (refresh-on-view, zero background cost). Two source types: system events (filtered from EC activity_log.jsonl via WebSocket) and agent feeds (single agent call on stale interval). ModulePreset schema and library (§6.2C). Pre-built presets: System Activity, System Notices, Gate Approvals, Active Operations (system), Morning Summary, Email Watch, Deadline Tracker (agent). +Module toolbar button replaces + inserter. Block spacing: 8px margin above/below, no auto-inserted spacers. Feed dormancy after configurable expiry (default 15 days). Feed cache at notes/{note_id}/feed_cache/. |
| 2026-03-19 | Workspace mockup iteration (Claude + Will), block architecture design session | §3.4–3.5 Browser scopes, §6.1–6.2 Notes model, §6.15 Notes AI, §7 Schemas, §8 Commands, §11–§13 | R1.7: Block-Based Workspace Architecture — notes become block-based modular documents. Block types: Text (implicit), TaskList, ActivityFeed, InlineThread, ConfigurableBar. + inserter between blocks, /slash commands, drag reorder, collapse/expand. Today note as workspace home page with rollover behavior. Notes scope added to browser with dedicated folder tree (replaces separate notes sidebar). Inline threads as durable comments with display_mode. @mention agent invocation. Unified note model: to-do lists are notes with TaskList blocks, no separate to-do schema. New commands: BlockInsertCommand, BlockDeleteCommand, BlockReorderCommand, TodayNoteRolloverCommand, InlineThreadCreateCommand. |
| 2026-03-19 | 5-reviewer red-team cycle (ChatGPT, Grok, Gemini, Codex, Claude Code) + self-audit | §3 Browser, §6.6 Comments, §6.10 Tracked Changes, §6.15 Notes AI, §6.16 Document Viewer, §6.18 Content Map, §7 Schemas, §8 Commands, §12 Cross-doc | R1.6: Contract Hardening — Canonical Enum Lock (§7.0A), ECCommandResult response envelope + idempotency, per-command error tables, Browser Context Menu Routing Table (§8.8), CommentAnchor discriminated union, RendererCapabilities, NoteReviewRequest + NoteReviewCommand, note write serialization, BrowserResolver SLOs, archive/delete/trash model, orphaned comment display, tracked-change overlap policy, stale reference UX, artifact creation heuristics, version major/minor rules, Save as Prompt modal, full-text note search, table support, NoteDuplicateCommand, 4 ArtifactDiff commands, empty states, cross-doc obligations expanded. |
| 2026-03-16 | Cross-doc analysis (EC Core, DOC7, DOC12, DOC15, DOC17, DOC3, DOC4, DOC18, DOC16) | §6.18 Unified Content Map, §7.18-7.19 Contracts, §8.6B-8.6C Commands, §12-§13 | R1.5: Unified Content Map (§6.18) — complete registry of 16 browsable + 20 system-internal content types cross-referenced from 9 specs. Artifact creation pipeline (chat output → durable artifact → DocIndex). Attachment model (reference-not-copy, dedup by hash). Browser discovery aggregation (BrowserResolver reads 13+ sources). Elnor content awareness (4-layer: DocIndex → QMD → DOC18 → DOC15). Lifecycle rules (delete/archive/orphan). Save as Prompt (→DOC17) and Save as Artifact from chat flows. Content Registration check added to cross-doc review process. |
| 2026-04-06 | Complete spec delta V7.4→V7.11 (527-line delta covering 8 mockup versions), DOC72 R5.6 entity mapping discussion, DOC24 R2.4 delivery architecture | §0.4, §1.6, §6.1 (nav rail), §6.3 (tabs), §6.4 (browser column), §6.12 (doc title), §6.14 (note canvas modules), §6.15 (chat tab), §6.16 (ask panel), §6.17 (status bar), §6.19.9 (bookmarks bar), §6.20 (major expansion — 20+ subsections), §6.21 (new — To-Do System), §6.22 (new — Calendar Module), §6.23 (new — Icon System), §6.24 (new — Split View), §6.25 (new — Notification System), §6.18.2 (content registry), §7 (schemas), §8 (commands) | R4: Floating Palette V2 + To-Do + Calendar + Icon System + Split View + Notifications — Floating palette redesigned from 3 tabs to sidebar architecture (☰ sidebar with Command bar, Notices, Activity, Quick Actions + 3 persistent content tabs Chat/Note/To Do). Unified To-Do system with shared `fpTodoLists` data pool across 3 view surfaces (palette, note modules, standalone tabs). Calendar module (Month/Week/Day/List views, Outlook integration via DOC16, event editor, agent instructions). Two-layer data architecture: EC application tables (Layer 1) + DOC72 entity graph extraction (Layer 2) for to-do items → `obligation` nodes, calendar events → `obligation` nodes, lists → `work_product` nodes. Complete SVG icon system replacing all emoji (Heroicons primary, Lucide/Tabler selective). Chrome-style tab bar (white active tabs, proportional shrinking, `display:contents` groups, colored outlines). Split view (independent dual panes, draggable divider). Linear-style notification inbox with source-colored cards, snooze, silent mode, delivery matrix. Pin (always-on-top) toggle. Document title right-click menu. Zoom slider in status bar. Per-tab right panel state. Tab icon format migration (emoji → string IDs). Module block type `tasks` → `todo`. Browser scope expansion (Notes tab scope toggles: Notes/To Do/Cal). Ask button simplification (per-task removed, list-level kept). Naming: "Conversations" → "Chats", "Knowledge Manager" → "Knowledge". Tab colors: web → slate gray, chat → dark blue, todo → teal (new). |
| 2026-04-07 | Architecture discussion — centralized data model, multi-surface rendering, networking readiness, agent contention, DOC72 intake contracts | §2.4 (new), §2.5 (new), §2.6 (new), §6.21.2 (schema expanded), §6.21.7 (new), §6.21.8 (new), §6.22.4 (schema expanded), §6.22.8 (new), §6.22.9 (new), §12 (updated) | R4.1: Centralized Data, Multi-Client Architecture, and Surface Intake Contracts — Establishes the surface independence principle: every Q rendering surface is a stateless viewport subscribing to EC-owned durable state. Defines three-tier content sync matrix. Specifies EC subscription contract (surface registration, push channel, optimistic updates, note edit lock, agent busy indicators). Expands TodoList/TodoTask schemas with `project_id`, `tags`, `attachments`, `done_at`, `created_at`, `updated_at` and TodoAttachment schema. Expands CalendarEvent schema with `event_type` (10 categories), `participants`, `attachments`, `project_id`, `tags`, `source`, `external_id`. Adds signal emission and DOC72 integration subsections for To-Do (§6.21.7, deterministic + entity resolution) and Calendar (§6.22.8, deterministic + entity resolution). Identifies Notes intake gap (§6.22.9 — LLM-assisted extraction needed, cross-doc to DOC72). Produces separate DOC72 Proposal: Surface Intake Contracts specifying full intake contracts for all four surfaces. Flags DOC25 (proposed) for server-side sync. |
| 2026-04-07 | Entity resolution hierarchy clarification, project_id vs matter distinction | §6.21.2 (updated), §6.21.7 (updated) | R4.2: Entity Resolution Hierarchy — Establishes that individual task `text` is the primary entity resolution signal (not the list name). Each task item ("Prepare expert report in Paramount") is resolved independently for matter/case, people, document types, and actions. List `name` is contextual — provides temporal framing ("February 8") or matter framing ("Henderson MTD Prep") that boosts confidence on task text matches. Subtask text inherits parent task's resolved entity context. Explicit separation: `project_id` is organizational grouping; matter/case association is independent via DOC72 entity resolution. Extraction table reordered by priority. DOC72 Proposal updated to V2 with matching hierarchy. |
---
## 0. Preamble
### 0.1 Purpose
This addendum defines the visible UX and implementation contract for fourteen closely related surfaces:
1. the **Q Unified Workspace** shell — tab system, navigation, layout, session system (§6.20, R3);
2. the **Browser** column in Q — including the Nav tab (R3);
3. **Projects** and the Project page;
4. **DOC7 Context Buckets as used by projects**;
5. the first-class **Notes** system — including clips (unified with notes, R3);
6. **Places**, **Linked Folders**, and **Folders** as organizational and file-access surfaces;
7. the **Document Viewer**;
8. the **Embedded Web Browser** (§6.19);
9. the **Floating Palette** — always-on-top mini workspace with Chat, Note, To Do tabs and sidebar (§6.20.30, R4);
10. the **To-Do System** — unified data pool with 3 view surfaces (§6.21, R4);
11. the **Calendar Module** — embeddable calendar with Outlook integration (§6.22, R4);
12. the **Icon System** — SVG icon components replacing all emoji (§6.23, R4);
13. the **Split View** — independent dual panes with draggable divider (§6.24, R4); and
14. the **Notification System** — Linear-style inbox, delivery matrix, silent mode (§6.25, R4); and
15. the **Skills & Connectors** page — unified capability management, connectors, OpenClaw tools, reverse MCP, learning flows (§6.29, R4.3).
This document is intentionally implementation-heavy. Its purpose is to make these surfaces concrete enough that a coding agent can build them without guessing at schemas, state shapes, command payloads, or interaction sequences.
### 0.2 Non-negotiable architecture rules
The following existing suite rules remain in force:
- **EC is the single durable writer.** Q never writes durable project, bucket, note, collection, saved view, folder, or file-link state directly.
- **OpenClaw autonomy must be preserved.** OpenClaw continues to own native tool execution, browser/desktop/file actions, and terminal execution. DOC20 may guide and trigger those actions, but must not over-micromanage them.
- **Memory and learning must be bounded, observable, and defeatable.** Projects and notes must not create hidden background learning or silent long-term memory mutation.
- **Single-writer discipline is sacred.** All new durable write paths introduced here must go through EC commands and append-only JSONL + atomic JSON patterns where practical.
- **No ambient hidden focus mode.** Merely viewing or selecting a project in Q must not silently alter unrelated chats, tasks, rooms, panels, or scheduled automations.
### 0.3 Relationship to prior specs
This addendum is **additive**. It does not replace DOC7. It reuses DOC7's bucket model, assignment semantics, indexing, manifests, background storage, and picker concepts, but allows the project UI to present those capabilities differently.
This addendum also narrows the visible UX role of "projects" relative to older project-loading concepts. DOC20 treats projects as:
- an organizational grouping;
- a durable default context container for project-scoped work; and
- a filtered operating surface.
If older internal project-loading behavior remains elsewhere in the suite, it must not create hidden ambient context beyond what is visible and explicit in this addendum.
### 0.4 Core design choices in DOC20
DOC20 adopts the following decisions from the design discussion:
- Keep the word **Scope** in the Browser.
- Permit **Project**, **Context Bucket**, and **Folder** to exist both as browser scope values and as item types.
- Solve overlap by list behavior rules, not by inventing a different abstraction.
- Keep **Configure** as the canonical place to add/remove items to/from a project.
- Keep **Chats** and **Rooms** together in the visible UX where appropriate.
- Use **Tiptap OSS** as the notes editor base.
- Add **custom comments** and **custom Elnor/AI editing actions** on top of Tiptap OSS.
- Use **Pandoc** for DOCX/PDF export.
- Keep **Notes** as their own main menu page while also allowing notes to belong to a project.
- Use **Folders** as a virtual hierarchical organizer (scope family in the Browser) that does not change storage layout.
**R3 additions:**
- The **Q Unified Workspace** is the primary interface for the entire Q app. The tab system holds any content type — not just notes, documents, and web pages.
- The **Nav tab** in the browser column is the app's main navigation surface, replacing the traditional left sidebar menu.
- **Clips are notes.** Same renderer, same toolbar, same block modules. Only difference: purple accent color and auto-populated clip blocks. No separate code path.
- **Agent name is always dynamic.** All UI text references `agent.name` — never hardcoded "Elnor." Users may change the agent name.
- **Utility tabs are transient but persistent.** They persist when navigating away (no auto-close), but only one exists at a time — opening a new utility page replaces the current one. Blue outline border distinguishes from working tabs.
- **IBM Plex Sans** is the working UI font. Production may use Söhne (licensed) or final selection TBD.
- **Content fills available width.** No artificial maxWidth constraints. User controls width via resizable browser column and right panel.
- **Folders button is separate from scope chips.** Visually distinct (dotted border, tinted background) but triggers the same scope mechanism. Not duplicated in the scope chip list.
- **Bookmark favicons are colored squares** with domain initial letter (14px, 3px border-radius). Simulates Chrome favicon behavior.
- The **right chat column** is independent of the Ask Agent panel. Chat column at app edge; Ask panel within workspace content area.
**R4 additions:**
- **Two-layer data architecture** for To-Do and Calendar data: Layer 1 = EC application tables (`ELNOR_MEMORY/todo_items`, `ELNOR_MEMORY/todo_lists`, `ELNOR_MEMORY/cal_events`) for fast CRUD; Layer 2 = DOC72 entity graph for semantic queries. DOC20 describes the UI; EC owns the writes; DOC72 owns the extracted knowledge.
- **Floating Palette V2** is a sidebar-based control surface. Three persistent content tabs (Chat, Note, To Do) with state preservation. ☰ sidebar slides out left with Command bar, Notices (notification inbox), Activity feed, Quick Actions. Pin (always-on-top) toggle. Designed as a standalone micro-workspace — press one hotkey, check notifications, fire a chat, dismiss. Eventually becomes the mobile view.
- **Unified To-Do system.** Single `fpTodoLists` data pool shared across three view surfaces: (1) palette To Do tab, (2) note canvas to-do modules, (3) standalone to-do tabs. Changes propagate instantly. Each list: `{id, name, noteId, tasks:[{id, text, done, sub:[{id, text, done}]}]}`.
- **Calendar module** — embeddable in notes or as standalone tabs. Month/Week/Day/List views. Event editor with reminders. Settings with agent instructions, Outlook sources (DOC16 Entry 16.7), sync rules, notification preferences.
- **Chrome-style tab bar.** Active tabs white, seamlessly connected to content. Inactive slightly darker with separators. Tab groups use `display:contents` for uniform proportional shrinking. Group labels as colored pills. Active grouped tabs get continuous colored border outline.
- **SVG icon system.** All emoji icons replaced. Heroicons (outline) primary. Lucide for selective swaps. Tabler for two specialized icons. Components: `Ic` → `I` → `TabIcon` → `NavIcon`.
- **Split view.** Two independent panes with draggable vertical divider. Each pane: own tab bar, active tab, content area. Left pane keeps browser column toggle.
- **Linear-style notification inbox.** Source-colored cards (Calendar amber, Agent purple, Email blue, System gray). Filter by All/Unread/Snoozed. Snooze options. Silent mode suppresses sounds/glow/auto-show but not badges. Delivery matrix configurable per source.
- **Document title** — clickable (opens in default app), right-click context menu (Show in Finder, Open in Default App, Copy Ref, Copy Path, Copy File Name).
- **Zoom slider** in status bar — Word-style minus/range/plus/percentage, 50%–200%, CSS `transform: scale()`.
- **Per-tab right panel state.** Panel resets on tab switch. Utility tabs auto-close panel.
- **Tab icon format migration.** Tab `icon` field changed from emoji strings to string IDs consumed by `TabIcon` component. Affects tab serialization.
- **Module block type rename.** `"tasks"` → `"todo"` for consistency. Backwards-compatible rendering.
- **Naming changes:** "Conversations" → "Chats" (nav section), "Knowledge Manager" → "Knowledge".
- **Tab color changes:** web → `#64748B` (slate gray), chat → `#1a5276` (dark blue), todo → `#0891B2` (teal, new).
- **Ask button simplification.** Per-task and per-subtask spark icons removed. Single list-level "Ask" button sends full list as context.
- **Bookmarks bar** — thin horizontal bar below web browser toolbar, populated from special "Bookmarks Bar" folder, toggle via settings.
**R4.1 additions:**
- **Surface independence principle (§2.4).** Every Q rendering surface is a stateless viewport. No surface owns data. All content is EC-owned durable state delivered via subscription. `fp*` variables are Q-local UI selection state, not data ownership. This enables mobile clients, multi-user collaboration, and multi-device sync without data model changes.
- **Content sync matrix (§2.5).** Three-tier classification: Tier 1 (multi-surface + networking) = chats, rooms, notes, to-do lists, calendar events, panels, forums; Tier 2 (networking) = projects, context buckets; Tier 3 (config, pull-on-open) = tasks, skills, agents, overlays.
- **Surface-side sync contract (§2.6).** Surface registration, EC push channel, optimistic updates with reconciliation, note edit lock (Phase 1) with CRDT readiness (Phase 2), rename propagation, selector population from EC state, agent busy indicators.
- **DOC25 proposed** as new spec for server-side sync transport, conflict resolution, networking layer, auth, and offline queue. DOC20 owns the surface contract; DOC25 owns the server contract.
- **Expanded To-Do schema (§6.21.2).** `TodoList` gains `project_id`, `tags`, `created_at`, `updated_at`. `TodoTask` gains `done_at`, `attachments`, `created_at`, `updated_at`. New `TodoAttachment` schema for document references. List `name` documented as semantically significant for DOC72 entity resolution. `project_id` is organizational; matter/case association is independent via entity resolution.
- **Expanded Calendar schema (§6.22.4).** `CalendarEvent` gains `event_type` (10 categories from `hearing` to `personal`), `participants`, `attachments`, `project_id`, `tags`, `source`, `external_id`. Event type enables differentiated reasoning (hearings are hard deadlines; vacations block availability).
- **Signal emission contracts (§6.21.7, §6.22.8).** To-Do and Calendar surfaces now specify exactly when and how they emit signals to DOC72 — deterministic extraction for structured fields, entity resolution for text fields, optional LLM for inference. Triggered at EC on every CRUD command regardless of originating surface.
- **Notes intake gap identified (§6.22.9).** Notes need LLM-assisted extraction with a significance gate. Cross-doc obligation flagged for DOC72 to design the `intake.notes` contract.
- **DOC72 Proposal: Surface Intake Contracts** produced as separate document with full specifications for `intake.todo`, `intake.calendar`, `intake.notes`, and `intake.browser`.
---
## 1. High-level product model
### 1.1 Browser
The Browser is a second-column browse/filter/launch surface immediately to the right of the main menu column. It is not the command palette and not the owner of most objects. It is a universal finder, launcher, organizer, and attach surface.
### 1.2 Project
A Project is a durable grouping and default-context container.
A project has two jobs:
1. **Grouping:** show all relevant notes, chats, rooms, tasks, panels/forums, generated artifacts, and project-scoped context in one filtered view;
2. **Default project context:** apply the project's DOC7-backed context to project-scoped work in a visible, durable, and defeatable way.
A project is **not** a temporary global focus mode.
### 1.3 Project context
Project context is implemented using DOC7 buckets, not a second context engine.
A project may have:
- exactly one **primary project bucket** (auto-created with the project);
- zero or more **attached shared buckets** (existing DOC7 buckets linked to the project);
- zero or more **linked folder aliases** (filesystem references with optional project-bucket sync).
### 1.4 Notes
A Note is a rich-text editing surface with:
- Tiptap/ProseMirror-based editor with AI agent integration;
- comment rail with anchored threaded comments;
- tracked changes / redlining with per-author colors;
- import (DOCX, Markdown) and export (DOCX, PDF, Markdown);
- cross-note linking and backlinks;
- task list items;
- version history and autosave.
Notes belong to zero or one projects.
### 1.5 Folders
A Folder is a virtual hierarchical organizer. Folders are purely for the user's visual organization in the Browser — they do not change how items are stored. A folder can contain items and subfolders. Any browser item type can be placed in a folder. Folders are only visible when the Folders scope is selected in the Browser.
### 1.6 Unified Workspace Shell (R3, updated R4)
The Q Unified Workspace is the primary interface for the entire Q app. The workspace shell provides:
- a **universal tab system** supporting 9 content types (note, doc, web, clips, chat, room, task, todo, utility) with Chrome-style tab bar (R4);
- a **4-mode browser column** (Nav, Browser, Notes, Web) where the Nav tab replaces the traditional left sidebar menu;
- a **simplified left nav rail** (6 items — Q logo, browser toggle, split view toggle, quick command/palette toggle, chat column toggle, settings) (R4 — updated);
- a **right chat column** (320px, independent of the Ask Agent panel, at the app's right edge);
- a **session system** for temporal workspace organization;
- a **split view** for side-by-side panes (R4);
- a **floating palette** (always-on-top mini workspace, see §1.8) (R4).
The workspace shell is defined in §6.20.
### 1.7 Sessions (R3)
A Session is a temporal container for workspace activity. Sessions auto-create on Q launch and produce a clips note ("Clips: 4.4-1") in the Session Notes folder. Sessions close on quit, manual action, or 2-hour idle. The clips note persists forever as a regular note. See §6.20.22–6.20.24.
### 1.8 Floating Palette (R4)
The floating palette is a dark-themed, always-on-top overlay window that functions as a complete mini control surface. Toggled via ⌥Space or the ⚡ icon in the left nav rail. In Electron, it is a separate `BrowserWindow` with `alwaysOnTop` capability and `frame:false`.
The palette has three persistent content tabs (Chat, Note, To Do) whose state is preserved across tab switches, and a sidebar (☰) that slides out from the left edge with a Command bar, Notices (notification inbox), Activity feed, Quick Actions list, mute toggle, and hotkey indicator. Pin (thumbtack) toggles `alwaysOnTop`. See §6.20.30.
### 1.9 To-Do System (R4)
To-do lists are a first-class data type with a unified data pool (`fpTodoLists`) shared across three view surfaces: the floating palette To Do tab, note canvas to-do modules (embedded in notes), and standalone to-do tabs (opened from browser). Changes in any surface propagate to all others immediately. Each list has a `noteId` linking back to the note it was created in. Lists support tasks with subtasks, due dates, reminders, and agent assignment. A single list-level "Ask" button sends the full list as context to the agent. See §6.21.
### 1.10 Calendar Module (R4)
The calendar module is insertable as a note canvas module or as a standalone tab. It supports Month, Week, Day, and List views. Events include title, date/time range, calendar source, location, notes, and reminders. Calendar sources connect to Outlook via DOC16 Entry 16.7. Settings cover agent instructions, source management, sync rules, and notification preferences. See §6.22.
### 1.11 Two-Layer Data Architecture (R4)
To-Do and Calendar data follow a two-layer architecture:
- **Layer 1: EC application tables** — `ELNOR_MEMORY/todo_items`, `ELNOR_MEMORY/todo_lists`, `ELNOR_MEMORY/cal_events`. Operational data stores for fast CRUD. Q reads and writes via EC commands.
- **Layer 2: DOC72 entity graph** — extracted knowledge nodes for semantic matching. When items are created or modified, EC extracts entities and creates nodes in DOC72's graph. This enables semantic queries ("what deadlines relate to the Paramount case?") without polluting the operational store.
DOC72 node mapping: to-do items → `obligation` nodes, to-do lists → `work_product` with `entity_subtype: "todo_list"`, calendar events → `obligation` with `source_type: "calendar_event"`, calendars → `world_entity` nodes. New edge types: `subtask_of`, `belongs_to_list`, `synced_to_calendar`. See DOC72 for entity graph details.
---
## 2. Ownership split and durable state rules
### 2.1 Ownership split
| Surface | Durable state owner | Read-model consumer |
|---|---|---|
| Browser column | EC (catalog read model) | Q |
| Projects | EC | Q |
| DOC7 buckets | EC (via DOC7 commands) | Q |
| Notes | EC | Q |
| Collections | EC | Q |
| Saved Views | EC | Q |
| Places | EC | Q |
| Folders | EC | Q |
| Linked Folder Aliases | EC | Q |
| To-Do lists and items | EC (`ELNOR_MEMORY/todo_lists`, `ELNOR_MEMORY/todo_items`) | Q (palette, note modules, standalone tabs) |
| Calendar events | EC (`ELNOR_MEMORY/cal_events`) | Q (calendar modules, standalone tabs) |
| Notifications | EC (notification store) | Q (palette inbox) |
| DOC72 extracted knowledge | EC → DOC72 entity graph | Q (semantic queries via EC) |
### 2.2 Single-writer discipline
All creates, updates, and deletes for browser-visible objects go through EC commands. Q reads from EC-materialized read models and session overlays.
### 2.3 Read-model principle
Q renders from read models, not from raw event logs. EC materializes current-state projections from append-only event logs. Q may cache read-model snapshots locally for responsiveness but must not treat local cache as source of truth.
### 2.4 Surface independence principle (R4.1)
**Every Q rendering surface is a stateless viewport.** No surface owns data. All content — chats, notes, to-do lists, calendar events, rooms, panels, forums — is EC-owned durable state. Surfaces subscribe to EC state and render it. Mutations always flow through EC commands. This principle applies uniformly to:
- Main workspace content tabs
- Floating palette content tabs (Chat, Note, To Do)
- Right chat column
- Note canvas embedded modules (to-do modules, calendar modules, inline threads)
- Split view panes (both independently)
- Browser column selectors and lists
- Future: mobile clients, shared/collaborative views, third-party participant views
**What this means for `fp*` state variables:** Variables like `fpTodoLists`, `fpActiveTodoList`, `fpSelectedChatId`, and `calEvents` as described in §6.20–6.22 are **Q-local UI state** — they track which item is selected, which tab is active, which list is expanded. They are NOT data ownership. The underlying data (the actual to-do items, calendar events, chat messages, note content) lives in EC application tables and is delivered to surfaces via subscription. The `fp*` variables select which slice of EC state to render.
**Consequence:** If the same to-do list is open in the palette, a note canvas module, and a standalone tab, all three surfaces subscribe to the same EC state for that list. Checking an item in one surface sends a command to EC; EC writes the change; EC pushes the updated state to all subscribed surfaces; all three re-render. No surface-to-surface communication. No shared React state. EC is the sole synchronization point.
**Rationale:** This architecture enables three capabilities that would otherwise require data model changes:
1. **Mobile clients** — a mobile app is just another surface subscribing to the same EC state. No data migration, no separate API.
2. **Multi-user collaboration** — another human joining a chat, editing a shared note, or participating in a panel is just another surface subscribing to the same EC state, plus an auth/permissions layer.
3. **Multi-device sync** — the user's Mac, phone, and any future device all see the same state because they all subscribe to the same EC truth.
### 2.5 Content sync matrix (R4.1)
Not all content types have the same sync requirements. This matrix defines three tiers based on current multi-surface rendering needs and future networking needs.
#### Tier 1 — Multi-surface rendering + networking
These content types are rendered on multiple surfaces simultaneously within a single client AND will need real-time sync across network participants (other humans, remote devices).
| Content type | Current rendering surfaces | Future surfaces | Networking scenario | Owner spec |
|---|---|---|---|---|
| **Chats** | Main chat tabs, palette Chat tab, right chat column | Mobile, shared chats | Other humans join a chat with user + AI agents. Agent streaming responses must fan out to all surfaces and all participants. | DOC12 |
| **Rooms** | Main room tabs | Palette (future), mobile | Same as chats — multi-human rooms with AI agents. | DOC12 |
| **Notes** | Main note tabs, palette Note tab, note canvas modules (embedded) | Mobile, shared/collaborative notes | Third parties can view and edit shared notes. Requires conflict resolution for concurrent edits. | DOC20 §6 |
| **To-Do Lists** | Palette To Do tab, note canvas to-do modules, standalone to-do tabs | Mobile | Shared to-do lists where multiple users check items. Lower conflict risk than notes (discrete operations, not continuous text). | DOC20 §6.21 |
| **Calendar Events** | Note canvas calendar modules, standalone calendar tabs, notification system | Mobile | Shared calendars, meeting invitations. External sync via Outlook (DOC16). | DOC20 §6.22 |
| **Panels** | Main panel tabs | Palette (future), mobile, shared | Human participants joining AI panel reviews (e.g., CANDOR output review, research panels). Same real-time requirements as rooms. | DOC12 |
| **Forums** | Main forum tabs | Palette (future), mobile, shared | Forum posts from multiple humans + agents. Lower real-time requirement than chats (async discussion). | DOC12 |
#### Tier 2 — Networking required, single surface per client
These content types currently render on one surface at a time but need sync for multi-device and collaboration.
| Content type | Networking scenario | Owner spec |
|---|---|---|
| **Projects** | Project state, membership, configuration shared across team members and devices. Project-attached content (chats, notes, tasks, buckets) all reference back to the project — project sync is foundational for everything else. | DOC20 §4 |
| **Context Buckets** | In a networked environment: firm-shared buckets vs personal buckets. Bucket contents and file references must sync across nodes. Critical because buckets feed context assembly — stale bucket contents on a remote node means the agent gets wrong context. | DOC7 |
#### Tier 3 — Verify alignment, low sync urgency
These content types are likely already architecturally sound (EC-owned, surface just renders) but should be verified during the DOC25 design process.
| Content type | Notes | Owner spec |
|---|---|---|
| **Tasks (DOC23)** | Task system's modular canvas references task state — doesn't own a separate copy. EC ownership model is sound. Gap: task status updates (completion, gate approvals) need to push to any surface displaying that task. | DOC23 |
| **Skills, Agents, Overlays** | Configuration/registry data. Changes rarely. Pull-on-open is sufficient — real-time push is overkill. Must be correct across devices but doesn't need streaming. | DOC3, EC Core |
### 2.6 Surface-side sync contract (R4.1)
This section defines what every Q surface must implement to participate in the centralized data model. The server-side implementation (transport, conflict resolution, networking) is owned by DOC25 (proposed). DOC20 owns only the surface contract.
#### 2.6.1 Surface registration
When a surface opens a content item (a chat, note, to-do list, etc.), it registers with EC:
```ts
interface SurfaceRegistration {
surface_id: string // unique per rendering surface instance
surface_type: "main_tab" | "palette_tab" | "chat_column" | "note_module" | "calendar_module" | "todo_module" | "split_pane" | "mobile"
content_type: "chat" | "room" | "note" | "todo_list" | "calendar" | "panel" | "forum"
content_id: string // the specific item being rendered
capabilities: SurfaceCapabilities
}
interface SurfaceCapabilities {
can_edit: boolean // false for read-only views
supports_streaming: boolean // true for chat/room surfaces that show token-by-token agent responses
supports_optimistic: boolean // true for surfaces that update locally before EC confirms
}
```
When the surface closes (tab closed, palette switched, module unmounted), it deregisters. EC uses the registration table to know which surfaces need push updates for which content items.
#### 2.6.2 EC push channel
EC pushes state updates to registered surfaces via WebSocket (or equivalent local IPC for Electron). The push payload is:
```ts
interface ECStateUpdate {
content_type: string
content_id: string
update_type: "full_state" | "delta" | "stream_token" | "metadata_only"
payload: any // content-type-specific
version: number // monotonic — surfaces reject updates older than their current version
source_surface_id: string | null // which surface originated the mutation (null for EC-internal changes)
}
```
**Fan-out rule:** When EC processes a mutation, it pushes the resulting state update to ALL registered surfaces for that content item EXCEPT the source surface (which already applied the optimistic update). If the optimistic update was wrong (EC rejected the command), EC pushes a correction to the source surface too.
**Chat streaming:** When an agent generates a response, EC pushes `update_type: "stream_token"` to all surfaces registered for that chat. Each token is fanned out independently. Surfaces append tokens to the in-progress message. When the response completes, EC pushes a `"delta"` update with the final message.
#### 2.6.3 Optimistic updates
For responsive UI, surfaces apply mutations locally before EC confirms:
1. User checks a to-do item in the palette.
2. Palette immediately renders the item as checked (optimistic local state).
3. Palette sends `TodoItemUpdateCommand` to EC.
4. EC writes the change to `ELNOR_MEMORY/todo_items/`.
5. EC pushes `ECStateUpdate` to all OTHER registered surfaces for that to-do list.
6. Those surfaces re-render with the checked item.
7. If EC rejected the command (validation error, conflict), EC pushes a rollback to the source surface. The palette unchecks the item and shows an error toast.
**Optimistic update scope:** Optimistic updates are appropriate for discrete, low-conflict operations: checking to-do items, renaming, starring, archiving, adding tasks, creating events. They are NOT appropriate for continuous text editing (notes) — see §2.6.4.
#### 2.6.4 Note editing conflict strategy
Notes present a unique challenge because they involve continuous rich-text editing, not discrete operations. If the same note is open in the palette and a main tab, two Tiptap editors have independent local state against the same document.
**Phase 1 (single-user, multi-surface):** Single active editor with a lock indicator.
- When a surface opens a note for editing, it acquires an edit lock from EC.
- If another surface already holds the lock, the new surface opens in **read-only mode** with a banner: "This note is being edited in [palette / tab name]. Edits here will be disabled until the other editor closes."
- The read-only surface still receives live push updates as the editing surface auto-saves, so the user sees changes in near-real-time.
- Lock releases on: surface close, tab switch away, blur after idle (30s), explicit release.
- The existing auto-save contract (1200ms debounce, 15s max unsaved, flush on blur/navigation) continues to govern when the editing surface pushes to EC.
**Phase 2 (multi-user collaboration):** CRDT/Yjs integration for real-time concurrent editing. This is deferred but the Phase 1 lock model is designed to be replaceable — the lock is an EC-side concept, not baked into Tiptap. When CRDT is ready, the lock is removed and Yjs handles merge. The surface contract (subscribe, render, push edits) does not change.
#### 2.6.5 Rename and metadata propagation
When any content item is renamed (chat title, note title, to-do list name, project name), the rename propagates to:
- All rendering surfaces showing that item (via ECStateUpdate)
- Tab titles in the tab bar (the tab bar subscribes to title updates for all open tabs)
- Browser column lists (Nav tab chats section, Notes browser list, selector dropdowns)
- Selector dropdowns (palette selectors, +Module Link Existing selectors, standalone tab header selectors)
- History entries and search indexes (EC rebuilds these on rename)
This is not special handling — it's a consequence of the surface independence principle. Surfaces subscribe to EC state; EC state includes the title; title changes push to all subscribers.
#### 2.6.6 Selector population
Content selectors (§6.20.30J) show recent items from EC state. When a new item is created (new chat, new note, new to-do list) from any surface, it appears in all selectors across all surfaces because:
1. The creation command goes to EC.
2. EC writes the new item.
3. EC pushes a catalog update to all surfaces.
4. Selector dropdowns re-query the catalog on open (or subscribe to catalog changes if performance requires).
The selectors do NOT maintain their own item lists. They query EC's read models.
#### 2.6.7 Agent busy indicators
When an agent is engaged on a task (running a DOC23 pipeline, generating a long response, processing a review), every surface that could invoke that agent should show its availability status:
- **Palette chat tab:** If the selected agent is busy, show status below the input: "{Agent} is working on [task name]… [Interrupt] [Wait]"
- **Ask button (notes, documents, to-do lists, calendars):** If the agent is busy, the Ask button shows a busy indicator. Click still works but queues the request with a toast: "Request queued — {Agent} is currently engaged."
- **Chat input in main tabs:** Same busy indicator as palette.
- **Inline thread @mention:** Same queuing behavior.
Agent availability is EC state (the dispatch queue / active task registry). Surfaces subscribe to agent status the same way they subscribe to content state. The interrupt/queue/wait logic is owned by DOC11 (gateway routing) and EC Core (dispatch coordination). DOC20 owns only the surface indicators.
**Cross-doc obligation:** DOC11 must define the agent contention protocol: what happens when a second request arrives while the agent is busy. Options include: queue with priority, interrupt current task (with user confirmation), spawn a parallel session (if the model supports it), or reject with "busy" status. DOC20 surfaces will render whatever DOC11 decides.
#### 2.6.8 Networking readiness — what changes, what doesn't
The surface independence principle means the data model does NOT change for networking. What changes is the transport and auth layers, owned by DOC25 (proposed):
| Concern | Single-node (current) | Networked (future) | What changes |
|---|---|---|---|
| **Transport** | WebSocket to localhost EC | WebSocket to EC via Cloudflare Tunnel or relay server | Transport layer only — surface contract unchanged |
| **Auth** | None (single user, single machine) | Per-user identity, session tokens, per-item permissions | New auth middleware between surface and EC |
| **Permissions** | Implicit full access | Read/write/admin per content item per user | EC enforces; surfaces show/hide edit controls based on permission grants |
| **Presence** | Not needed | "Will is typing…", "3 participants", online/offline indicators | New subscription channel for presence state |
| **Conflict resolution** | Optimistic + lock (Phase 1) | CRDT for notes (Phase 2), last-write-wins for discrete ops, merge for concurrent discrete ops on same item | DOC25 owns resolution strategy per content type |
| **Offline queue** | Not needed | Mobile clients queue mutations when disconnected, reconcile on reconnect | New client-side queue with idempotent command replay |
| **Invitation flow** | Not needed | Invite user to chat/note/project, accept/decline, permission grant | New EC commands + UI surfaces in DOC20 |
**Key architectural point:** Because surfaces are stateless viewports subscribing to EC state, adding a mobile client or a third-party participant is "just" adding another subscriber. The data model, command contract, and push channel are the same. The complexity is in transport (reaching the subscriber), auth (verifying they're allowed), and conflict resolution (merging concurrent edits) — all of which belong in DOC25, not DOC20.
---
## 3. Browser
## 3.1 Browser layout
**Browser column toggle shortcut:** `⌘+B` (Mac) / `Ctrl+B` (Windows). Also available via sidebar icon. (R1.6)
### 3.1.1 Browser shell
- Entire browser column can be shown/hidden.
- Toggle button lives near the top of the main menu column, aligned right.
- Browser width is adjustable by dragging a **4px drag handle on the right edge**.
- Browser width persists between sessions via EC user preferences.
- Default width: **260px**. Minimum: **200px**. Maximum: **450px**.
### 3.1.2 Browser vertical structure
Top to bottom:
1. Search bar
2. Collections quick row
3. Scope chips
4. Scope detail list (context-sensitive based on selected scope family)
5. Draggable vertical splitter
6. Type chips (two rows)
7. Filter/sort bar
8. Results list
### 3.1.2A Browser wireframe
```text
┌──────────────────────────────────────────────┐
│ [ Search… ][This View▼] │
│ ● ● ● ● ● ● ● ● ● ● (+) │
│ [Collection] [Project] [Bucket] [Places] │
│ [Folders] [Saved Views] │
├──────────────────────────────────────────────┤
│ Scope Detail │
│ Henderson │
│ Adams │
│ Global │
│ No Project │
│ │
├────────────── draggable vertical splitter ───┤
│ [Document] [Chat] [Preset] [Skill] [Task] │
│ [Agent] [Panel] [Forum] [Overlay] [Note] │
│ [Bucket] [Project] │
│ [Filter▼] [Sort: Modified▼] │
├──────────────────────────────────────────────┤
│ pin • title … status TYPE 2h │
│ pin • title … status TYPE 4h │
│ pin • title … status TYPE 1d │
│ … │
└──────────────────────────────────────────────┘
```
### 3.1.2B Browser state object
Use the following in-memory browser state shape in Q:
```ts
type BrowserPrimaryScopeFamily = "project" | "bucket" | "places" | "folders" | "notes" | "saved_view" | null // R1.7: added "notes"
interface BrowserViewState {
q_session_id: string
follow_current_page: boolean
search_mode: "this_view" | "everywhere"
search_query: string
selected_collection_ids: string[]
primary_scope_family: BrowserPrimaryScopeFamily
primary_scope_detail_id: string | null
selected_type_ids: BrowserItemType[]
active_filter_state: Partial<Record<BrowserFilterKey, boolean>>
sort_key: BrowserSortKey
sort_dir: "asc" | "desc"
include_archived: boolean
current_page_hint: BrowserPageHint | null
folder_overlay_open: boolean // R1.6 — tracks folder overlay panel visibility
show_nested: boolean // R1.6 — recursive results for folder/place scopes
place_smart_filter: boolean // R1.6 — noise-file filtering for Places (default: true)
}
```
### 3.1.2C Collection quick row and Collection scope relationship
The Collections quick row and the Collection scope detail list MUST operate on the same `selected_collection_ids[]` state.
Rules:
- clicking a collection circle in the quick row toggles that collection id in `selected_collection_ids[]`;
- selecting or deselecting collections in the Collection scope detail list updates the same `selected_collection_ids[]`;
- the Collection quick row shows only collections where `show_in_quick_row = true`;
- the Collection scope detail list shows **all** collections, including archived collections when `include_archived = true`;
- the Browser may have zero or more selected collections at any time;
- selected collections are always applied as an **intersection** filter:
- an item matches if it contains **every** selected collection id;
- if zero collections are selected, no collection filtering is applied.
### 3.1.2D Primary scope exclusivity
The Browser may have **at most one** selected primary scope at a time.
The primary scope families are:
- `project`
- `bucket`
- `places`
- `folders`
- `saved_view`
Selecting a new primary scope family clears the previous primary scope family and its detail id.
The Collection quick row and Collection scope detail list do **not** count as the primary scope. Collections are additive filters that combine with any selected primary scope.
### 3.1.3 Vertical splitter
A draggable horizontal bar separates the scope detail area from the type chip area.
- Default position: 35% of browser column height.
- Min: enough room for at least 3 scope detail entries.
- Max: enough room for at least the type chip rows plus filter bar.
- Grab handle: 4px tall, centered 20px wide grip indicator at 25% opacity.
### 3.1.4 Sticky behavior
When the user scrolls the results list, the search bar, collections row, scope chips, scope detail, splitter, type chips, and filter bar all remain fixed. Only the results list scrolls.
### 3.1.5 Vertical height budget guidance
In a 900px-tall viewport with the browser at default width:
- search bar: ~36px
- collections row: ~30px
- scope chips: ~26px
- scope detail: ~110px (adjustable via splitter)
- splitter: 4px
- type chips: ~44px (two rows)
- filter/sort bar: ~26px
- results list: remaining (~624px, approximately 19 rows at 32px)
## 3.2 Browser search
### 3.2.1 Separation from command palette
Browser search searches the browser's current universe. It does not replace the global command palette.
### 3.2.2 Search behavior
Browser search has two explicit modes:
- **This View** — search inside the current browser universe after applying current page hint, selected collections, selected primary scope, selected types, and active filters.
- **Everywhere** — search across the full Browser catalog read model, ignoring current page hint but still respecting:
- selected collections;
- selected primary scope, if one is selected;
- selected types, if any are selected;
- `include_archived`.
Search behavior rules:
1. Empty query string returns the current filtered list without title/content scoring.
2. Non-empty query performs ranked search over:
- title;
- aliases, where supported;
- excerpt/subtitle, where supported;
- path basename, for path-backed items;
- note plain text, where the type is `note`;
- bucket file title and docmeta summary, where the type is `document`.
3. Ranking order:
- exact title match;
- title prefix match;
- title token match;
- alias exact or prefix match;
- subtitle/excerpt match;
- body/path match.
4. Search scoring is stable and deterministic.
5. Search results always return through the same `BrowserItemSchema` read model used by the Browser UI.
6. Search never mutates durable state.
### 3.2.3 Search actions
The search control contains:
- a left dropdown/chevron button with:
- **Save current view/search**
- **Open saved view**
- **Manage saved views**
- **Clear search**
- a segmented mode control on the right side of the field:
- **This View**
- **Everywhere**
### 3.2.4 Save current view/search flow
Clicking **Save current view/search** opens a compact modal with:
- **Name** (required text input)
- **Pin in Saved Views** (checkbox)
- **Overwrite existing** (checkbox + target selector shown only if a name collision exists)
The saved payload MUST persist:
- `selected_collection_ids`
- `primary_scope_family`
- `primary_scope_detail_id`
- `selected_type_ids`
- `active_filter_state`
- `sort_key`
- `sort_dir`
- `search_query`
- `search_mode`
- `include_archived`
## 3.3 Collections quick row
### 3.3.1 Purpose
Collections are additive tag-based groupings. The quick row provides fast toggle access to collections the user uses frequently.
### 3.3.2 UI
- Row of colored circular collection chips renders **between the search bar and the scope chips**.
- Each collection appears as an **18px colored circle** with its assigned color. Circles render at 70% opacity by default, brightening to 100% on hover.
- A **dashed circle with + icon** (18px, dashed border in textTer color) appears first in the row as the create/edit entry point.
- Clicking a collection circle toggles that collection id in `selected_collection_ids[]`. It does not set a primary scope.
- The row scrolls horizontally if collections exceed the browser width.
- There are **10 color options**.
### 3.3.3 Create/edit collection popup
Clicking the `+` button opens a compact popup with:
- name text input
- color picker (10 dots)
- show in quick row checkbox
- save/cancel/delete actions
### 3.3.4 Selection rules
Collections act as additive filters and may combine with one selected primary scope (Project, Bucket, Places, Folders, or Saved View).
## 3.4 Scope block
### 3.4.1 Scope families
The Browser has **seven** scope families. Five appear as chips in the scope row; Folders and Notes are accessed via dedicated UI elements.
1. **Collection** — multi-select tag filter (handled by collections quick row + detail list, not a primary scope)
2. **Project** — show items in a specific project, or "No Project"
3. **Bucket** — show items in a specific context bucket
4. **Places** — show saved filesystem folder aliases and their browseable contents
5. **Saved Views** — activate a saved browser configuration
6. **Folders** — show the user's virtual folder hierarchy (see §3.15). Accessed via a dedicated dotted-border button inline with scope chips but visually distinct (1.5px dashed border, tinted background, marginLeft:4). Not rendered as a scope chip. Activates the same scope mechanism as other scopes. (R3)
7. **Notes** — dedicated note folder tree with structured organization. Auto-filters results to notes only. Accessed via the Notes browser mode tab. (R1.7)
### 3.4.2 Scope philosophy
Scoping answers: "What universe am I looking at?"
A scope narrows the visible items. It does not mutate them. Selecting a project scope does not attach items to that project.
### 3.4.3 Scope selection rules
- **Project**, **Bucket**, **Places**, **Folders**, **Notes**, and **Saved Views** are mutually exclusive as primary scopes.
- **Collection** is an overlay and combines with any primary scope.
- At most one primary scope may be active.
- Within a primary scope, one detail entry may be selected (e.g., a specific project, a specific bucket, a specific folder).
### 3.4.4 Scope chip UI
Scope chips render as a row of compact buttons below the collections row:
- padding: 2px 8px, height: 20px, borderRadius: 3, fontSize: 10
- active: border accentBtn+"60", bg accentBtn+"10", color accentBtn, fontWeight 600
- inactive: border borderLight, bg transparent, color textTer, fontWeight 400
## 3.5 Scope detail list
### 3.5.1 Purpose
When a scope family is selected, the scope detail list shows the entries within that family. The user clicks an entry to narrow results to that specific scope.
### 3.5.2 Project scope detail entries
Project scope detail list includes all non-archived projects plus a "No Project" entry.
Each row shows:
- project color dot (7px)
- project title
- active state (selected row highlighted)
A `+ New Project` action is available from the Project detail header.
### 3.5.3 Places scope detail entries
Places scope detail list includes saved place aliases.
**Collection filter dropdown (R4):** When Places scope is active, a collection filter dropdown appears in the scope detail header area. Shows a colored dot per collection, folder icon for "All." Dropdown trigger displays the selected collection's dot or folder icon. Filtering by collection restricts the Places list to aliases tagged with that collection. X (remove) button works on ALL place items including those with `missing` status.
Collection data example: `[{name:"All",color:null},{name:"Active Cases",color:"#31588c"},{name:"Research",color:"#2E8B57"},{name:"Templates",color:"#D97706"}]`
Each row shows at minimum:
- alias title
- path preview (truncated, e.g. `~/Documents/Henderson/…`)
- availability badge (`ok`, `missing`, `needs access`)
- file count (immediate children)
A `+ Add Place` action is available from the Places detail header.
### 3.5.3A Places browsing behavior
When a Place is selected in the scope detail list, the results area shows the **browseable contents** of that folder:
**What shows up:**
- All files in the immediate directory (not recursive by default).
- Subdirectories show as navigable rows with a folder icon. Clicking a subdirectory row navigates into it (the scope detail list updates to show the current path with a "← Back" breadcrumb).
- A **"Show nested"** toggle in the filter bar enables recursive display of all files in the selected folder and its descendants (flattened list, not tree).
**File type filtering:**
- All files are visible by default regardless of type.
- Unsupported files (binary, system files, etc.) are visually dimmed with a "Not previewable" badge but still listed.
- Type chips apply on top — selecting "Document" shows only document-type files from the Place.
- The same include/exclude globs from §3.12.9 can optionally be applied as a "Smart filter" toggle to hide noise files (.git, node_modules, .DS_Store, etc.).
**File row metadata:**
- file name (title)
- file extension / type badge
- file size
- modified date
- availability badge (if file has been moved or deleted since last refresh)
**Drag from Place:**
- Files from a Place can be dragged onto:
- A project's "Add to Project" drop zone → creates a DOC7 local-path ref in the project's primary bucket
- A context bucket → creates a DOC7 local-path ref
- The note editor → creates a `[[link]]` to the file
- The chat composer → attaches the file to the chat message
- Dragging a file from a Place does **not** copy the file. It creates a reference.
**Supported path types:**
- Local filesystem paths (default, fully supported)
- External drives / USB volumes (supported if mounted and readable)
- iCloud Drive paths (supported if synced locally; `needs_access` if download-on-demand and not yet downloaded)
- Network paths / SMB mounts (supported if mounted; `missing` if unmounted)
- Symlinks are followed; the resolved path is used
**Refresh behavior:**
- Right-click a Place → "Refresh" rescans the directory for changes (new files, removed files, updated metadata).
- Refresh is metadata-only — no file content is read or imported.
- If a Place's path no longer exists, the Place shows `missing` badge and its contents are empty until the path becomes available again.
### 3.5.4 Folders scope detail entries
Folders scope detail list shows the user's virtual folder hierarchy as an expandable tree.
Each row shows:
- expand/collapse chevron (if folder has subfolders)
- folder title
- item count badge
- indentation indicating depth
A `+ New Folder` action is available from the Folders detail header. Right-clicking a folder shows: New Subfolder, Rename, Delete.
When a folder is selected in the detail list, the results area shows only items assigned to that folder (not recursively — only direct children unless explicitly toggled).
### 3.5.5 Saved Views detail entries (R4 — updated)
Saved Views scope detail list includes all saved views.
**Built-in views (R4 — updated):** The "Current" and "No Project" built-in views have been **removed**. Default views:
- **Recent** (built-in) — cannot be edited or deleted
- **Deadlines this week** (user-created default)
- **Active matters** (user-created default)
**User views:** Each row shows:
- title
- Edit (pencil icon) on hover — opens inline rename
- Delete (X icon) on hover — removes view
**"Save current view" (R4):** A subtle text link with save icon (not a prominent button). Click opens a save popover: text input for view name, Enter to save, Escape to cancel.
### 3.5.6 Bucket scope detail entries
Bucket scope detail list includes browseable DOC7 buckets.
Each row shows:
- bucket title
- health badge
- item count
### 3.5.7 Overlap rule
If a specific project/bucket/place/folder is selected in the scope detail list, the item list shows items **within** that selected scope and suppresses the selected scope object itself by default.
### 3.5.8 Notes scope detail entries (R4 — updated from R1.7)
When Notes scope is active, the scope detail area shows a dedicated note folder tree with scope toggles.
**Scope toggles (R4):** Three toggle buttons at the top of the Notes scope area: **Notes** | **To Do** | **Cal**. State tracked in `noteBrowserScope: Set<"notes"|"todo"|"cal">` — multiple scopes can be active simultaneously. When "To Do" is active, to-do lists appear in the results with a teal "To Do" type badge. When "Cal" is active, calendar items appear with an amber "Cal" type badge. Type badges have transparent background with text colored by type. See also §6.20.8 for the browser column Notes mode.
**Folder tree:**
1. **"All Notes"** entry at top — shows all notes regardless of folder. Always present.
2. **User-created folders** — hierarchical folder tree for organizing notes. Supports subfolder creation (+ button on hover), rename (click title), delete (trash icon on hover with inline confirmation). Same interaction patterns as the Folders scope (§3.5.4).
3. **Special folders:**
- **Daily Notes** — auto-populated. Contains yesterday's Today notes renamed to their date (see §6.2B).
- **Templates** — contains note templates (see §6.15.15B).
**Draggable splitter (R4):** Between the folder tree and the results list. State: `noteSplitterPos` (default 140px). User drags to resize.
Selecting a folder filters the results list to notes in that folder and its subfolders. The scope detail area height is adjustable via the standard splitter (§3.1.3).
**Results auto-filter:** When Notes scope is active, the results list automatically filters to `item_type: "note"` (plus to-do lists and calendar items when those scope toggles are active). Type chips are hidden (redundant). The results list shows: pin icon, Today badge (sun icon for the Today note), title, modified time, tracked change count (✏), task count (☐ N tasks). Comment count indicators have been **removed** from note list items (R4).
**Note folder tree read derivation (R2):** The Notes scope folder tree is derived from `FolderSchema` records in `notes_folders_current.json` (stored under `ELNOR_MEMORY/notes/folders/`). Q reads this file to build the hierarchical tree. The browser results list is still a flat query from `browser_catalog_current.json` filtered by `folder_id`. The tree structure is a Q-side derivation from the folder registry — no separate hierarchical read contract is needed.
**Note folder tree replaces the separate Notes sidebar.** There is no standalone Notes sidebar panel. The browser's Notes scope provides the same functionality (folder organization, search, sort) within the existing browser framework.
## 3.6 Type block
### 3.6.1 Purpose
Type chips filter the results list to show only items of the selected types.
### 3.6.2 Visible type categories
```ts
type BrowserItemType =
| "document"
| "chat"
| "preset"
| "skill"
| "task"
| "agent"
| "panel"
| "forum"
| "overlay"
| "note"
| "bucket" // R2: renamed from context_bucket
| "project"
| "automation"
| "artifact" // R2: renamed from generated_artifact
| "place"
```
### 3.6.3 Chat and room treatment
Chats, rooms, and room subtypes all appear under a single `chat` type chip. Multi-agent rooms, red-team rooms, and standard rooms are filtered via subtab or the filter dropdown, not by separate type chips.
### 3.6.4 Adaptive type rules
If the current scope makes certain types impossible (e.g., a Places scope will never contain tasks), those type chips should be visually dimmed or hidden. The adaptive resolver uses the filter support map from §3.7.4.
### 3.6.5 Layout
**Responsive collapse rules (R1.6):**
- ≥ 260px: All chips visible in wrapping rows (default behavior)
- < 260px: Chips hidden behind a "Types…" toggle button. Click to expand a dropdown showing all chips. Active chip count shown on the toggle: "Types (3)"
- Collapsible behavior: Double-click the splitter or click a chevron above the type chip area to toggle chip visibility
Type chips render in **two rows** below the vertical splitter:
- padding: 1px 6px, height: 18px, borderRadius: 3, fontSize: 9.5
- active: border accentBtn+"50", bg accentBtn+"10", color accentBtn, fontWeight 600
- inactive: border borderLight, bg transparent, color textTer, fontWeight 400
## 3.7 Adaptive filters and sort
### 3.7.1 Filter bar UI
The Browser renders a **single compact toolbar row** between the type chips and the results list.
Layout:
```text
[Filter ▼] [Sort: Modified ▼] [Include archived □]
```
Rules:
- `Filter ▼` opens a popover listing only filters supported by at least one currently visible selected type.
- `Sort` is a dropdown.
- `Include archived` is a checkbox shown for all views except when the selected Saved View hardcodes archived visibility.
### 3.7.2 Browser filter key enum
```ts
// R1.6: UI label "Running" renamed to "Last Used" everywhere. No enum change — last_used_at already sorts by recency.
type BrowserItemStatus = "active" | "running" | "waiting" | "paused" | "completed" | "failed" | "archived" | "deleted"
// R1.6: Replaces loose status: string | null across all schemas.
type BrowserFilterKey =
| "open"
| "in_progress"
| "scheduled"
| "completed"
| "failed"
| "unread"
| "recent"
| "generated"
| "pinned"
| "errors"
| "preset"
| "archived"
| "todo"
| "unread_comments"
| "system"
| "has_context"
| "has_unread"
```
### 3.7.3 Filter semantics
| Filter key | Meaning |
|---|---|
| `open` | item is currently open in the active Q session |
| `in_progress` | task status is `queued`, `running`, or `waiting_human`; automation status is `running` |
| `scheduled` | task status is `scheduled` or automation has `next_run_at != null` and `enabled = true` |
| `completed` | task status is `completed` |
| `failed` | task status is `failed` |
| `unread` | item has unseen output or unseen updates |
| `recent` | `updated_at >= now - 7 days` |
| `generated` | item_type is `artifact` or document row has `is_generated = true` |
| `pinned` | `pinned = true` |
| `errors` | bucket health is `degraded` or place availability is not `ok` |
| `preset` | row subtype is a preset subtype |
| `archived` | row or owning record is archived |
| `todo` | note has TaskList blocks with open (undone) items. R1.7: was note_kind = "todo", now checks block content. |
| `unread_comments` | note has `comment_unread_count > 0` |
| `system` | bucket is `system_managed = true` |
| `has_context` | project has primary bucket or at least one attached bucket |
| `has_unread` | project has at least one child row with `unread = true` |
### 3.7.4 Filter support by visible type
```ts
const FILTER_SUPPORT_BY_TYPE: Record<BrowserItemType, BrowserFilterKey[]> = {
document: ["generated", "recent", "pinned", "archived", "errors"],
chat: ["open", "unread", "recent", "pinned", "archived"],
preset: ["recent", "pinned", "archived", "preset"],
skill: ["recent", "pinned", "archived"],
task: ["in_progress", "scheduled", "completed", "failed", "pinned", "archived", "preset", "recent"],
agent: ["recent", "pinned", "archived"],
panel: ["open", "unread", "recent", "pinned", "archived"],
forum: ["open", "unread", "recent", "pinned", "archived"],
overlay: ["recent", "pinned", "archived"],
note: ["recent", "pinned", "todo", "unread_comments", "archived"],
bucket: ["pinned", "archived", "errors", "system", "recent"],
project: ["archived", "has_context", "has_unread", "recent"],
automation: ["in_progress", "scheduled", "recent", "pinned", "archived"],
artifact: ["generated", "recent", "pinned", "archived"],
place: ["errors", "pinned", "recent", "archived"],
}
```
### 3.7.5 Adaptive filter resolver
```ts
function resolveVisibleFilterKeys(selectedTypes: BrowserItemType[]): BrowserFilterKey[] {
const types = selectedTypes.length ? selectedTypes : ALL_VISIBLE_BROWSER_ITEM_TYPES
const keys = new Set<BrowserFilterKey>()
for (const t of types) {
for (const key of FILTER_SUPPORT_BY_TYPE[t] ?? []) keys.add(key)
}
return FILTER_DISPLAY_ORDER.filter(key => keys.has(key))
}
const FILTER_DISPLAY_ORDER: BrowserFilterKey[] = [
"open", "in_progress", "scheduled", "completed", "failed",
"unread", "recent", "generated", "pinned", "todo",
"unread_comments", "errors", "preset", "system",
"has_context", "has_unread", "archived",
]
```
### 3.7.6 Sort key enum
```ts
type BrowserSortKey = "updated_at" | "title" | "item_type" | "last_used_at" | "created_at"
```
Sort dropdown labels:
- `updated_at` → Date Modified
- `title` → Alphabetical
- `item_type` → Type
- `last_used_at` → Last Used
- `created_at` → Created Date
Default sort: `updated_at desc` for all views except project lists and saved views (which default to `title asc` unless overridden).
### 3.7.7 Filter application order
Browser filtering applies in this exact order:
1. current page hint, if `follow_current_page = true` and `search_mode = "this_view"`
2. primary scope filter
3. folder filter (if Folders scope is active)
4. collection intersection filter
5. type filter
6. filter predicates
7. search scoring / search match pruning
8. sort
9. pagination / virtualization window
## 3.8 Follow Current Page
### 3.8.1 Behavior
When Follow Current Page is on and search_mode is "this_view", the browser automatically adjusts its results based on the active main content page:
- Notes page → hints toward notes
- Tasks page → hints toward tasks
- Agents page → hints toward agents
- etc.
The hint narrows the default results but does not override explicit type chip selections.
### 3.8.2 Configuration
Follow Current Page can be toggled via a small icon in the search bar or in browser settings.
### 3.8.3 Elnor browser awareness
Elnor can query the browser and present results directly in chat using BrowserQuerySkill. When Elnor performs a browser query, the results include clickable deep links so the user can navigate directly to any result.
## 3.9 Browser results list
### 3.9.1 General behavior
The results area is the main item list. It is virtualized for large lists and supports mixed-type lists.
**Each item renders as a single line** at **32px row height** with the following inline layout:
```
[pin icon?] [unread dot?] [title — truncated with ellipsis] ... [status?] [TYPE label] [time]
```
Field details:
- **Pin icon**: 8px pin icon in warn color. Only shown for pinned items. Flush left.
- **Unread dot**: 4px dot in accentBtn color. Only shown for unread items.
- **Title**: flex:1, minWidth:0, fontSize 11.5, fontWeight 600 for unread / 450 for read. Overflow: hidden, text-overflow ellipsis, white-space nowrap.
- **Status badge** (optional): 8.5px uppercase text. Color varies by status (running=accentBtn, waiting=warn, complete=green, scheduled=neutral). Only shown for task-type items with an active status.
- **Type label**: 8.5px uppercase, fontWeight 600, letterSpacing 0.03em. Flush right before time. Color is type-specific: Note=accentBtn, Chat=green, Doc=textTer, Task=warn, Agent=#5B5F97, Panel=accent, Forum=accent, Preset=neutral, Skill=neutral, Bucket=textTer, Project=accentBtn, Overlay=neutral. minWidth 28px, textAlign right.
- **Time**: 9px, color textTer. Relative format (2m, 4h, 1d, 1w). flexShrink:0, minWidth 18px, textAlign right.
**Project color dots and project names are NOT shown on individual rows.** Project context is established by the scope selection above, not repeated per-row.
### 3.9.2 Selection and open behavior
Use standard Mac selection behavior:
- **single click** = select row
- **double click** = default open action
- **Cmd-click** = toggle multi-selection
- **Shift-click** = range selection
- **Enter/Return** = open selected item
- **Right click** = context menu (see §3.10)
No checkbox gutter is required for standard browser multi-select.
### 3.9.3 Default open behavior by type
- Document → open preview/viewer if supported; otherwise reveal/open in Finder/default system app
- Note → open note editor
- Context Bucket → open DOC7 bucket page
- Project → open project page
- Chat (including room subtypes) → open conversation surface
- Panel / Forum → open that surface
- Task → open task configure/detail page
- Agent → open agent config page
- Skill → open skill config page
- Saved View → activate saved view
- Collection → activate filter view
### 3.9.4 Multi-select action bar
When `selected_row_count >= 2`, Q MUST render a compact bulk action bar above the results list.
```text
[12 selected] [Add to Project…] [Add to Collection…] [Pin] [Unpin] [Remove from Project] [Clear]
```
Rules:
- actions shown are the intersection of capabilities across all selected rows;
- destructive actions require explicit confirmation;
- `Add to Project…` opens the same selector used elsewhere;
- `Remove from Project` is shown only if every selected row supports project membership removal in the current scope;
- `Clear` clears current selection;
- the bulk action bar disappears when selection count returns to 0 or 1.
### 3.9.5 Keyboard navigation
- Arrow keys move selection up/down through the results list.
- Enter opens the selected item.
- Escape clears selection and returns focus to search bar.
- Type-ahead filtering: typing while the results list is focused narrows visible results by title prefix. The filter resets after 500ms idle.
- Tab moves focus between browser sections (search → scope → type → results).
### 3.9.6 Pinned items
Pinned items sort to the top of the results list within their current sort order. Pin/unpin is available via right-click context menu (§3.10).
### 3.9.7 Scrollbar
The browser results list uses a **4px thin auto-hide scrollbar**. The scrollbar thumb is invisible by default and fades in when the user hovers over the scrollable container. Thumb color: #d1d5db. Track: transparent. This scrollbar style applies globally within the browser column, the notes editor, and the comment rail.
```css
::-webkit-scrollbar { width: 4px }
::-webkit-scrollbar-track { background: transparent }
::-webkit-scrollbar-thumb { background: transparent; border-radius: 2px }
*:hover::-webkit-scrollbar-thumb { background: #d1d5db }
```
### 3.9.8 Empty states (R1.6)
| State | Message | Action |
|---|---|---|
| No results (filtered) | "No items match your filters." | "Try removing some filters or searching everywhere." Link: "Clear all filters" |
| No results (search) | "No results for '{query}'" | "Try different keywords or search everywhere." |
| No results (empty scope) | "No {type} items in {scope_name}" | Context-appropriate action: "Create a note" / "Add a document" |
| Loading | Skeleton rows (3–5 shimmer rows matching BrowserItemSchema layout) | — |
### 3.9.9 BrowserResolver performance contract (R2 — expanded from R1.6)
**Canonical filename:** `browser_catalog_current.json` everywhere. This is the single materialized read model for all browser queries.
**Schema envelope:**
```ts
interface BrowserCatalogEnvelope {
materialized_at: string // ISO timestamp of last materialization
source_count: number // number of sources read
item_count: number // total items in catalog
degraded_sources: string[] // source names that failed on last build
items: BrowserItemSchema[]
}
```
**Cold-start:** On first launch or if `browser_catalog_current.json` is missing/corrupt, EC rebuilds synchronously from all 13+ sources. Q shows loading skeleton (§3.9.8) during rebuild. On subsequent launches, EC loads from cache and incrementally updates via file watchers.
**Update notification:** EC emits `browser.catalog_updated` via the Q Backend WebSocket (§Q Dashboard 1.4) when the materialized read model changes. Q re-reads the file on receipt. 500ms debounce to coalesce rapid changes.
**Rebuild command:** `BrowserCatalogRebuildCommand` (§8) for manual recovery. Available via Settings or command palette.
**Integrity check:** On load, EC validates `item_count` matches actual array length. Mismatch triggers automatic rebuild.
EC maintains `browser_catalog_current.json` as a materialized read model. This file contains the pre-computed `BrowserItemSchema[]` array for all items. Q reads this file for browser queries instead of computing results from raw data files.
**SLOs:**
| Operation | Target | Degraded threshold |
|---|---|---|
| Cached query (no filter change) | < 200ms | > 500ms |
| Delta update (incremental) | < 250ms | > 600ms |
| Full rebuild | < 3s | > 8s |
**Degraded-source behavior:** If any data source (notes, chats, artifacts, etc.) is unreachable or stale, the browser still renders with available data. A warning badge appears in the browser footer: "⚠ Some sources unavailable — results may be incomplete." The badge tooltip lists which sources are degraded.
## 3.10 Right-click context menu
Browser rows expose actions by **capability**, not by hardcoded global menu.
```ts
type BrowserCapability =
| "open"
| "open_in_new_tab"
| "rename"
| "duplicate"
| "delete"
| "add_to_collection"
| "assign_project"
| "remove_from_project"
| "move_to_project"
| "show_in_finder"
| "open_with_default_app"
| "reveal_source_bucket"
| "attach_to_bucket"
| "export"
| "pin"
| "archive"
| "cancel"
| "copy_link"
| "copy_reference"
| "move_to_folder"
```
Capability map:
```ts
const CAPABILITIES_BY_TYPE: Record<BrowserItemType, BrowserCapability[]> = {
document: ["open", "open_in_new_tab", "show_in_finder", "open_with_default_app", "assign_project", "attach_to_bucket", "pin", "copy_link", "copy_reference"],
chat: ["open", "open_in_new_tab", "assign_project", "move_to_project", "remove_from_project", "archive", "pin", "copy_link", "copy_reference"],
preset: ["open", "open_in_new_tab", "duplicate", "delete", "pin", "copy_link", "copy_reference"],
skill: ["open", "open_in_new_tab", "pin", "copy_link", "copy_reference"],
task: ["open", "open_in_new_tab", "assign_project", "move_to_project", "remove_from_project", "cancel", "pin", "copy_link", "copy_reference"],
agent: ["open", "open_in_new_tab", "duplicate", "pin", "copy_link", "copy_reference"],
panel: ["open", "open_in_new_tab", "assign_project", "move_to_project", "remove_from_project", "pin", "copy_link", "copy_reference"],
forum: ["open", "open_in_new_tab", "assign_project", "move_to_project", "remove_from_project", "pin", "copy_link", "copy_reference"],
overlay: ["open", "open_in_new_tab", "pin", "copy_link", "copy_reference"],
note: ["open", "open_in_new_tab", "assign_project", "move_to_project", "remove_from_project", "duplicate", "delete", "export", "pin", "copy_link", "copy_reference", "move_to_folder"],
bucket: ["open", "open_in_new_tab", "attach_to_bucket", "pin", "copy_link", "copy_reference"],
project: ["open", "open_in_new_tab", "archive", "delete", "pin", "copy_link", "copy_reference"],
automation: ["open", "open_in_new_tab", "assign_project", "move_to_project", "remove_from_project", "cancel", "pin", "copy_link", "copy_reference"],
artifact: ["open", "open_in_new_tab", "show_in_finder", "assign_project", "move_to_project", "remove_from_project", "export", "pin", "copy_link", "copy_reference"],
place: ["open", "show_in_finder", "rename", "delete", "pin", "copy_link", "copy_reference"],
}
```
Context menu rendering order:
1. Open
2. Open in New Tab
3. separator
4. Add to Project…
5. Move to Project…
6. Remove from Project
7. Move to Folder…
8. Add to Collection…
9. Attach to Bucket…
10. separator
11. Pin / Unpin
12. Copy Link
13. Copy Reference
14. Reveal in Finder / Show in Finder
15. Open with Default App
16. Export
17. Duplicate
18. Rename
19. separator
20. Archive
21. Cancel
22. separator
23. Delete
### 3.10.2 Copy Reference
"Copy Reference" copies a structured reference string to the clipboard that can be pasted into the chat composer, note editor, or any text field. Format:
```
@[{title}]({item_type}:{item_id})
```
Examples:
- `@[Henderson_Complaint.pdf](document:d1)`
- `@[Henderson Discovery Priorities](note:n1)`
- `@[Review batch #4](task:t1)`
When pasted into the **chat composer**, the reference renders as a clickable chip showing the item's title and type icon. Elnor receives the reference in the message context with the item's type and ID, allowing him to resolve it via NoteReadSkill, BrowserQuerySkill, or other appropriate skill without the user needing to explain what the item is or where to find it.
When pasted into the **note editor**, the reference renders as a `[[link]]` to the item (consistent with §6.13).
Copy Reference is available on **every item type** in both the main browser and the notes list.
### 3.10.1 Show in Finder
`Show in Finder` / `Reveal in Finder` is only available for **path-backed items**:
- local file document rows
- linked folder alias rows
- place rows
- generated artifacts with `path`
- exported note files
- project output folder
It is not available for rows that have no meaningful filesystem target.
## 3.11 Drag and drop
### 3.11.1 Priority
DOC20 prioritizes drag/drop for **documents first**.
Supported first-wave document drops:
- document → chat composer / attachment well
- document → project context docs
- document → context bucket
- document → panel/forum config
- document → task config if task surface supports it
- **any browser item → note editor** (creates a `[[link]]` to the item at the drop position — see §6.13)
- **any browser item → folder** in Folders scope detail (assigns item to that folder)
### 3.11.2 Project membership drop
A dedicated drag/drop surface exists in Project Configure and in the persistent project header to add eligible items to a project.
Eligible first-wave dragged item types:
- chat / room
- panel
- forum
- task / automation
- note
- document / artifact
- bucket
If a dragged item type is not supported for a target surface, Q must show a clear no-drop state.
### 3.11.3 Move confirmation
If an item can belong to only one project and already belongs to another, drop must prompt:
- Move to this project
- Cancel
### 3.11.4 Drop payload schema
```ts
interface ProjectDropPayload {
item_type: ProjectMemberType | "document" | "bucket" // R2: renamed
item_id: string
title: string
current_project_id: string | null
}
```
If the payload is unsupported for the target, show no-drop state and do not submit a command.
## 3.12 File handling, Places, and aliasing
### 3.12.1 Core principle
The browser must prefer **references/aliases over copies**.
Adding a Place, linking a folder into a project, or showing a generated artifact in a list must not duplicate the underlying file by default.
### 3.12.2 Place aliases
A **Place** is a saved filesystem alias/reference for browsing.
Initial place support is folder-first.
```ts
interface PlaceAliasSchema {
place_id: string
title: string
path: string
kind: "folder"
availability_status: "ok" | "missing" | "needs_access"
last_checked_at: string | null
pinned: boolean
created_at: string
updated_at: string
}
```
### 3.12.3 Linked folder aliases in projects
A **linked folder alias** is a project-level reference to a Place, allowing project documents to include filesystem-backed files.
### 3.12.4 Unsupported files
Files in Places or linked folders that are not natively previewable or not directly usable should still be visible when possible, but visually marked.
### 3.12.5 Add-to-context behavior
Adding a file from Places or a linked folder to context does **not** copy the file content into EC durable storage by default. It creates the appropriate DOC7/local-path reference, after which DOC7 indexing/caching rules apply.
### 3.12.6 Rename/delete behavior for external files
Browser rename/delete for external files from Places and linked folders:
- rename should rename the filesystem file where possible;
- delete should remove the alias/reference; filesystem deletion requires additional confirmation;
- external path-backed files should generally use **Show in Finder**;
- generated artifacts with known paths may allow direct rename and Trash-move.
### 3.12.7 Place add / refresh / remove flow
Availability detection rules:
- `ok` = path exists and is readable by the running app process
- `missing` = path does not exist
- `needs_access` = path exists but the running app process does not currently have read permission
Place add flow:
1. User clicks `+ Add Place`.
2. OpenClaw opens a native folder picker.
3. Q submits `place_add`.
4. EC validates path against allowed roots policy.
5. EC writes the PlaceAlias record.
6. Browser refreshes.
Place refresh flow:
- right-click Place row → `Refresh`
- Q submits `place_refresh`
- EC rescans availability and immediate children metadata only
- no file content is imported during refresh
Place remove flow:
- removing a Place deletes the alias record only
- it does not delete the external folder or any files inside it
- if a linked folder alias references the removed place, that linked folder alias becomes `broken` until reassigned or removed
### 3.12.8 Linked folder alias semantics
A linked folder alias is a project-level reference to a Place.
```ts
interface LinkedFolderAliasSchema {
linked_folder_id: string
project_id: string
place_id: string
mode: LinkedFolderMode
sync_status: LinkedFolderSyncStatus
include_globs: string[]
exclude_globs: string[]
last_scan_at: string | null
last_sync_at: string | null
created_at: string
updated_at: string
}
type LinkedFolderMode = "browse_only" | "sync_to_project_bucket"
type LinkedFolderSyncStatus = "idle" | "syncing" | "ok" | "warning" | "error"
```
Rules:
- one `LinkedFolderAlias` points to exactly one `PlaceAlias`
- multiple projects may link the same Place
- linked folder aliases do not create DOC7 file rows unless explicit sync or add-to-context occurs
- linked folder aliases may be `browse_only` or `sync_to_project_bucket`
### 3.12.9 sync_to_project_bucket semantics
`sync_to_project_bucket` is **opt-in** and is defined as:
- source of truth remains the filesystem folder;
- EC creates or updates DOC7 local-path file refs in the project's primary bucket for files matching include rules;
- EC never copies file bytes into project storage solely because sync is enabled;
- EC stores refs and indexing metadata only, consistent with DOC7 local-path refs;
- sync job is manual by default (`Sync now` button);
- optional watch mode may be enabled later but is out of scope for v1.
Default inclusion/exclusion globs:
```ts
const LINKED_FOLDER_DEFAULT_INCLUDE_GLOBS = [
"**/*.md", "**/*.txt", "**/*.docx", "**/*.pdf",
"**/*.rtf", "**/*.html", "**/*.json",
]
const LINKED_FOLDER_DEFAULT_EXCLUDE_GLOBS = [
"**/.git/**", "**/node_modules/**", "**/.DS_Store", "**/~$*",
]
```
Dedupe rule: if a candidate file path already exists as a DOC7 file ref in the primary project bucket, update/reindex that row instead of creating a duplicate.
### 3.12.10 Linked folder sync status
A linked folder row in project documents should show:
- alias title
- path
- mode
- last scan at
- last sync at (if sync_to_project_bucket)
- sync status badge
## 3.13 Saved Views
### 3.13.1 Definition
A Saved View is a persisted snapshot of a browser filter/scope configuration that can be restored with one click.
### 3.13.2 Save flow
See §3.2.4 for the save current view flow.
### 3.13.3 Labels
Saved views appear in the scope detail list when the Saved Views scope is selected, and in the search dropdown.
### 3.13.4 Built-in Saved Views (R4 — updated)
DOC20 defines these built-in saved views. The "Current" and "No Project" built-in views have been **removed** (R4). Only "Recent" remains as a system view. Default user views are seeded on first launch but can be edited or deleted.
```ts
const BUILT_IN_SAVED_VIEWS: SavedViewSchema[] = [
{
saved_view_id: "sv_recent",
title: "Recent",
scope_family: null,
scope_detail_ids: [],
collection_ids: [],
type_ids: [],
filters: { recent: true },
sort_key: "updated_at",
sort_dir: "desc",
search_query: "",
search_mode: "this_view",
include_archived: false,
pinned: true,
system_view: true,
system_view_key: "recent",
},
]
// Default user-created views (seeded on first launch, editable/deletable):
const DEFAULT_USER_VIEWS: SavedViewSchema[] = [
{
saved_view_id: "sv_deadlines_week",
title: "Deadlines this week",
scope_family: null,
scope_detail_ids: [],
collection_ids: [],
type_ids: [],
filters: { due_this_week: true },
sort_key: "updated_at",
sort_dir: "asc",
search_query: "",
search_mode: "this_view",
include_archived: false,
pinned: false,
system_view: false,
system_view_key: null,
},
{
saved_view_id: "sv_active_matters",
title: "Active matters",
scope_family: "project",
scope_detail_ids: [],
collection_ids: [],
type_ids: [],
filters: { open: true, in_progress: true },
sort_key: "updated_at",
sort_dir: "desc",
search_query: "",
search_mode: "this_view",
include_archived: false,
pinned: false,
system_view: false,
system_view_key: null,
},
]
```
### 3.13.5 BrowserQuery read contract
The Browser UI and BrowserQuerySkill MUST use the same normalized read contract.
```ts
interface BrowserQueryRequest {
q_session_id?: string
follow_current_page: boolean
current_page_hint: BrowserPageHint | null
search_mode: "this_view" | "everywhere"
search_query: string
selected_collection_ids: string[]
primary_scope_family: BrowserPrimaryScopeFamily
primary_scope_detail_id: string | null
selected_type_ids: BrowserItemType[]
active_filter_state: Partial<Record<BrowserFilterKey, boolean>>
sort_key: BrowserSortKey
sort_dir: "asc" | "desc"
include_archived: boolean
limit: number
cursor?: string | null
}
interface BrowserQueryResponse {
items: BrowserItemSchema[]
total_count: number
next_cursor: string | null
available_type_ids: BrowserItemType[]
available_filter_keys: BrowserFilterKey[]
applied_state: BrowserViewState
warnings: string[]
}
```
### 3.13.6 BrowserQuery execution rules
1. `open` and `unread` may depend on Q session overlay state.
2. If `q_session_id` is absent:
- `open` is ignored;
- `unread` falls back to durable unread/has_unseen flags from owner read models only;
- response `warnings[]` includes `"SESSION_OVERLAY_UNAVAILABLE"`.
3. BrowserQuerySkill MUST accept `q_session_id` when invoked from the active Q chat session.
4. BrowserQuerySkill MUST return `deep_link` metadata on every result row.
5. BrowserQuerySkill MUST never write durable state.
### 3.13.7 Browser resolver pseudocode
```ts
function runBrowserQuery(req: BrowserQueryRequest, catalog: BrowserItemSchema[], session?: BrowserSessionOverlay): BrowserQueryResponse {
let rows = catalog
if (req.follow_current_page && req.search_mode === "this_view" && req.current_page_hint) {
rows = rows.filter(row => pageHintMatches(req.current_page_hint!, row))
}
rows = applyPrimaryScope(rows, req.primary_scope_family, req.primary_scope_detail_id)
rows = applyCollectionIntersection(rows, req.selected_collection_ids)
rows = applyTypeFilter(rows, req.selected_type_ids)
rows = applyPredicateFilters(rows, req.active_filter_state, session, req.include_archived)
rows = applySearch(rows, req.search_query, req.search_mode)
rows = sortRows(rows, req.sort_key, req.sort_dir)
const total = rows.length
const paged = paginate(rows, req.limit, req.cursor)
return {
items: paged.items,
total_count: total,
next_cursor: paged.next_cursor,
available_type_ids: deriveAvailableTypes(rows),
available_filter_keys: resolveVisibleFilterKeys(req.selected_type_ids),
applied_state: deriveBrowserViewState(req),
warnings: deriveBrowserWarnings(req, session),
}
}
```
## 3.14 Project color usage
Each project has a configurable color from the 10-color palette shared with Collections.
This color appears in:
- the project header dot;
- the project switch dropdown;
- the project scope detail list in the Browser;
- project rows when the Browser is listing projects as items;
- notes lists and other project-specific lists where the spec explicitly says a project color dot is shown.
This color does **not** appear on general mixed browser result rows when the Browser is showing non-project items. Project context is carried by the selected scope and by the item's deep link metadata, not by repeating the project color on every row.
## 3.15 Folders
### 3.15.1 Purpose
Folders provide virtual hierarchical organization for the user's items. They are purely for visual organization — they do not change how items are stored, which project they belong to, or any durable metadata beyond the folder assignment.
### 3.15.2 Data model
```ts
interface BrowserFolderSchema {
folder_id: string
title: string
parent_folder_id: string | null // null = root folder
order_index: number
created_at: string
updated_at: string
}
interface BrowserFolderMembershipSchema {
folder_id: string
item_type: BrowserItemType
item_id: string
created_at: string
}
```
### 3.15.3 Folder hierarchy rules
- Folders support unlimited nesting depth (but UI should practically discourage more than 3-4 levels).
- An item can belong to **zero or one** folder. Folder membership is optional — items without a folder assignment simply don't appear when the Folders scope is filtered to a specific folder.
- An item's folder assignment is independent of its project membership. You can have a note in Project Henderson AND in Folder "Privilege Issues" — these are orthogonal organizational axes.
- Deleting a folder detaches all items from it (items survive). Subfolders are also deleted, and their items are detached.
- Renaming a folder does not affect its items.
### 3.15.4 Folder scope behavior
When the Folders scope is selected in the Browser:
1. The scope detail list shows the folder tree (expandable/collapsible, indented by depth).
2. Selecting a specific folder shows only items directly in that folder (not recursive by default).
3. A "Show nested" toggle in the filter bar enables recursive display of all items in the selected folder and its descendants.
4. Items without any folder assignment are shown when no specific folder is selected (i.e., the Folders scope root shows "Unfiled" items).
5. Type chips and filters still apply on top of the folder filter.
### 3.15.5 Folder management
- **Create folder**: "+" button in Folders scope detail header. Prompts for name only.
- **Create subfolder**: Right-click parent folder → "New Subfolder". Or drag a folder onto another folder.
- **Rename**: Right-click → Rename. Inline edit in scope detail list.
- **Delete**: Right-click → Delete. Confirms with "This will remove the folder. Items in this folder will become unfiled." Subfolders are also deleted.
- **Move folder**: Drag a folder to a new parent in the scope detail tree.
### 3.15.6 Assigning items to folders
- **Drag and drop**: Drag any browser item onto a folder in the Folders scope detail list.
- **Right-click → Move to Folder…**: Opens a folder picker tree. Includes "Remove from folder" option.
- **Bulk action**: Multi-select items → "Move to Folder…" in bulk action bar.
### 3.15.7 Folder visibility rules
Folders are **only visible when the Folders scope is selected**. When the user is in Project scope, Bucket scope, Places scope, or Saved Views scope, folder hierarchy is completely hidden. This avoids visual clutter.
The exception: if a Saved View was created while the Folders scope was active with a specific folder selected, restoring that Saved View activates the Folders scope and selects that folder.
### 3.15.8 Interaction with other scopes
Folders are orthogonal to projects and collections:
- An item can be in a project AND in a folder AND tagged with collections simultaneously.
- Filtering by project scope shows all project items regardless of folder assignment.
- Filtering by folder scope shows all items in that folder regardless of project assignment.
- Collections always act as intersection filters on top of whatever scope is active.
## 3.16 Scope deselection (R1.4)
**Persistence rule (R1.6):** When a scope chip is deselected, only the scope-related fields are cleared (`primary_scope_family` → null, `primary_scope_detail_id` → null). All other filter state is preserved: `selected_collection_ids`, `selected_type_ids`, `active_filter_state`, `sort_key`, `sort_dir`, `search_query`.
Clicking an already-active scope chip **deselects** it, returning to "no scope" mode where all items are shown unfiltered by scope. The scope detail area shows "No scope — showing all items."
## 3.17 Extended sort options (R1.4)
The sort control is a dropdown with five options:
- **Modified** (default) — most recently modified first
- **Alphabetical** — A-Z by title
- **Type** — grouped by entity type
- **Created** — most recently created first
- **Running** — running/active items sorted to top, then by modified
The sort actually re-orders the results list. Pinned items always float to top.
## 3.18 Collections filter behavior (R1.4)
Clicking a collection dot toggles it as an active filter. Multiple collections active simultaneously use AND logic (item must be in all selected collections). Selected dots: dark border, 1.1x scale. "clear" button when any are active.
## 3.19 Extended type chip set (R1.4)
14 types: Document, Chat, Preset, Skill, Task, Agent, Panel, Forum, Overlay, Note, Bucket, Project, Artifact, Prompt. Each toggles independently. Multiple active: OR logic. "clear" button when any active.
## 3.20 Folder overlay toggle (R1.4)
A persistent folder overlay accessible from any scope via a **folder icon button** (dashed border, no label) at the end of the scope chip row.
When active:
- Compact folder tree strip (max-height 90px, scrollable) appears between scope detail and type chips
- Header: "Drag items to folders" + X close
- Full folder hierarchy with expand/collapse
- Results items become **draggable** (grab cursor)
- Drag onto folder → dashed blue border drop target → toast confirms assignment
- Current scope, filters, search, sort all remain active
- Auto-hides when switching to Folders scope (already visible)
- Browser footer shows "📁 Folders" when active
Solves: assigning items to folders while in a filtered/scoped view.
### 3.20.1 Folder overlay DnD implementation contract (R2 — expanded)
**State machine:**
1. `idle` — no drag in progress
2. `dragging` — user has picked up item(s). Source items show reduced opacity. Drop targets highlight on hover.
3. `drop_pending` — user released on a valid target. Optimistic move: Q immediately moves the item(s) in the browser results list. EC command in flight.
4. `committed` — EC accepted. No further action.
5. `reverted` — EC rejected (e.g., folder doesn't exist, permission error). Q reverts the optimistic move. Error toast shown.
**Optimistic update:** On drop, Q immediately updates the browser results list and folder tree to reflect the move. If EC rejects, Q reverts to the previous state. This provides instant visual feedback.
**Cross-scope edge cases:**
- Drag from "No Project" scope into a folder: item moves to folder, no project change.
- Drag while a filter is active: item may disappear from the filtered results after moving (expected — filter still applies). Toast: "Moved to {folder_name}."
- Drag while Places scope is active: Places items are filesystem references and cannot be moved to virtual folders. Show no-drop cursor on all folder targets.
- Drag multiple selected items: all move together. If any single item fails, the entire batch reverts.
- Multi-select drag: All selected items move to the target folder. Payload: `{ item_ids: string[], target_folder_id: string }`
- Auto-expand on hover: If a collapsed folder is hovered for > 500ms during drag, it expands
- Cancel: Dropping outside any folder cancels the move
- Folder delete/rename during active drag: Blocked (folder actions disabled while drag is in progress)
## 3.21 Browser footer (R1.4)
Compact bar inside browser column (bottom) showing:
- Active scope name (or "All" if none)
- Project name / "No Project" when applicable
- Active collection names
- Active type chip names
- "📁 Folders" when overlay active
- Sort key (right-aligned)
## 3.22 Archive/Delete browser items (R1.4)
Right-click context menu on any result item gains:
- **Archive** — sets status "archived." Hidden by default. Toggle in filter bar to show.
- **Delete** — inline "Delete permanently? Yes / No" confirmation. Soft-delete (preserved in event log).
## 3.23 Save view from any scope (R1.4)
A save/bookmark icon next to the sort dropdown in the filter bar. Click → inline input below filter bar with name field, Save button, Cancel button (Escape also dismisses). Saved views appear in the Saved Views scope.
In Saved Views scope: user-created views show Edit (pencil) and Delete (X) icons on hover. The built-in "Recent" view is not editable or deletable. "Save current view" appears as a subtle text link with save icon — click opens a popover with name input (R4).
## 3.24 Remove places (R1.4)
Hover a place in Places scope → X icon appears. Click → inline "Remove? Yes / No" confirmation. Files on disk NOT deleted; only the alias is removed. EC: `PlaceRemoveCommand`.
## 3.25 Delete folders (R1.4)
Hover a folder in Folders scope → trash icon appears (alongside + subfolder icon). Click → inline "Delete? Yes / No" confirmation. Items inside become unfiled. Subfolders cascade-deleted. EC: `FolderDeleteCommand`.
## 3.26 "No Project" clickable (R1.4)
In Project scope, "No Project" is a clickable item filtering to items with project_id = null. Visual: dashed circle border, italic when unselected, accent color when selected.
---
## 4. Projects
## 4.1 Project purpose
A Project is a durable grouping and default-context container for related work.
### 4.1.1 What selecting a project does
- filters visible items in the Browser to project members and project-scoped content;
- makes the project's DOC7 buckets available as default context for new chats, tasks, and other operations;
- scopes the Home tab notes workspace to project notes;
- provides a persistent surface for organizing chats, documents, tasks, panels, forums, and automations.
### 4.1.2 What selecting a project does not do
- does not silently alter unrelated chats, tasks, rooms, panels, or scheduled automations;
- does not create hidden background context injection;
- does not activate a global focus mode;
- does not prevent access to items outside the project.
### 4.1.3 Project membership model
DOC20 uses **single-primary-project membership** for workflow objects.
The following entity types support **0 or 1** primary project association in v1:
- chat / room
- panel
- forum
- task
- automation
- note
- generated artifact
Membership for these entity types is represented durably as a project membership edge plus the owning object's current `project_id` field/read-model projection.
The following do **not** use the same membership mechanism:
- context buckets → attached to projects through project-bucket attachment records;
- direct context documents in a project's primary bucket → represented by DOC7 file refs in the primary bucket;
- bucket-inherited documents → represented by attached bucket membership, not direct project membership;
- linked folders → represented by `LinkedFolderAlias` records;
- places, saved views, collections, folders → not project members.
### 4.1.3A Entity-by-entity membership table
| Entity type | Association model | Max projects | Remove from Project means | Move to Project means |
|---|---|---:|---|---|
| Chat / Room | `project_id` + membership edge | 1 | clear `project_id` → item becomes No Project | set `project_id` to new project |
| Panel | `project_id` + membership edge | 1 | clear `project_id` | set `project_id` to new project |
| Forum | `project_id` + membership edge | 1 | clear `project_id` | set `project_id` to new project |
| Task | `project_id` + membership edge | 1 | clear `project_id` | set `project_id` to new project |
| Automation | `project_id` + membership edge | 1 | clear `project_id` | set `project_id` to new project |
| Note | `project_id` + membership edge | 1 | clear `project_id` | set `project_id` to new project |
| Generated artifact | `project_id` on artifact record | 1 | clear artifact `project_id` only; origin stays unchanged | set artifact `project_id` to new project |
| Direct project context document | DOC7 file ref in primary project bucket | n/a | remove file ref from primary project bucket | re-add file ref to another project's primary bucket |
| Bucket-inherited document | attached bucket membership | n/a | detach bucket from project | attach bucket to another project |
| Linked folder alias | `LinkedFolderAlias` record | n/a | remove linked folder alias from project | add linked folder alias to another project |
### 4.1.4 Global vs No Project
"Global" means no project filter is active — the user sees everything.
"No Project" is a filter that shows only items with `project_id = null`.
### 4.1.5 Project schema
```ts
type ProjectStatus = "active" | "paused" | "archived"
type ProjectKind = "standard" | "global"
interface ProjectSchema {
project_id: string
project_kind: ProjectKind
title: string
slug: string
description: string
color_token: string
status: ProjectStatus
primary_project_bucket_id: string
default_agent_id: string | null
default_model_id: string | null
fallback_chain_id: string | null
think_level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
default_overlay_ids: string[]
output_folder_path: string | null
budget_enabled: boolean
budget_amount_usd: number | null
budget_period: "day" | "week" | "month" | "quarter" | null
budget_alert_threshold_pct: number | null
show_budget_in_header: boolean
created_at: string
updated_at: string
created_by: string
updated_by: string
background_instructions: string | null // R2: project-level instructions and context notes
current_spend_usd: number // R2: read-only projection from EC cost tracking
current_spend_updated_at: string | null // R2
archived_at: string | null
deleted: boolean
deleted_at: string | null
}
```
### 4.1.6 Project bucket attachment schema
```ts
type ProjectBucketAttachmentRole = "primary" | "attached"
interface ProjectBucketAttachmentSchema {
project_id: string
bucket_id: string
role: ProjectBucketAttachmentRole
order_index: number
created_at: string
created_by: string
}
```
### 4.1.7 Project creation flow
Project creation MUST be atomic from the user's point of view.
Flow:
1. User submits `project_create`.
2. EC allocates `project_id`.
3. EC creates the primary DOC7 bucket first:
- title = `{project.title} — Project Bucket`
- summary = `Primary project bucket for {project.title}`
- system_managed = false
- pinned = false
4. EC allocates `primary_project_bucket_id`.
5. EC writes:
- project create event
- primary bucket create event (via DOC7 command)
- project-bucket attachment row with `role = "primary"`
6. EC materializes `ProjectSchema` current state.
7. Q opens `/projects/:project_id/home`.
Failure rule: if primary bucket creation fails, project creation fails and no project record is materialized.
### 4.1.8 Project page route contract
```ts
type ProjectTabId = "home" | "documents" | "activity" | "project_context" | "configure"
type ProjectDocumentsSubtabId = "generated" | "context_docs" | "notes" | "all"
type ProjectActivitySubtabId = "chats" | "panels_forums" | "tasks" | "automations" | "all"
```
Routes:
- `/projects/:project_id`
- `/projects/:project_id/:tab`
- query param `documents_subtab`
- query param `activity_subtab`
Defaults: missing `:tab` → `home`; invalid subtab → first valid subtab for that tab.
### 4.1.9 Paused project semantics
`paused` means:
- the project remains visible and openable;
- creating new items from outside the project does not auto-suggest it;
- creating new items **from inside** the project page still defaults to that project;
- existing project members continue to retain their project membership;
- existing project members continue to inherit project context on their normal project-scoped turns/dispatches;
- automated scheduled work already attached to the project is not cancelled solely because the project is paused.
`paused` is therefore a **creation-default / discovery** state, not a context-removal state.
## 4.2 Project page header
The project page header contains:
**First row** (flex, align center):
- Project color dot (12px circle)
- Project name (h1, 20px, fontWeight 700)
- **Switch project button**: subtle bordered button with "Switch ▾" label. Opens a dropdown listing all projects with color dots. Current project shows a check. Clicking a project switches the page.
- flex spacer
- **Persistent "Add to Project / Drop here" surface** (right-aligned): 2px dashed border in accentBtn at 40% opacity, padding 10px 24px, borderRadius md, minWidth 260px, fontSize 13.5, fontWeight 600. Persists across all tabs. Valid drag-and-drop target. On hover, border becomes solid accentBtn. On drop, triggers standard project membership add flow.
**Below the add-to-project surface** (right-aligned):
- Cost indicator: subtle pill (fontSize 10, textTer color, bgInput background, borderRadius full). Shows "$XX.XX / $YYY" format. Clicking hides it for the session. Only visible when budget tracking is enabled and "Show budget in project header" is on (see §4.7.7).
**Second row**:
- Project description (fontSize 13, textSec color, left margin 22px). Editable in Configure tab (§4.7.2).
### 4.2.1 Autosave
Project metadata edits autosave. A separate save button is not required for project metadata.
## 4.3 Project tabs
Project page tabs:
1. **Home** (full notes workspace as default view — see §4.3.2)
2. **Documents**
3. **Activity**
4. **Project Context**
5. **Configure**
Default tab: **Home**.
### 4.3.1 Tab visual style
Tabs use **connected box style**:
- Tabs share borders: each tab has a 1.5px border on all sides, with `borderRight: none` between adjacent tabs (except the last).
- **Active tab**: backgroundColor bgPanel (white), color accentBtn, borderBottom 2px solid accentBtn, zIndex 1, marginBottom -1. Font weight 700 for Home/Documents/Activity, 650 for Project Context/Configure.
- **Inactive tabs**: backgroundColor **#E8EAEE**, color **#2A2D35**, border 1.5px solid #CDD1D8. Font weight 450.
- First tab: borderRadius "6px 0 0 0". Last tab: borderRadius "0 6px 0 0".
### 4.3.2 Home tab
The Home tab renders the full notes workspace within the project page. This is functionally identical to the standalone Notes page (§6) but scoped to the current project's notes.
Layout: three-pane (note list | editor | comment rail) consuming the full content area below the tabs.
The note list defaults to showing only notes belonging to the current project. Filter pills (All, Pinned, Todo) filter within the project scope. The "New" button creates a note automatically associated with the current project.
The note list is **collapsible** — a collapse button (SplitV icon) appears in the note list header next to "New." When collapsed, the note list shrinks to a 28px vertical bar with a chevron and rotated "Notes" label. Click to expand.
Note list font sizes: title 12.5px, excerpt 11px, metadata 9.5px.
### 4.3.2A Project page wireframe
```text
┌────────────────────────────────────────────────────────────────────────────┐
│ ● Henderson [Switch ▼] [ Add to Project / Drop here ]│
│ [$42.15 / $200] │
│ Securities fraud / deposition / motion work │
├────────────────────────────────────────────────────────────────────────────┤
│ [Home][Documents][Activity][Project Context][Configure] │
├────────────────────────────────────────────────────────────────────────────┤
│ Home = project-scoped notes workspace │
│ ┌──────────────┬───────────────────────────────────────┬─────────────────┐ │
│ │ Notes list │ Editor │ Comment rail │ │
│ │ │ │ │ │
│ └──────────────┴───────────────────────────────────────┴─────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
```
## 4.4 Documents tab
### 4.4.1 Subtabs
Subtab order: **Generated · Context Docs · Notes · All**
Subtabs use the same connected box style as main tabs but smaller (padding 7px 16px, fontSize 12.5). Active subtab: accentBtn color, accentBtn+"06" bg. Inactive: textTer color, bgPanel bg.
### 4.4.2 Shared list behavior across Documents subtabs
Each subtab shows a list of items with consistent row layout:
- icon (14px, type-colored)
- title (13px, fontWeight 550) + subtitle (11px, textTer)
- source badge (Pill)
- modified time (10px, textTer)
- Remove button (where applicable)
Hover: bgPanelAlt.
### 4.4.3 Notes subtab
Shows project notes with title, excerpt, modified time, comment count, pin indicator, and "Remove" button that detaches the note from the project (does not delete it).
### 4.4.4 Context Docs subtab
Shows project-relevant context documents using DOC7-backed project context plus linked folders.
Each document row shows a **source badge** as a Pill:
- Directly added documents: badge shows **"Project Bucket"** in accentBtn color. Shows a **"Remove" button**.
- Documents inherited from an attached bucket: badge shows **"Bucket: {bucket_name}"** in accent color with italic **"via bucket"** label. Do **NOT** show individual Remove button. User must detach bucket from Project Context tab.
### 4.4.5 Generated subtab
Shows generated artifacts associated with this project. Each row shows: origin source ("from Chat: Henderson discovery"), type badge (Artifact, Document, etc.), modified time, Remove button.
### 4.4.6 All subtab
Union of Generated + Context Docs + Notes, with type grouping headers.
## 4.5 Activity tab
### 4.5.1 Purpose
Activity shows chats, panels/forums, tasks, and automations associated with the project.
### 4.5.2 Subtabs
Subtab order: **Chats · Panels & Forums · Tasks · Automations · All**
Connected box style matching Documents subtabs.
### 4.5.3 Chats subtab
Shows project chats with: title, message count, last active time, subtype badge (Chat / Multi-Agent / Red-Team), unread indicator, Remove button.
### 4.5.4 Panels & Forums subtab
Shows project panels and forums with: title, type badge, status, participant count, Remove button.
### 4.5.5 Tasks subtab
Shows project tasks with: title, status pill (running/waiting/complete/scheduled/failed), assigned agent, cost, Remove button.
### 4.5.6 Automations subtab
Shows project automations with: title, enabled/disabled badge, next run time, last run status, Remove button.
### 4.5.7 All subtab
Union of all activity items with type grouping.
## 4.6 Project Context tab
### 4.6.1 Purpose
Project Context shows the DOC7 bucket configuration that provides default context for project-scoped work.
### 4.6.2 Core model
A project's context is built from:
1. **Primary project bucket** — auto-created with the project; the project's "own" document store.
2. **Attached shared buckets** — existing DOC7 buckets linked to the project for additional context.
3. **Linked folder aliases** — filesystem folder references optionally synced to the primary bucket.
### 4.6.3 UI sections
The Project Context tab shows:
- **Primary Bucket card**: title, health badge, document count, "Open Full Bucket" action.
- **Attached Buckets card**: list of attached buckets with health badges, "Detach" action per bucket, "Attach Bucket" action.
- **Linked Folders card** (if any): list of linked folder aliases with mode, sync status, "Sync Now" / "Remove" actions.
- **Project Background / Instructions**: editable text area for project-level instructions and context notes.
### 4.6.4 Inheritance behavior
Project context is durable through project membership and is applied at **operation assembly time**, not because a page is merely visible.
Project context candidate buckets are:
1. primary project bucket
2. attached project buckets in `order_index` order
3. surface-specific buckets already attached to the target
4. global buckets from DOC7
The project bucket candidate set MUST be de-duplicated by `bucket_id` before materialization.
### 4.6.5 Operation assembly timing by surface
| Surface | When project context is assembled |
|---|---|
| Chat / Room | on every new outbound user turn and on every agent turn |
| Task | at dispatch start and each resume after waiting_human / retry / scheduled wake |
| Panel | when a panel run is started and at each agent sub-dispatch |
| Forum | when a forum thread turn is dispatched |
| Automation | at execution start |
| Note AI request | when EC handles `note_ai_request` for a project-scoped note |
### 4.6.6 Project-context assembly pseudocode
```ts
function resolveProjectBucketsForTarget(target: {
target_type: "chat" | "room" | "task" | "panel" | "forum" | "automation" | "note_ai"
target_id: string
project_id: string | null
surface_bucket_ids: string[]
per_run_excluded_bucket_ids: string[]
}): string[] {
const buckets: string[] = []
if (target.project_id) {
const project = readProject(target.project_id)
buckets.push(project.primary_project_bucket_id)
for (const attached of readProjectAttachedBuckets(target.project_id)) {
buckets.push(attached.bucket_id)
}
}
for (const surfaceBucketId of target.surface_bucket_ids) buckets.push(surfaceBucketId)
for (const globalBucketId of readGlobalBucketIds()) buckets.push(globalBucketId)
return dedupePreserveOrder(buckets)
.filter(bucketId => !target.per_run_excluded_bucket_ids.includes(bucketId))
.filter(bucketId => !isArchivedOrDeletedBucket(bucketId))
}
```
### 4.6.7 Explicit bulk actions
Project Context MUST offer these explicit actions:
- **Apply Project Context to Open Now** — opens a review modal with checkboxes for current chat, open chats/rooms, open panels/forums, in-progress tasks, scheduled tasks. Nothing applied until user clicks **Apply**.
- **Attach Project Buckets to Selected Items**
### 4.6.8 Per-run exclude model
Per-run excludes remain DOC7-native:
- per-run excludes are ephemeral;
- they are stored in the dispatch/request object, not in project durable state;
- excluding a project bucket for one run does not detach it from the project;
- the UI must show excluded buckets visibly in the run/dispatch review UI.
### 4.6.9 Project Context vs Context Docs split
Project Context tab manages bucket relationships. Context Docs subtab (under Documents) shows the resulting visible files. They are not duplicate surfaces — one manages the plumbing, the other shows the visible content.
## 4.7 Configure tab
### 4.7.1 Purpose
Configure is the canonical place to manage project settings, membership, defaults, overlays, budget, and lifecycle.
### 4.7.2 Required sections
Configure contains the following sections in order:
1. **General** — Project name (editable input), description (editable textarea), color picker (10 preset dots, 20px each, selected has 2px border in textPri), status dropdown (Active / Paused / Archived — see §4.7.8).
2. **Project Content** — Table listing all content types (Chats, Notes, Tasks, Panels, Forums, Documents, Generated, Automations) with icon, count, **"View in Browser →"** link per row, and **"Manage Members →"** link per row. An **"Add to Project / Drop items here"** surface appears immediately after the "Project Content" title, spanning flex:1 maxWidth 300px.
3. **Defaults** — Default agent (dropdown), default model (dropdown), fallback chain (dropdown with chain preview), think level (dropdown: off/minimal/low/medium/high/xhigh).
4. **Prompt & Overlay (DOC17)** — Toggle: "Apply overlays to all project chats" (on/off). **Multiple overlays supported.** Each active overlay listed with remove button (×). "Add Overlay" button. Overlays set the default overlay set for new chats.
5. **Output & Cost** — Output folder (path + Change button). Budget tracking toggle (on/off). Budget amount (editable input). Budget period (dropdown: Daily / Weekly / Monthly / Quarterly). Current spend (display + percentage). Alert threshold toggle. "Show budget in project header" toggle. See §4.7.7.
6. **Project Management** — Standard border, neutral header color. Contains: Duplicate, Archive, Delete. See §4.7.9.
### 4.7.3 Add to Project selector modal
The selector modal has:
- title: "Add to Project: {project name}"
- search field
- type filter chips (Chat, Note, Task, Panel, Forum, All)
- multi-select item list (defaults to showing "No Project" items first)
- toggle: "Show items already in another project"
- if an item already has another project and is single-project-only, confirm move on submit
- Cmd-click and Shift-click selection supported
- Cancel / Add Selected buttons
### 4.7.3A Manage Members drawer
Clicking **Manage Members →** on a content type row opens an in-page drawer for that type:
- title: `Manage {Type} in {Project}`
- search field
- item list filtered to items already in the project
- per-item "Remove from Project" action
- "Add…" button opens the Add to Project selector modal pre-filtered to that type
- "Close" button
### 4.7.4 Single-project move rule
If an item that supports only one project association is added here and already belongs to another project, Q must prompt before moving it. Text: `Move "{title}" from "{old_project}" to "{new_project}"?`
### 4.7.5 Project defaults
Defaults configurable in Configure > Defaults: default agent, default model, fallback chain, think level. Project context defaults live in Project Context, not Configure.
### 4.7.6 Individual item removal
The Remove button on item rows **detaches** the item from the project. It does not delete the item.
Exact detach behavior:
- workflow objects with single-primary-project membership → set `project_id = null`
- direct context documents from the project's primary bucket → remove the DOC7 file ref from the primary bucket
- bucket-inherited documents → `Remove from Project` is **not shown**; user must detach bucket from Project Context tab
- linked folder rows → remove the `LinkedFolderAlias` from the project
### 4.7.7 Budget visibility and configurability
- When budget tracking is **off**: no budget pill in header, no spend tracking, no alerts.
- When budget tracking is **on**: cost pill appears in header (unless "Show budget in project header" is off). Budget amount and period are configurable. Alert threshold is configurable.
- Budget period options: Daily, Weekly, Monthly, Quarterly. Spend counter resets at start of each period.
- "Hide" behavior: clicking cost pill in header hides it for the session. "Show budget in project header" toggle in Configure controls persistent default.
### 4.7.8 Project status semantics
- **Active**: Fully operational. Visible in sidebar, browser, project dropdown. Context auto-applies. Appears in project switcher.
- **Paused**: Visible and accessible but dormant. New chats/tasks do not default to it. Context continues to apply to existing members. Appears with "Paused" badge. See §4.1.9.
- **Archived**: Hidden from normal views (sidebar, browser defaults, project dropdown). All content preserved with associations intact. Recoverable by changing status to Active. Accessible via direct URL or browser with "include archived" filter.
### 4.7.9 Archive vs Delete
**Archive**: Sets `project.status = "archived"`. Preserves everything — primary bucket, attached buckets, membership edges, notes, chats, tasks, panels/forums, generated artifacts, linked folders. Reversible.
**Delete**: Permanently removes project record and settings. Delete flow presents two choices:
1. **Detach all content**: Clear project membership on all workflow objects. Delete linked folder alias rows. Delete primary project bucket. Delete project-bucket attachment rows. Preserve underlying shared buckets. Preserve workflow objects themselves.
2. **Delete project-owned content**: Delete notes whose `project_id` equals this project. Clear `project_id` on chats, rooms, panels, forums, tasks, automations, and artifacts. Delete primary project bucket. Delete linked folder alias rows. Delete project-bucket attachment rows. Preserve shared buckets.
Delete confirmation modal shows count summary for all content types. Copy: `Delete project "{title}"? This permanently removes the project record and its project-level settings. Choose whether project-associated content is detached or deleted. Shared attached buckets are detached, not deleted.`
---
## 5. DOC7 bucket integration in DOC20
## 5.1 Reuse rule
DOC20 reuses DOC7's existing bucket model. It does not create a second context engine. Project context is implemented as DOC7 bucket references, not a parallel system.
## 5.2 Primary project bucket
Every project has exactly one primary project bucket, auto-created at project creation time.
## 5.3 Additional attached buckets
Projects may attach existing DOC7 shared buckets for additional context. Attaching a bucket does not move or copy the bucket — it creates a project-bucket attachment record.
## 5.4 Project-scoped DOC7 behavior
When a bucket is attached to a project, that bucket enters the project's candidate set according to §4.6.4 and is subject to normal DOC7 de-duplication, health checks, cap/order rules, materialization mode, and per-run excludes.
The primary project bucket MUST always be ordered before attached buckets unless explicitly excluded.
## 5.5 Project-specific presentation of DOC7
The project's Documents tab and Project Context tab show DOC7 bucket contents in project-specific views. These views do not replace the full DOC7 bucket management surface — they provide convenient project-scoped access.
## 5.6 Assign to open now
`Assign to open now` is an explicit batch convenience command. It resolves concrete target ids from the active Q session and submits normal DOC7 bucket attach or project-context apply commands. It never creates a hidden ongoing mode.
## 5.7 Linked folders and bucket files
Linked folder aliases connect filesystem Places to projects. Files from linked folders can be browsed inline or synced to the project's primary bucket as DOC7 local-path refs. See §3.12.8 for semantics.
## 5.8 Open full DOC7 surface
Each bucket card and bucket row in DOC20 surfaces should include an "Open Full Bucket" action that navigates to the canonical DOC7 bucket management page.
---
## 6. Notes
### 5.9 Project Home empty state (R2)
If a project has 0 notes, the Home tab displays:
- Project details summary (title, description, color badge, budget if enabled)
- Prominent [Create Project Note] button
- Quick links: "Drop files into Documents" and "Start a Chat"
- If the project has documents but no notes: "This project has {N} documents. Create a note to organize your work."
---
## 6.1 Notes as a first-class page
Notes are block-based modular documents. Every note is a sequence of blocks: free-form text interspersed with structured modules (task lists, activity feeds, inline threads, configurable bars). Notes appear in the Browser as a type and are organized via the Browser's Notes scope (§3.5.8). There is no separate Notes sidebar — the browser provides all note navigation, folder organization, search, and sort.
**The Today note is the default workspace home page.** On first launch each day, Q opens (or generates) the Today note — a pinned daily workspace with configurable block sections. See §6.2B for lifecycle. Users can navigate to any other note from the browser; the Today note is just the starting point.
**Unified note model (R1.7):** To-do lists are notes with TaskList blocks. Daily briefs are notes with ActivityFeed blocks. There is no separate to-do schema or to-do surface. All notes support all block types. A "to-do list" is simply a note whose primary content is a TaskList block, filed in whatever folder the user chooses.
## 6.2 Note model
```ts
type NoteKind = "standard" | "today" // R1.7: "todo" removed (tasks are blocks), "today" added
type NoteStatus = "active" | "archived" | "deleted"
// R2: SavedView system_view_key extended
// system_view_key?: "current" | "recent" | "no_project" | "recently_deleted"
interface NoteMetadataSchema {
note_id: string
title: string
note_kind: NoteKind
status: NoteStatus
project_id: string | null
folder_id: string | null
pinned: boolean
excerpt: string
word_count: number
task_count: number // R1.7: total open tasks across all TaskList blocks
comment_count: number
comment_unread_count: number
inline_thread_count: number // R1.7: count of inline thread blocks
pending_change_count: number
last_opened_at: string // R2: for feed dormancy sweep
block_type_summary: string[] // R1.7: list of block types present, e.g. ["text","tasks","feed"]
created_at: string
updated_at: string
created_by: string
updated_by: string
}
```
## 6.2A Block architecture (R4 — updated from R1.7)
Notes are composed of an ordered sequence of blocks. The document structure is: title → metadata bar → block[0] → block[1] → ... → block[N]. Between any two blocks (and before block[0] / after block[N]), the user can type free-form text or insert a new block.
**Editable canvas with module islands (R4):** The entire note content area is `contentEditable` — all space between modules is typeable. Modules render as bordered "island" components embedded in flowing text. Editable paragraph gaps are enforced between modules. A final editable area at the bottom ensures typing below the last module. In TipTap: modules = custom NodeView extensions; ambient text = standard paragraphs.
### 6.2A.1 Block types (R4 — updated)
```ts
type NoteBlockType = "text" | "task_list" | "todo" | "activity_feed" | "inline_thread" | "bar" | "note_block" | "calendar"
interface NoteBlockBase {
block_id: string
block_type: NoteBlockType
position: number // 0-indexed order in document
collapsed: boolean
title: string | null // editable title for non-text blocks
created_at: string
updated_at: string
}
```
**Block type rename (R4):** The `"tasks"` block type has been renamed to `"todo"` for consistency with the tab type and UI labeling. `renderBlock` handles both `b.type==="tasks"` and `b.type==="todo"` for backwards compatibility. New blocks are created with `"todo"` type via `insertBlock("todo")`. The `"task_list"` type in the schema remains valid for existing data.
**Text blocks** are implicit — free-form Tiptap content between module blocks. They do not have a `NoteBlockBase` record. Text is stored in the Tiptap JSON document as standard ProseMirror nodes. Module blocks are custom Tiptap node extensions that render as structured UI within the document flow.
**Note Block (R4):** A new module type — a boxed, movable note distinct from ambient canvas text. Renders as a bordered island with drag handle, collapse/expand, and delete button.
### 6.2A.2 TaskList block
```ts
interface TaskListBlock extends NoteBlockBase {
block_type: "task_list"
title: string // editable, e.g. "To Do", "Henderson Tasks"
tasks: TaskItem[]
}
interface TaskItem {
task_id: string
text: string
done: boolean
due_date: string | null
linked_note_id: string | null
subtasks: SubTaskItem[]
created_at: string
updated_at: string
}
interface SubTaskItem {
subtask_id: string
text: string
done: boolean
}
```
**Rendering:** Collapsible module with header bar showing title + open task count. Each task: checkbox + text + optional due date + optional linked note badge + subtask count. Expandable to show subtasks with individual checkboxes, add-subtask input, @Elnor button, Link Note button. "Add task…" input at the bottom. Done section collapsible via `<details>`.
**Elnor interaction:** Elnor can add tasks as tracked changes (so user sees what was added). Elnor can check off tasks. Elnor can add subtasks. All via NoteWriteSkill operating on the Tiptap JSON.
### 6.2A.3 ActivityFeed block (R1.8 — rewritten)
An ActivityFeed block is a passive display surface configured by a ModulePreset (§6.2C). It displays items from one of two source types and updates only when viewed — no background processes, no cron jobs, no subscriptions.
```ts
interface ActivityFeedBlock extends NoteBlockBase {
block_type: "activity_feed"
title: string // editable, e.g. "Activity Brief", "Henderson Emails"
preset_id: string | null // links to a ModulePreset (§6.2C), null for inline config
source: SystemEventSource | AgentFeedSource
max_items: number // default 25. Display cap. Older items scroll off.
max_visible_height: number // px, default 280. Internal scroll beyond this.
dormancy_days: number // default 15. Feed goes dormant if note not opened for this many days. 0 = never.
last_updated_at: string // ISO timestamp of last refresh
dormant: boolean // true if expired. Shows "[Refresh now]" instead of auto-refreshing.
sections: FeedSection[] // cached display data
}
interface SystemEventSource {
source_type: "system_events"
event_filters: {
event_types: string[] // e.g. ["agent.complete", "chain.complete", "watcher.event"]
agent_id?: string // filter to specific agent
chain_id?: string // filter to specific chain
watcher_id?: string // filter to specific watcher
forum_id?: string // filter to specific forum/panel
project_id?: string // filter to specific project
}
}
interface AgentFeedSource {
source_type: "agent_instruction"
instruction: string // natural language instruction
agent_id: string // which agent processes this feed
refresh_interval_minutes: number // minimum minutes between refreshes. Default 60.
}
// R2: FeedSections are display-only groupings. For system event feeds, EC assigns events to sections
// by matching event_type against section labels. For agent feeds, the agent's structured response
// specifies which section each item belongs to via an optional "section" field in the JSON array.
interface FeedSection {
section_id: string
label: string
icon: string
collapsed: boolean
items: FeedItem[]
}
interface FeedItem {
item_id: string
text: string
time: string
accent_color: string | null
created_at: string
}
```
**Refresh lifecycle (lazy evaluation — R1.8):**
1. **On note open:** Q checks `last_updated_at` on each feed block. If the configured interval has elapsed AND `dormant` is `false`, Q fires one refresh.
2. **System event feeds:** Q reads from EC's `system/activity_log.jsonl` with the block's `event_filters` applied. Returns the last `max_items` matching events. While the note is open, new WebSocket events matching the filter append in real-time. Navigate away → display freezes. Navigate back → backfill from log. Zero agent cost.
3. **Agent feeds:** Q sends one request to EC with the feed's `instruction` and `agent_id`. Agent returns items. Items are cached in `feed_cache/{block_id}.json` (see §6.3.1). Next open, if interval hasn't elapsed, show cache. If it has, one fresh call. Zero background cost.
4. **While note is open:** If another `refresh_interval_minutes` passes, Q fires one more refresh. No batching of missed intervals — if 3 hours pass, one refresh, not three.
5. **Navigate away:** All refreshes stop. No background activity.
**Dormancy (R1.8):**
If the note has not been opened for `dormancy_days` (default 15), the feed block is marked `dormant: true`. On next note open, instead of auto-refreshing, the block displays: "Feed dormant — last updated {date}. [Refresh now]". Clicking Refresh fires one update and resets `dormant: false` and `last_updated_at`. This prevents accidental agent cost when opening old notes.
**Dormancy sweep mechanism (R2):** EC runs a daily sweep (on heartbeat at midnight or on EC restart) checking all notes' `last_opened_at` (from `NoteMetadataSchema`) against each feed block's `dormancy_days`. Notes where `last_opened_at + dormancy_days < now` get their feed blocks marked `dormant: true` in the Tiptap JSON. This means when the user opens the note, the dormancy flag is already set. Q sends a `NoteMetadataUpdateCommand` with `last_opened_at: now` on every note open to keep the timestamp current.
Setting `dormancy_days: 0` disables dormancy (the Today note uses this — it's opened daily and should always auto-refresh).
**Rendering:** Collapsible module with header bar showing title + total item count + gear config. Each section: collapsible sub-header with icon + label + count. Items: ▹ indicator + optional accent dot + text + timestamp. Internal scroll at `max_visible_height`.
**Configuration:** Gear icon on the block header opens the Module Preset picker (§6.2C) in edit mode. User can change the preset, adjust `max_items`, `dormancy_days`, `refresh_interval_minutes`, or switch source type.
**Agent feed response format (R2):** For agent feeds, EC sends the instruction to the agent with a system prompt appendix: "Respond with a JSON array of items. Each item: `{ text: string, time: string, accent_color: string | null }`. Return only the JSON array." EC parses the response, assigns `item_id` and `created_at` to each item, and stores as `FeedItem[]` in `feed_cache/{block_id}.json`. If the agent response is not parseable JSON, EC wraps the entire response text as a single FeedItem with `time` = now.
**No cron. No subscriptions. No background processes. No Mode 2.** If you need background alerts (e.g., "notify me when opposing counsel emails"), use a watcher (EC Core §15A) which triggers a notification via the notification drawer — that's independent of any note or feed block.
### 6.2A.4 InlineThread block
```ts
interface InlineThreadBlock extends NoteBlockBase {
block_type: "inline_thread"
context_quote: string | null // text that prompted the thread (from selection)
display_mode: "inline" // always "inline" for this block type
messages: ThreadMessage[]
}
interface ThreadMessage {
message_id: string
author: string
body: string
created_at: string
edited: boolean
edited_at: string | null
}
```
**Rendering:** Collapsible. Expanded: header bar ("Thread · N" + optional "re: {context_quote}"), chronological message list (flat, not nested), reply input at bottom. Collapsed: single line "[E] Elnor · N messages · time".
**Invocation (see §6.2D for full @mention spec):**
1. **@mention on a line:** User types `@Elnor {question}` on any line. Tiptap converts the line into an InlineThread block and sends the question to the agent. Agent response appears as a reply.
2. **Bubble menu → "Ask inline":** User selects text, clicks "Ask inline" in bubble menu. Creates an InlineThread block below the selection with the selected text as `context_quote`.
3. **/ask slash command:** User types `/ask` → prompt appears → type question → Enter creates InlineThread block.
**Async lifecycle (R2):** While an agent is processing an inline thread request, the reply input shows a loading indicator (spinner + "Agent thinking..."). The user can continue typing but cannot submit another @mention until the current request completes or is cancelled (via NoteAICancelCommand). On error, the thread shows the error as a system message with [Retry] and [Dismiss] actions.
**Editing:** User can edit or delete their own messages. User can delete agent messages (with confirmation). The whole thread is deletable via block delete.
**Durable storage:** InlineThread blocks are stored both in the Tiptap JSON (for rendering position) and in `comments_current.json` with `display_mode: "inline"`. This means inline threads appear in the global comments index (§8.10.2) and can be queried cross-surface.
### 6.2A.5 ConfigurableBar block
```ts
interface BarBlock extends NoteBlockBase {
block_type: "bar"
title: string // the notice/alert text
accent_color: string | null
time: string | null
elnor_ref: string | null // chat reference for Elnor configuration
}
```
**Rendering:** Single-line bar with drag handle + text + optional timestamp + "Elnor" button + dismiss X. No icons next to the text. The "Elnor" button opens a chat with a reference to this block, so the user can instruct Elnor on how to update or manage it.
**Use cases:** Email arrival notices, deadline reminders, status alerts. Elnor inserts these at a configured position (default: top of document) when trigger conditions are met. User can dismiss (removes block) or click Elnor to configure the trigger.
### 6.2A.6 Block insertion (R4 — updated from R1.8)
**+Module toolbar button:** The note editor toolbar includes a "+Module" button (grid icon + label). Click opens a **two-step dropdown picker** (R4):
- **Step 1 ("choose"):** List of available module types: Note Block, To Do, Link Existing To Do, Activity Feed (→ step 2), @{Agent} Thread, Notice Bar, Calendar.
- **Step 2 ("feed_presets"):** When "Activity Feed" is selected, shows the ModulePreset picker (§6.2C) with tabs: **system** | **agent**. Feed presets: System Activity, System Notices, Gate Approvals, Active Operations (system tab); Morning Summary, Email Watch, Deadline Tracker (agent tab).
The selected block is inserted at the current Tiptap cursor position via `editor.commands.insertContentAt(editor.state.selection.from, blockNode)`.
**"Link Existing To Do" (R4):** Instead of creating a new empty to-do block, this option opens the **universal content selector** (§6.20.30J) showing all existing to-do lists from the unified `fpTodoLists` pool (§6.21): up to 8 recent lists, "Create new list" at top, "Browse all…" at bottom (opens Notes browser with To Do scope active). Selecting a list embeds it as a to-do module that reads/writes the same shared data. Changes in the module propagate to the palette and standalone tab views.
**Each module (R4):** Drag handle, collapse/expand, delete button, title bar.
**No + inserters between blocks.** The hover-gap inserter from R1.7 is removed. The toolbar button and /slash commands are the only insertion mechanisms.
**/slash commands:** On any empty line, typing `/` triggers a command palette:
- `/todo` or `/tasks` → insert To Do block
- `/feed` → insert ActivityFeed block (opens preset picker inline)
- `/ask` → insert InlineThread block (with prompt for initial message)
- `/bar` or `/notice` → insert ConfigurableBar block
- `/table` → insert table (§6.15.15A)
- `/cal` or `/calendar` → insert Calendar block (R4)
- `/note` → insert Note Block (R4)
**Block spacing (R1.8):** Module blocks have 8px margin above and below. This provides a compact visual separation. Blocks do not auto-insert text spacers between themselves — if two modules are adjacent, they render with 16px total gap (8px bottom of first + 8px top of second). The user can press Enter between blocks to add a text line for more space if desired.
**Drag reorder:** Every non-text block has a drag handle (grip icon) in its header row. Drag to reorder blocks within the document. Text between blocks is user content and stays in place — dragging a module does not move adjacent text. If a drag results in two modules becoming adjacent (no text between them), no spacer is auto-inserted.
**Block deletion:** Every block has a delete button (X icon). Text blocks show the X on hover. Module blocks show the X in their header bar. Deletion is immediate with no confirmation (undo via standard Ctrl+Z). Deleting a feed block also deletes its cached items in `feed_cache/{block_id}.json`.
**Collapse/expand:** Every non-text block collapses to a single-line header bar showing: chevron + icon + title + summary count. Click to expand. Collapsed state persists across sessions.
**Editable titles:** TaskList, ActivityFeed, and Calendar block titles are editable. Click the title text to enter edit mode (inline input). Enter to save, Escape to cancel.
## 6.2B Today note lifecycle (R1.7)
### 6.2B.1 Purpose
The Today note is the default workspace surface. It opens automatically when Q launches. It provides a daily workspace combining tasks, agent briefings, alerts, and scratch notes in a single editable surface using the block architecture (§6.2A).
### 6.2B.2 Generation
On first open each day (or at a configurable time, default: midnight), EC generates a new Today note:
1. Create a note with `note_kind: "today"`, `title: "Today — {weekday}, {month} {day}"`, `pinned: true`.
2. Apply the user's Today template (stored in Settings or as a template note). Default template:
- TaskList block titled "To Do" (populated with carried-over tasks from yesterday — see §6.2B.3)
- ActivityFeed block configured with the "System Activity" preset (§6.2C.3), `dormancy_days: 0`
- Text block (empty — "Working Notes" area)
3. All feed blocks in the new Today note start with `dormant: false`, `last_updated_at` = now. They refresh on first view.
The Today template does not hardcode specific feed sections (e.g., "Elnor", "Deadlines", "Email"). Feed content is determined by the preset configuration. Users can customize the Today template to include any combination of presets — for example, adding a "Morning Summary" agent preset and a "Gate Approvals" system preset alongside the default "System Activity".
### 6.2B.3 Rollover
When a new Today note is generated:
1. **Unchecked tasks carry forward:** All tasks in yesterday's TaskList blocks with `done: false` are copied to today's "To Do" block. They appear as regular task items (not tracked changes — they're the user's tasks).
2. **Checked tasks stay:** Completed tasks remain in yesterday's note as a record.
3. **Yesterday's note renamed:** Title changes from "Today — ..." to "{Weekday}, {Month} {Day}" (e.g., "Tuesday, March 18").
4. **Yesterday's note moved:** Folder changes to "Daily Notes" folder (auto-created if it doesn't exist).
5. **Yesterday's note unpinned:** `pinned: false`.
### 6.2B.4 Configuration
The Today template is configurable via Settings → Workspace → Today Template. Users can:
- Add, remove, or reorder default blocks
- Configure which ActivityFeed sections appear
- Set the rollover time (default: midnight local)
- Disable Today note generation (starts with blank workspace instead)
Alternatively, users can edit a template note in the Templates folder named "Today Template" — the note's block structure becomes the template.
### 6.2B.4A Today note resolution (R2)
Q resolves the current Today note on launch via a read call:
```ts
interface TodayNoteResolveReadCall {
op: "resolve_today_note"
}
interface TodayNoteResolveResponse {
note_id: string | null // null if not yet generated today
needs_rollover: boolean // true if yesterday's note is still current
}
```
If `needs_rollover` is true, Q triggers `TodayNoteRolloverCommand` first, then opens the new note. If `note_id` is null and `needs_rollover` is false (first-ever launch), EC auto-generates from template.
Deep-link route: `/notes/today` internally resolves via this read call, then redirects to `/notes/{resolved_note_id}`.
### 6.2B.5 Navigation
The Today note is always pinned to the top of the browser results when Notes scope is active. It shows a sun icon (☀️) badge. Clicking any other note in the browser navigates to that note. Clicking "Today" in the browser (or pressing a configurable shortcut) returns to the Today note.
### 6.2B.5A Today rollover while editor is open (R2)
When `TodayNoteRolloverCommand` completes while the old Today note is open in the editor, Q shows a non-blocking toast: "Today note has rolled over. [Open new Today note]". The old note remains editable (it's now a regular note in the Daily Notes folder). The toast link navigates to the new Today note via the `/notes/today` deep-link.
## 6.2C ModulePreset schema and library (R1.8)
### 6.2C.1 Purpose
A ModulePreset is a saved configuration for an ActivityFeed block. It packages a source type, filters or agent instruction, and display preferences into a reusable unit. Pre-built presets ship with ELNOR. Users can create custom presets and reuse them across notes.
### 6.2C.2 Schema
```ts
interface ModulePreset {
preset_id: string
name: string // "System Activity", "Henderson Deadlines"
icon: string // icon key from Q design system
description: string // shown in picker
block_type: "activity_feed" // currently only feed blocks use presets
category: "system" | "agent" | "custom"
source: SystemEventSource | AgentFeedSource // from §6.2A.3
default_max_items: number // default 25
default_refresh_interval_minutes: number // default 60 for agent, 0 for system (real-time)
default_dormancy_days: number // default 15
created_by: "system" | "user"
created_at: string
}
```
Storage: `ELNOR_MEMORY/module_presets/presets_current.json` (array of `ModulePreset`).
### 6.2C.3 Pre-built system presets
These ship with ELNOR and cannot be deleted (but can be hidden in Settings):
| Preset | Source type | Event filters / instruction | Cost |
|---|---|---|---|
| System Activity | `system_events` | `["agent.complete", "chain.complete", "chain.aborted", "memory.extracted"]` | Zero |
| System Notices | `system_events` | `["agent.error", "chain.step_error", "watcher.event"]` filtered to errors/warnings | Zero |
| Gate Approvals | `system_events` | `["chain.gate_waiting", "chain.approval_needed"]` | Zero |
| Active Operations | `system_events` | `["agent.status", "chain.started", "chain.step_started"]` | Zero |
| Morning Summary | `agent_instruction` | "Summarize overnight agent activity, upcoming calendar deadlines, and flagged emails. One line per item." Default agent: Elnor. | 1 agent call/refresh |
| Email Watch | `agent_instruction` | "Check Outlook inbox for important emails per standing orders. Summarize each in one line." Default agent: Elnor. | 1 agent call/refresh |
| Deadline Tracker | `agent_instruction` | "List upcoming deadlines from Outlook calendar and case files, ordered by date." Default agent: Elnor. | 1 agent call/refresh |
### 6.2C.4 Picker UX
The +Module toolbar button opens a dropdown. When "Activity Feed" is selected, a second-level panel shows the preset picker:
**Tab row:** System | Agent | My Presets
**Preset cards:** Each card shows icon, name, description. Click to insert a feed block configured with that preset.
**My Presets tab:** Shows only user-created presets (`category: "custom"`). Empty state: "No custom presets yet. Configure a feed and click 'Save as Preset'."
**"Create Custom" link:** Opens a configuration form with:
- Feed name (text input)
- Source type (System Events | Agent — two selectable cards)
- Agent picker (dropdown of available agents — default Elnor)
- Instruction (textarea — only for agent source)
- Update every: number input + unit (min/hr/day)
- Keep last: number input + "items"
- Expires after: number input + "days" with "never" option
- Create button → inserts block and closes picker
**Preset → BlockInsert data flow (R2):** When a preset is selected in the picker, Q builds a `BlockInsertCommand` (§8.5) with:
- `block_type: "activity_feed"`
- `block_data.source` = preset's `source`
- `block_data.max_items` = preset's `default_max_items`
- `block_data.dormancy_days` = preset's `default_dormancy_days`
- `block_data.preset_id` = selected preset's `preset_id`
- `block_data.last_updated_at` = now
- `block_data.dormant` = false
- `block_data.max_visible_height` = 280 (default)
- `block_data.sections` = [] (populated on first refresh)
For TaskList blocks, no preset is used — Q builds `BlockInsertCommand` with `block_type: "task_list"` and empty `tasks[]`.
**Save as Preset:** Any configured feed block's gear menu includes "Save as Preset" which saves the current configuration to `presets_current.json` with `category: "custom"`.
### 6.2C.5 Feed lifecycle rules (R1.8)
| Event | Effect on feed blocks |
|---|---|
| **Delete block** | Feed cache deleted at `feed_cache/{block_id}.json`. No other cleanup needed — no subscriptions to cancel. |
| **Delete note** | EC deletes entire `notes/{note_id}/` directory. Feed caches go with it. |
| **Note not opened for `dormancy_days`** | All feed blocks in that note marked `dormant: true`. Next open shows "Feed dormant" message. |
| **User clicks Refresh on dormant feed** | One refresh fires. `dormant` reset to `false`. `last_updated_at` updated. Dormancy counter resets. |
| **Today note rollover** | Yesterday's note becomes historical (no active feeds — dormancy applies normally). Today's new note gets fresh feed blocks from template with `last_updated_at` = now, `dormant: false`. |
| **Open the note** | Stale feeds auto-refresh (one call per stale feed). Non-stale feeds show cached data. Dormant feeds show "Refresh now" prompt. |
## 6.2D @mention agent invocation (R1.7)
### 6.2D.1 Invocation pattern
Typing `@` followed by an agent name (e.g., `@Elnor`) anywhere in a note triggers agent invocation. Tiptap recognizes the @mention via a custom mention extension and shows an autocomplete dropdown of available agents.
### 6.2D.2 Invocation contexts and behavior
| Context | Behavior |
|---|---|
| **On a new line:** `@Elnor check the Vivendi cite` | Converts the line into an InlineThread block (§6.2A.4). Sends the text after the @mention as the instruction. Agent response appears as a reply in the thread. |
| **Inside a TaskList item's @Elnor button** | Creates an InlineThread block indented below the task. Context includes the task text and any subtasks. |
| **In a comment reply** | Sends the comment thread to the agent via the existing per-comment ✨ Send flow (§6.15.17). Agent responds in the comment thread. |
| **In an InlineThread reply** | Sends the thread history + new message to the agent. Agent responds in the same thread. |
| **In the bubble menu** | "Ask inline" creates an InlineThread; "Ask in chat" routes to chat. Both use the selected text as context. |
### 6.2D.3 Agent dispatch (R2 — rewritten)
When @mention triggers an InlineThread, Q sends a single `InlineThreadCreateCommand` (§8.5). This is a compound command: EC creates the block AND dispatches to the agent. No separate `NoteAIRequest` is needed for the initial @mention.
**Dispatch flow:**
1. Q sends `InlineThreadCreateCommand` with `agent_id`, `initial_message`, `context_quote`, `position`.
2. EC creates the InlineThread block in the Tiptap JSON with the user's message as the first `ThreadMessage`.
3. EC extracts context: the surrounding 2-3 paragraphs (or full task/thread if invoked from those contexts) + the `initial_message` as the instruction.
4. EC dispatches to the agent via the standard Gateway path (DOC11). The `source_context` includes `{ source_type: "inline_thread", note_id, block_id }`.
5. Agent processes and returns a text response.
6. EC appends the agent's response as a new `ThreadMessage` (with `author: agent_id`) to the block's `messages[]` array via an internal `BlockUpdateCommand`.
7. Q receives the updated note revision and re-renders the thread with the new message.
**For subsequent replies:** `InlineThreadReplyCommand` with `mention_agent_id` follows the same dispatch pattern — EC saves the reply, dispatches to the agent, and appends the agent's response as another `ThreadMessage`.
**Error handling:** If the agent errors, EC appends a system message to the thread: `{ author: "system", body: "Agent error: {message}. [Retry]" }`. The user can click Retry to re-dispatch.
### 6.2D.4 Agent selector
The @mention autocomplete dropdown shows: configured agents (from SOUL.md agent list), with the default Note Advisor (§6.15.8) at the top. The bubble menu agent selector dot shows the currently selected agent and is clickable to change.
## 6.3 Note storage and revisions
### 6.3.1 Durable file layout
Use the following storage layout under `ELNOR_MEMORY/notes/`:
```text
ELNOR_MEMORY/notes/
notes_current.json
notes_events.jsonl
by_id/
<note_id>/
body_current.json
revisions.jsonl
comments_current.json
comments_events.jsonl
tracked_changes_current.json
tracked_changes_events.jsonl
links_current.json
exports.jsonl
feed_cache/ # R1.8: cached feed items per block
<block_id>.json # FeedItem[] cached from last refresh
```
`notes_current.json` is the materialized current-note registry (array of `NoteMetadataSchema`).
`notes_events.jsonl` is the append-only registry-level event log for create / metadata update / archive / delete.
Per-note current files are atomic JSON snapshots. Per-note event files are append-only JSONL.
### 6.3.2 Current body snapshot schema
```ts
interface NoteBodyCurrentSchema {
note_id: string
current_revision_id: string
body_tiptap_json: TiptapJSONContent
plain_text: string
excerpt: string
word_count: number
updated_at: string
updated_by: string
base_import: { format: "docx" | "markdown"; filename: string } | null
}
```
### 6.3.3 Autosave policy
Autosave rules:
- debounce after content edit: **1200ms**
- max unsaved interval while typing continuously: **15s**
- flush triggers (bypass debounce, save immediately):
- editor blur (clicking another note, clicking outside the editor)
- route change away from note (navigating to any other page)
- explicit close (closing the note or the app)
- before applying agent edit result
- switching to a different note in the note list
- browser/app losing focus (window blur)
- title edits and body edits share the same autosave pipeline
- one autosave flush creates one `NoteRevisionEvent`
- a keystroke stream MUST NOT create per-keystroke revisions
**Rapid switching behavior:** If a user types in note A, clicks note B, types in note B, clicks back to note A — each departure triggers an immediate flush. Note A's state is saved on departure to B; note B's state is saved on departure back to A. No data loss occurs regardless of switching speed. The debounce timer is cancelled and replaced by the immediate flush on each departure.
### 6.3.4 Revision source and coalescing
```ts
type NoteRevisionKind =
| "autosave"
| "manual_save"
| "ai_apply"
| "tracked_change_accept"
| "tracked_change_reject"
| "restore"
| "metadata_update"
| "import"
```
Autosave coalescing rule:
- edits by the same actor within one pending debounce window collapse into one autosave revision;
- once flushed, subsequent edits create a new revision on the next flush;
- agent-applied changes are never merged into a user autosave revision.
### 6.3.5 Conflict guard
All note write commands MUST include `base_revision_id`.
If the submitted `base_revision_id` does not match the current note body snapshot revision id:
1. Q flushes local unsaved editor state if any;
2. EC attempts position remapping for comment anchors and tracked changes where applicable;
3. if remap is safe, apply on latest body and append revision;
4. if remap is unsafe, reject with `NOTE_REVISION_CONFLICT`.
### 6.3.5A Agent edit while user is typing
Agents do NOT use the autosave pipeline. Agent edits go through EC commands (`NoteAIRequestCommand`) and create revisions with kind `ai_apply`. The autosave pipeline is exclusively for the user's local Tiptap editor state.
When an agent edit arrives while the user has unsaved local changes:
1. EC receives the agent's `NoteAIRequestCommand` with a `base_revision_id`
2. EC signals Q: "agent edit incoming for note {id}"
3. Q immediately flushes the user's unsaved editor state (cancels debounce, saves as `autosave` revision)
4. EC applies the agent's changes on top of the latest revision (which now includes the user's just-flushed state)
5. EC creates a new revision with kind `ai_apply`, containing the agent's tracked changes
6. Q receives the update event and refreshes the Tiptap editor state — the user sees the agent's tracked changes appear inline (red/green marks) without losing any of their own work
7. The user's cursor position is preserved if possible; if the agent's changes overlap the cursor position, cursor moves to the nearest safe position
This means:
- The user never loses work — their edits are flushed before the agent's changes apply
- The agent's changes always appear as tracked changes, never as silent overwrites
- The user sees a toast: "{Agent} made changes to this note" with the pending change count updating in the toolbar
- If the user is in the middle of typing a word, the flush captures the partial word — it's not lost
If the user and agent are both editing rapidly (unlikely but possible), EC serializes all writes through the `base_revision_id` chain. Each write builds on the previous. No parallel overwrites.
### 6.3.6 Version history
Version History shows: revision timestamp, source, kind, optional summary, preview, restore action.
Restoring a version creates a **new** revision with kind `restore`. It does not delete later revisions.
### 6.3.7 Note autosave state machine
```ts
type NoteAutosaveState = "clean" | "dirty" | "saving" | "error"
```
Transitions:
- `clean -> dirty` on local edit
- `dirty -> saving` when debounce timer fires or flush trigger occurs
- `saving -> clean` on successful save
- `saving -> error` on failed save
- `error -> saving` on user retry or next successful autosave trigger
Toolbar indicator text: `dirty` → "Unsaved"; `saving` → "Saving…"; `clean` → "Saved {relative time}"; `error` → "Save failed"
## 6.4 Notes page layout
**R2 clarification:** The Notes page renders the note editor in the main content area. Note hierarchy, folder navigation, search, and sort are handled by the Browser's Notes scope (§3.5.8). There is no separate NoteListPane — the browser column provides all note organization when open. When the browser is closed, the Notes page shows the currently selected note (or the Today note) without a left sidebar. The note list is the browser.
Three-pane layout:
```text
┌──────────────┬───────────────────────────────────────┬─────────────────┐
│ Note list │ Note editor │ Comment rail │
│ 240px │ flex:1 │ 260px │
│ [New][▤] │ Title │ Open │
│ Search │ Toolbar: format | AI | Review | ... │ Resolved │
│ rows… │ body… │ Orphaned │
└──────────────┴───────────────────────────────────────┴─────────────────┘
```
Note list: 240px, collapsible to 28px sidebar. Collapse button (SplitV icon) in header next to "New."
Comment rail: 260px, collapsible via toolbar Comments toggle button.
## 6.5 Note editor
### 6.5.1 Base editor
Tiptap (open-source) with ProseMirror. No paid Tiptap Pro features.
Supported formatting: paragraphs, headings (h1-h3), bold, italic, underline, bullet lists, numbered lists, task lists (checkboxes), links, code blocks, blockquotes, horizontal rules, keyboard shortcuts for common formatting.
### 6.5.2 Note title
The note title is editable inline at the top of the editor. fontSize 21, fontWeight 700. Title edits are part of the autosave pipeline.
### 6.5.3 Editor toolbar
Layout: flex row, wrap, gap 1, padding "4px 10px", bg bgPanelAlt, borderBottom border.
Groups separated by Sep (1px vertical divider, 18px tall, borderLight):
- **GROUP 1** — Block type: `<select>` (Paragraph, H1, H2, H3)
- **GROUP 2** — Inline format: B, I, U buttons (26x24px each)
- **GROUP 3** — Lists: Bullet, Numbered, Task
- **GROUP 4** — Insert: Link, Code, Rule
- **GROUP 5** — History: Undo, Redo
- **GROUP 6** — DOC20: Comments (toggle, badge when closed), AI, Import, Export, Version History
- **GROUP 7** — Review dropdown (see §6.10.4)
- **RIGHT SIDE** — flex:1 spacer → agent editing indicator → save status
### 6.5.4 Editor chrome
Below the title: project color dot + project name (if project-scoped) + modified time + note kind badge.
## 6.6 Comments
### 6.6.1 Comment architecture
The comment rail is a collapsible right-side panel anchored to the note editor. Toggle via toolbar button. When collapsed, a badge on the toggle button shows the count of open comments. Default state: collapsed for new notes, expanded if note has open comments.
### 6.6.2 Comment model
Comments are stored as current-state rows plus append-only events.
```ts
type NoteCommentStatus = "open" | "resolved" | "deleted" | "orphaned"
// R1.6: CommentAnchor discriminated union (replaces CommentAnchor)
type CommentAnchor =
| TextAnchor
| PageAnchor
| LineAnchor
| UnanchoredAnchor
interface TextAnchor {
anchor_type: "text"
anchor_id: string
block_id: string | null
from: number
to: number
quote_text: string
context_before: string
context_after: string
anchor_status: "active" | "orphaned" | "approximate"
mapped_at: string
}
interface PageAnchor {
anchor_type: "page"
anchor_id: string
page_number: number
rect?: { x_pct: number; y_pct: number; w_pct: number; h_pct: number } // R2: normalized 0.0-1.0 coordinates. Frontend multiplies by current bounding box dimensions.
quote_text: string
anchor_status: "active" | "orphaned" | "approximate"
mapped_at: string
}
interface LineAnchor {
anchor_type: "line"
anchor_id: string
line_start: number
line_end: number
quote_text: string
anchor_status: "active" | "orphaned" | "approximate"
mapped_at: string
}
interface UnanchoredAnchor {
anchor_type: "unanchored"
anchor_id: string
anchor_status: "active"
mapped_at: string
}
interface NoteCommentCurrentSchema {
comment_id: string
note_id: string
thread_root_id: string
parent_comment_id: string | null
anchor: CommentAnchor // R1.6: always present, use UnanchoredAnchor for general comments
body: string
status: NoteCommentStatus
author: string
created_at: string
updated_at: string | null
edited: boolean
display_mode: "rail" | "inline" // R2: "inline" for InlineThread blocks
thread_block_id: string | null // R2: links to InlineThread block when display_mode = "inline"
}
```
### 6.6.3 Threading
Comments support one level of threaded replies.
Rules:
- top-level comments have `parent_comment_id = null`
- replies have `parent_comment_id = top_level_comment_id`
- replies inherit the thread root and the anchor of the top-level comment
- replies do not define independent anchors
**Reply flow**: Clicking "Reply" on any open comment expands a textarea input below the comment's replies. Auto-focuses. Enter (without Shift) submits. Shift+Enter creates newline. "Reply" and "Cancel" buttons. New reply appears immediately with author "You", user color, "just now" timestamp.
### 6.6.4 Comment card layout
Each comment card shows:
- author avatar (user or agent icon, author-specific color)
- author name (fontSize 11.5, fontWeight 650, author color)
- relative timestamp (fontSize 9.5)
- edited indicator if modified
Body: fontSize 12, lineHeight 1.5.
Action row (fontSize 10.5): Reply, Resolve, Edit (own only), Delete (own only, error color at 70% opacity).
**Edit flow**: "Edit" converts body to editable textarea with Save/Cancel. Enter saves. Sets `edited: true`.
**Resolve flow**: "Resolve" moves comment + replies to "Resolved" section at 60% opacity with muted borders. "Resolve" action replaced by "Reopen."
**Delete flow**: "Delete" soft-deletes (status "deleted", hidden from rail, preserved in revision log).
Replies: borderLeft 3px solid **reply author's color** at 40%. Replies have same card structure minus Resolve action.
### 6.6.5 Comment grouping
The rail groups by top-level thread then by status:
- **Open** (expanded by default)
- **Resolved** (collapsed by default)
- **Orphaned** (collapsed, shown only when orphaned comments exist)
Ordering: anchored threads sort by current `anchor.from`. Unanchored comments sort after anchored threads by `created_at desc`. Replies always render under parent thread.
### 6.6.6 Adding comments
**Highlight-to-comment flow** (primary):
1. User selects text in editor.
2. **Floating bubble menu** appears above selection:
- **Comment** button (primary, blue bg, white text) — always first
- Separator
- Agent selector dropdown (see §6.15.6)
- **Ask {Agent}** with Spark icon
- **Rewrite**, **Expand**, **Shorten**
3. Click "Comment" → bubble disappears → new comment input appears at top of rail with:
- selected text as italic anchor quote (truncated 80 chars)
- textarea (auto-focuses)
- "Add Comment" + "Cancel"
4. Submit → comment appears in rail with anchor, author "You", user color.
**General (unanchored) comments**: "Add general comment…" input at bottom of rail.
**Agent-authored comments**: Via NoteWriteSkill `add_comment` operation.
### 6.6.7 Anchor visualization
Text with open comments shows subtle highlight (accentBtn at 8% opacity). "💬{count}" indicator after highlighted text, clickable to select first open comment.
**Click-to-navigate between comments and editor:**
- Clicking comment card in rail highlights anchor text: author color at 18% bg, 1.5px inset box-shadow in author color at 40%.
- Clicking different comment moves highlight. Clicking same comment deselects.
- Active comment card: border in author color at 60%, 6% bg tint.
- Resolved comment anchors: no highlight.
### 6.6.7A Comment anchor remapping
**R2 anchor type dispatch:** The remap function receives a `CommentAnchor` union. Dispatch by `anchor_type`:
- `text`: remap `from`/`to` positions using ProseMirror `Mapping`. Update `quote_text` from new positions. Score match quality — if < 0.7, mark `orphaned`; if 0.7–0.99, mark `shifted`; if 1.0, keep `active`.
- `page`: no remap needed (page numbers are stable within the same document version).
- `line`: remap `line_start`/`line_end` using line-level delta. If lines deleted, mark `orphaned`.
- `unanchored`: no remap needed.
Tiptap/ProseMirror integration MUST remap comment anchors across editor transactions.
```ts
function remapCommentAnchor(anchor: CommentAnchor, trMapping: Mapping, doc: ProseMirrorNode): CommentAnchor {
const nextFrom = trMapping.map(anchor.from)
const nextTo = trMapping.map(anchor.to)
if (nextFrom < nextTo) {
return { ...anchor, from: nextFrom, to: nextTo, anchor_status: "active", mapped_at: new Date().toISOString() }
}
const fallback = findQuoteWithContext(doc, anchor.quote_text, anchor.context_before, anchor.context_after)
if (fallback) {
return { ...anchor, from: fallback.from, to: fallback.to, anchor_status: "active", mapped_at: new Date().toISOString() }
}
return { ...anchor, anchor_status: "orphaned", mapped_at: new Date().toISOString() }
}
```
If a comment becomes orphaned: rail moves it to "Orphaned" section; editor shows no highlight; thread remains editable and resolvable.
`findQuoteWithContext` search order: (1) original `block_id` if still exists; (2) full note body for exact `quote_text` match; (3) best `context_before/after` match. Multiple candidates: choose closest to prior `from` position.
### 6.6.8 Author color coding
- **User**: #31588c (accentBtn)
- **Elnor**: #a1a7aa (agentAv)
- **Scout**: #5B5F97
- **Reviewer**: #7C3AED (default, per agent config)
- **Additional agents**: from AVATAR_COLOR_PRESETS palette by agent index.
Author colors used for: comment card left border, author name, avatar bg, active highlight in editor, reply left border (reply author's color, not parent's).
### 6.6.8A Anchor status visual treatment (R2)
| anchor_status | Visual treatment |
|---|---|
| `active` | Yellow highlight on anchored text. Comment card shows normally. |
| `shifted` | Yellow highlight at remapped position. Comment card shows ⚠️ "Anchor may have shifted" with original quote_text for comparison. Triggered when remap fuzzy-match scores > 0.7 but < 1.0. |
| `approximate` | Dashed yellow highlight at best-effort position. Comment card shows ⚠️ "Position approximate" (lossy migration). |
| `orphaned` | No highlight. Comment card shows ❌ "Original text was deleted" + quote_text in strikethrough + [Re-anchor] button. |
| `unanchored` | No highlight. Comment card shows in "General Comments" group at document end. |
### 6.6.9 Orphaned comment display (R1.6)
When `anchor_status` is `"orphaned"`:
- Warning label: amber badge "Anchor lost" above the comment body
- Quote text renders with strikethrough and reduced opacity (0.5)
- Action row: **Re-anchor** (opens note in selection mode — user selects new text, comment re-anchors to new position), **Resolve**, **Delete**
- Re-anchor updates `anchor_status` to `"active"` and sets new positional fields
**Orphan toast:** When a save creates orphaned comments (e.g., user deletes the anchored text), Q shows: "1 comment lost its anchor. [View in comments]"
**Undo-restores-anchor:** If the user undoes the edit that caused the orphan (within the standard undo stack), the anchor is automatically restored to `"active"`.
### 6.6.9A Block-local comment anchors (R2)
Comment anchor positions (`from`/`to`) are block-local when `block_id` is present. Block reorder does not invalidate anchors — the positions are relative to the block's own text, not the document. When `block_id` is absent (legacy notes without blocks), positions are document-global and subject to remapping on structural changes (§6.6.7A).
### 6.6.10 Comment rail empty states (R1.6)
- **Notes:** "No comments yet. Select text and click Comment, or add a general comment below."
- **Document Viewer:** "No comments yet. Select text and click Comment to annotate."
- For non-text-selectable formats (per RendererCapabilities §6.16.5A): "No comments yet. Add a general comment below."
- The "Add general comment" input area is always visible at the bottom of the rail regardless of comment count.
## 6.7 Elnor / AI integration for notes
### 6.7.1 Principle
Notes are AI-aware. Agents can read, write, comment on, and manage notes through structured skill operations. The user controls which agent handles each request and can accept/reject all proposed changes.
### 6.7.2 Selection-level AI actions
Selection-level actions in the bubble menu:
- Comment
- Ask {Agent}
- Rewrite
- Expand
- Shorten
- Convert to bullets
- Convert to checklist
- Replace selection
- Insert below
The bubble menu shows Comment + separator + agent selector + Ask {Agent} + Rewrite + Expand + Shorten. Additional actions (Convert to bullets, etc.) available in toolbar AI dropdown.
### 6.7.3 Whole-note AI actions
Whole-note actions in toolbar AI menu:
- Summarize note
- Update my to-do note
- Extract action items
- Clean up formatting
- Draft from outline
- Convert comments into proposed edits
### 6.7.4 Apply model
All AI-proposed text changes to existing content go through tracked changes. Users accept or reject. New content at empty positions may be inserted directly. Advisory responses appear as comments or chat messages. See §6.15.4 for full disposition rules.
### 6.7.5 Ownership and execution
Agents interact with notes through NoteReadSkill and NoteWriteSkill (registered in DOC3). Intent routing through DOC10. Context injection through DOC15 CIL. All write operations through EC.
## 6.8 Todo notes
### 6.8.1 Note kind
R1.7: The `note_kind = "todo"` concept is removed. Any note can contain TaskList blocks (§6.2A.2). Notes with TaskList blocks show a "☐ N tasks" indicator in the browser. The `todo` BrowserFilterKey now matches notes that contain TaskList blocks with open items.
### 6.8.2 Cross-surface usefulness
Todo note items can be surfaced in the Home page action items section and in the project activity view. Agents can add, toggle, remove, and reorder todo items via `manage_task_items` skill operation.
## 6.9 Export
### 6.9.1 Export formats
- **Markdown** — native, no conversion needed.
- **DOCX** — via Pandoc server-side.
- **PDF** — via Pandoc server-side.
### 6.9.2 Export pipeline
1. Q submits `NoteExportCommand` with format and save_mode.
2. EC prepares intermediate markdown/html.
3. OpenClaw runs Pandoc for DOCX/PDF as required.
4. EC appends export metadata to `exports.jsonl`.
5. Q either downloads the file or saves to chosen disk path.
### 6.9.3 Export ownership
EC owns the export pipeline. Q triggers and receives the result.
### 6.9.4 Save to disk
"Save to disk…" opens a native Finder chooser via OpenClaw for the user to select destination. Default filename: `{note_title}.{format}`.
### 6.9.5 Export actions summary
Export actions appear in: editor toolbar Export button dropdown, note list right-click menu, browser right-click menu for note items.
## 6.10 Tracked changes / redlining
### 6.10.1 Purpose
When Elnor or another agent proposes edits to existing note text, changes render as tracked modifications that the user can accept or reject individually or in bulk.
### 6.10.2 Visual treatment
- **Deletions**: strikethrough text in **author-specific** deletion color (§6.10.7). opacity 0.7.
- **Insertions**: underlined text in **author-specific** insertion color (§6.10.7). fontWeight 500.
- Inline action buttons: 15x15px, borderRadius 3. Accept (border author.ins+"40", bg author.ins+"10") and Reject (border author.del+"30", bg author.del+"08").
- Author name: fontSize 8.5, italic, author insertion color.
- **Right-clicking** tracked change text opens context menu (§6.10.8).
### 6.10.3 Change-set schema
Tracked changes are stored as **change sets**. A replacement is a grouped deletion span + insertion span sharing one `change_set_id`.
```ts
type TrackedChangeSetStatus = "pending" | "accepted" | "rejected"
type TrackedChangeOpType = "delete" | "insert"
interface TrackedChangeOpSchema {
op_id: string
op_type: TrackedChangeOpType
from: number
to: number
text: string
order_index: number
}
interface TrackedChangeSetSchema {
change_set_id: string
note_id: string
author: string
created_at: string
status: TrackedChangeSetStatus
ops: TrackedChangeOpSchema[]
}
```
Rendering: deletion ops = struck-through; insertion ops = underlined; replacement = one deletion op + one insertion op with same `change_set_id`.
### 6.10.4 Review dropdown
The toolbar contains a **Review** dropdown:
Button: "Review" + Edit icon + pending count badge + ChevD. Tracking on: green tint (green+"08" bg, green text, green+"50" border). Tracking off: normal textSec.
Dropdown:
1. **Track manual edits** — checkbox toggle for user-authored manual tracking only. Default off.
2. Separator.
3. **Accept All Changes ({N})** — green text, Check icon.
4. **Reject All Changes ({N})** — error red text, X icon.
Important: **agent edits to existing text always create tracked changes regardless of the "Track manual edits" toggle.** The toggle affects only whether manual user edits are converted into tracked changes.
### 6.10.5 Revision integration
- creating a tracked change set stores marked body + tracked-changes event (does NOT rewrite final body text);
- accepting: removes deletion text, keeps insertion text, strips marks, appends revision kind `tracked_change_accept`;
- rejecting: restores deletion text, removes insertion text, strips marks, appends revision kind `tracked_change_reject`.
Every accept/reject is individually undoable.
### 6.10.5A Accept / reject pseudocode
```ts
function acceptChangeSet(doc: ProseMirrorNode, changeSet: TrackedChangeSetSchema): ProseMirrorNode {
// Apply ops from highest position to lowest to avoid offset drift.
// 1. delete every span marked by a delete op
// 2. keep every span marked by an insert op
// 3. strip tracked-change marks from kept insert spans
return transformDoc(doc, changeSet, "accept")
}
function rejectChangeSet(doc: ProseMirrorNode, changeSet: TrackedChangeSetSchema): ProseMirrorNode {
// Apply ops from highest position to lowest.
// 1. keep every span marked by a delete op
// 2. strip tracked-change marks from kept delete spans
// 3. remove every insert span entirely
return transformDoc(doc, changeSet, "reject")
}
```
### 6.10.6 Agent write-back rule
When an agent modifies existing text: EC MUST create a tracked change set.
Rules:
- existing text modified → tracked change set
- new text inserted at a truly empty insertion point → direct insertion permitted
- advisory content → comment
- whole-note replies in chat → no note mutation unless user explicitly applies
### 6.10.7 Author-colored changes
| Author | Deletion color | Insertion color | Notes |
|---|---|---|---|
| Elnor | #B04040 (red) | #2E8B57 (green) | Primary — familiar red/green |
| Scout | #8B5E00 (amber) | #2563EB (blue) | Distinct from Elnor |
| Reviewer | #7C3AED (purple) | #7C3AED (purple) | Same color for both |
| Other agents | Assigned from palette by agent index | | Palette: teal, orange, pink |
### 6.10.8 Right-click context menu on tracked changes
Right-clicking tracked change text opens context menu:
1. **Accept This Change** — green, Check icon
2. **Reject This Change** — error red, X icon
3. Separator
4. **Accept All ({N})** — textSec
5. **Reject All ({N})** — textSec
### 6.10.8A Tracked-change flush-first rule (R2)
Agent tracked changes are always computed against the latest flushed revision (`base_revision_id`). When EC receives an agent tracked-change submission and the user has unflushed edits (detected by comparing the agent's `base_revision_id` against the latest flushed revision), EC auto-flushes the user's pending edits as a new revision first, then applies the agent's tracked changes against that new revision. The user sees their edits committed, then the agent's tracked changes appear on top.
If the auto-flush fails (concurrent write), EC retries once. If the retry also fails, EC returns `NOTE_WRITE_QUEUE_BUSY` to the agent. The agent's changes are NOT silently dropped.
**AI diffing approach (R2):** EC computes tracked changes by comparing the agent's output ProseMirror tree against the current flushed body using tree-level node comparison. Implementation should use `prosemirror-changeset` or equivalent library for structural diffing. String-level diffing is NOT acceptable — it will corrupt ProseMirror node boundaries.
### 6.10.9 Tracked-change overlap policy (R1.6)
**Overlap detection:** When an agent submits a tracked change whose character range overlaps with an existing pending tracked change, EC returns `PENDING_CHANGE_OVERLAP` instead of applying the change.
**Resolution dialog:** Q displays a dialog with three options:
1. **Replace existing** — reject the existing pending change, apply the new one
2. **Keep existing** — discard the incoming change, notify the agent
3. **Apply both sequentially** — accept the existing change first, then apply the new one on the updated text
The dialog shows both changes side-by-side with their author labels. If the user dismisses the dialog without choosing, the incoming change is held in a pending queue (max 3) until the overlap is resolved.
## 6.11 Import
### 6.11.1 Supported import formats
- **DOCX** → HTML via mammoth.js → Tiptap JSON
- **Markdown** → Tiptap JSON (native)
### 6.11.2 Import flow
1. User clicks Import in toolbar or uses File > Import.
2. File picker opens (or user drags file onto editor).
3. EC converts to Tiptap JSON.
4. New note created with imported content. Revision kind `import`.
### 6.11.3 Browser import
Dragging a .docx or .md file from the Browser (or Finder) onto the note editor triggers import into the current note or creates a new note.
### 6.11.4 Import limitations
Complex formatting (images, tables, embedded objects) may be simplified during import. User is warned if significant formatting loss is detected.
## 6.12 Sharing and remote access
### 6.12.1 Tailscale access
When Tailscale is configured, notes are accessible from other devices on the Tailscale network via the Q Dashboard web interface.
### 6.12.2 Export-and-send
User can export a note and attach it to an email or chat message.
### 6.12.3 OneDrive auto-sync (Phase 2)
Automatic sync of notes to OneDrive as markdown files. Deferred.
### 6.12.4 Shareable read-only links (Phase 2)
Generate a shareable URL for a note that can be viewed without authentication on the local network. Deferred.
## 6.13 Cross-note linking and backlinks
### 6.13.1 Link syntax
Typing `[[` in the editor opens an autocomplete popup listing notes by title. Selecting a note inserts a `[[Note Title]]` link.
### 6.13.2 Browser drag-to-link
Dragging any browser item onto the note editor creates a `[[link]]` to that item at the drop position.
### 6.13.3 Link rendering
Links render as clickable inline elements with a subtle icon + title. Clicking navigates to the linked item.
### 6.13.4 Backlinks
Each note has a "Backlinks" section (collapsed by default) at the bottom showing items that link to the current note.
## 6.14 Agent awareness and wiring
### 6.14.1 Capability registration (DOC3)
Register: **NoteReadSkill**, **NoteWriteSkill**, **BrowserQuerySkill** with schemas from §6.15.5.
### 6.14.2 Intent routing (DOC10)
DOC10's LocalIntentIndex must index skill names/aliases for notes and browser. Routes through `gateway_interactive_chat` with capability hints.
### 6.14.3 Context injection (DOC15 CIL)
`note_reference` signal type. Notes scored by project scope, title match, recent edit recency. Note content in context injection attributed as `[From note: {title}]`.
### 6.14.4 End-to-end write-back flow
1. User instructs Elnor in chat.
2. DOC10 classifies, identifies NoteWriteSkill, routes through Gateway.
3. Elnor reads note via NoteReadSkill.
4. Elnor generates changes.
5. Changes submitted as structured edit payload through EC.
6. EC records revision. Q renders.
### 6.14.5 Passive AI features
- Summarize note, cross-reference, monitor-and-alert, extract action items.
### 6.14.6 Live activity indicator
When an agent is working on a note: toolbar shows "Elnor editing…" with pulsing dot. Note list shows secondary line. Clears when operation completes.
## 6.15 AI Request Wiring Contract
### 6.15.0A Per-note write queue (R1.6)
EC maintains a per-note write queue keyed by `note_id`. All write commands targeting the same note are serialized:
1. Commands enter the queue in arrival order.
2. EC executes one command at a time per note.
3. If a command arrives while another is executing, it waits in the queue.
4. If the queue exceeds 5 pending commands, EC rejects the newest with `NOTE_WRITE_QUEUE_BUSY`.
5. Agent writes and user writes share the same queue — no priority lane.
This prevents race conditions between concurrent agent edits and user saves. The queue is per-note, not global — writes to different notes execute concurrently.
### 6.15.1 Purpose
Complete wiring between Notes UI, EC orchestration, and agent execution.
### 6.15.2 Request payload schema
```ts
type NoteAIActionType =
| "ask"
| "rewrite"
| "expand"
| "shorten"
| "convert_bullets"
| "convert_checklist"
| "replace_selection"
| "insert_below"
| "summarize"
| "extract_actions"
| "cleanup_formatting"
| "draft_from_outline"
| "convert_comments_to_edits"
| "update_todo_note"
| "add_comment"
interface NoteAISelectionSchema {
from: number
to: number
text: string
quote_text: string
context_before: string
context_after: string
}
interface NoteAIRequest {
command: "note_ai_request"
request_id: string
note_id: string
base_revision_id: string
selection: NoteAISelectionSchema | null
instruction: string | null
action_type: NoteAIActionType
agent_id: string
preview_required: boolean
note_context: {
title: string
project_id: string | null
note_kind: NoteKind
}
}
```
### 6.15.2A NoteReviewRequest schema (R1.6)
`NoteReviewRequest` is for drawer-initiated "Send to Agent" operations. `NoteAIRequest` (§6.15.2) is for bubble-menu and inline AI actions. They share `note_id`, `base_revision_id`, `agent_id`, and `note_context`, but `NoteReviewRequest` adds drawer-specific fields.
```ts
interface NoteReviewRequest {
command: "note_review_request"
request_id: string
note_id: string
base_revision_id: string
agent_id: string
chat_id: string
instruction: string
scope: "full" | "comments_only" | "selected_comments"
selected_comment_ids: string[]
output_mode: "respond_in_chat" | "tracked_changes" | "revised_copy" | "respond_in_comments"
note_context: {
title: string
project_id: string | null
note_kind: NoteKind
}
}
```
### 6.15.3 Request lifecycle and UI state
```ts
type NoteAIRequestState = "queued" | "running" | "awaiting_user_preview" | "applied" | "error" | "cancelled"
```
UI rules: loading state while queued/running; allow Cancel; if `preview_required = true` show diff preview before applying; if `base_revision_id` stale and unsafe to remap, surface `NOTE_REVISION_CONFLICT`.
### 6.15.4 Routing and response handling
1. Q builds `NoteAIRequest`.
2. EC validates note existence and `base_revision_id`.
3. EC reads current note body. For selection-level: focused context window (selection + 2-3 surrounding paragraphs + title + project context). For whole-note: full body.
4. EC dispatches through Gateway/OpenClaw.
5. Model returns result.
6. EC converts to disposition:
| action_type | Default disposition | Notes (R1.6) |
|---|---|---|
| rewrite | tracked_change | Replaces selection |
| expand | tracked_change OR direct_insert | tracked_change if replacing, direct_insert if insert_below mode |
| shorten | tracked_change | Replaces selection |
| convert_bullets | tracked_change | Replaces selection |
| convert_checklist | tracked_change | Replaces selection |
| replace_selection | tracked_change | Replaces selection |
| insert_below | direct_insert | Inserts after selection position |
| ask (selection) | comment | EC may reclassify as tracked_change if agent returns explicit replacement |
| ask (whole note) | chat_only | Agent responds in chat, not in note |
| summarize | chat_only | Agent responds in chat |
| extract_actions | chat_only | Agent responds in chat |
| cleanup_formatting | tracked_change | Full-note tracked changes |
| draft_from_outline | direct_insert OR tracked_change | direct_insert for new content, tracked_change for modifications |
| convert_comments_to_edits | tracked_change | Each open comment → tracked change at anchor position |
| update_todo_note | tracked_change + direct_insert | tracked_change for modifications, direct_insert for new task items |
| add_comment | comment | Agent adds a comment at the selection anchor |
### 6.15.5 Response contract
```ts
type NoteAIResultDisposition = "tracked_change" | "direct_insert" | "comment" | "chat_only" | "error"
interface NoteAIResultEvent {
request_id: string
note_id: string
disposition: NoteAIResultDisposition
message: string | null
change_set_ids: string[]
inserted_revision_id: string | null
created_comment_ids: string[]
error_code: string | null
completed_at: string
}
```
### 6.15.6 Skill operation definitions
**NoteReadSkill:**
```ts
type NoteReadSkillOp =
| { op: "list_notes"; project_id?: string | null; kind?: NoteKind | null; include_archived?: boolean }
| { op: "read_note"; note_id: string }
| { op: "search_notes"; query: string; project_id?: string | null }
| { op: "read_comments"; note_id: string }
| { op: "read_backlinks"; note_id: string }
```
**NoteWriteSkill:**
```ts
interface EditOp {
from: number
to: number
proposed_text: string
change_type: "insertion" | "deletion" | "replacement"
}
type TaskItemOp =
| { op: "add"; block_id: string; text: string; position?: number }
| { op: "toggle"; block_id: string; item_id: string; checked: boolean }
| { op: "remove"; block_id: string; item_id: string }
| { op: "reorder"; block_id: string; item_id: string; after_item_id: string | null }
| { op: "set_due_date"; block_id: string; item_id: string; due_date: string | null }
| { op: "set_linked_note"; block_id: string; item_id: string; linked_note_id: string | null }
| { op: "add_subtask"; block_id: string; item_id: string; text: string }
| { op: "toggle_subtask"; block_id: string; item_id: string; subtask_id: string; checked: boolean }
type NoteWriteSkillOp =
| { op: "create_note"; title: string; project_id?: string | null; note_kind?: NoteKind; content?: string }
| { op: "edit_note_text"; note_id: string; base_revision_id: string; edits: EditOp[] }
| { op: "add_comment"; note_id: string; anchor?: CommentAnchor | null; body: string }
| { op: "reply_to_comment"; note_id: string; parent_comment_id: string; body: string }
| { op: "resolve_comment"; note_id: string; comment_id: string }
| { op: "reopen_comment"; note_id: string; comment_id: string }
| { op: "update_comment"; note_id: string; comment_id: string; body: string }
| { op: "delete_comment"; note_id: string; comment_id: string }
| { op: "update_note_metadata"; note_id: string; updates: { title?: string; project_id?: string | null; pinned?: boolean; note_kind?: NoteKind } }
| { op: "manage_task_items"; note_id: string; operations: TaskItemOp[] }
```
**BrowserQuerySkill:**
```ts
type BrowserQuerySkillOp = { op: "query_browser"; request: BrowserQueryRequest }
```
EC enforces tracked change vs direct insertion rule (§6.10.6). Agents do not decide this.
### 6.15.7 Content format guidance
Agents produce structured content using: paragraphs, headings (h1-h3), bold, italic, bullet lists, numbered lists, task lists, links, code blocks, blockquotes, horizontal rules. EC converts to Tiptap JSON.
### 6.15.8 Default Note Advisor agent
**Settings > Notes**: Default Note Advisor dropdown (defaults to Elnor).
**Bubble menu**: compact agent selector at top of AI actions. Shows agent color dot + name. Updates "Ask {Agent}" label dynamically. Priority: per-request selection > project default > Settings default.
**Toolbar AI dropdown**: same agent selector for whole-note actions.
### 6.15.9 Agent note access model
Agents NEVER interact with Tiptap/ProseMirror/filesystem directly. Agents call skill operations. EC handles storage + mark conversion. EC enforces tracked change vs direct insertion.
### 6.15.10 Send to Agent drawer for Notes (R1.4)
Notes gain a Send to Agent drawer identical in structure to the Document Viewer's (§6.16.8), accessed via a "Send to Agent" tab in the tabbed right panel. The tab label reads "Send to Agent" (not "Send" — avoids document-context ambiguity).
**Output mode top level:** Respond in chat vs Send with instructions (same two-tier as Document Viewer).
**Result formats for Notes** (different from Document Viewer):
- **Apply as tracked changes** (default) — agent revisions appear as tracked changes colored per-agent
- **Create revised copy** — new note with changes applied, original preserved
- **Respond in comments** — agent reads comments, replies as threaded replies, no body mutation
Agent selector, chat selector, context scope (including select-comments), instruction field, reference ID note, and per-comment send icon all work identically to the Document Viewer.
**Chat selector:** Lists open chats ordered by recency. Chat most recently associated with the note's project tagged "origin." "New chat" at bottom. For bubble menu "Ask {Agent}" (lightweight), sends to most recent project chat without prompting.
### 6.15.11 Notes tabbed right panel (R1.4)
The Notes right panel uses the same tab pattern as the Document Viewer:
```text
[ 💬 Comments (N) ] [ ✨ Send to Agent ] [ × ]
```
Both tabs retain state when switching. Width: 260px (Comments), 290px (Send to Agent).
### 6.15.12 User-colored tracked changes (R1.4 — supplements §6.10.7)
When "Track manual edits" toggle is active, the user's own edits create tracked changes in **blue** (#31588c):
| Author | Delete color | Insert color |
|---|---|---|
| You (user) | #31588c (blue strikethrough) | #31588c (blue underline) |
| Elnor | #B04040 (red strikethrough) | #2E8B57 (green underline) |
| Scout | #8B5E00 (amber strikethrough) | #2563EB (blue underline) |
| Reviewer | #7C3AED (purple) | #7C3AED (purple) |
### 6.15.13 Note list search and sort (R1.4)
**Full-text search (R1.6):** Add a toggle button "Title / Body" to the note list search bar that switches between title-only search (default) and full-text body search. Full-text search queries the `plain_text` field. Results highlight matching excerpts below the note title.
The note list sidebar gains search and sort immediately below the header:
**Search:** Compact input with search icon. Filters by title (case-insensitive substring). X to clear. Empty state: "No notes match '{query}'."
**Sort dropdown:** Modified (default), Alphabetical, Created. Pinned notes float to top.
### 6.15.14 Archive and delete notes (R2 — expanded)
**Archive:** No confirmation needed (reversible, visible with archive filter).
**Delete (note with no children, no comments, no tracked changes):** 5-second undo toast. Immediate soft-delete.
**Delete (note with comments, tracked changes, or child notes in folder):** Confirmation modal:
- Title: "Delete Note"
- Body: "'{note_title}' has {N comments}, {M tracked changes}, and {K child notes}. It will be moved to Recently Deleted for 30 days."
- Primary: [Delete Note] (danger/red)
- Secondary: [Cancel]
**Right-click context menu** on any note row:
- **Archive** — status "archived." Disappears from active list. "Archived (N)" section at bottom (collapsed, click to expand). 50% opacity with "Restore" button.
- **Delete** — inline "Delete permanently? Yes / No." Soft-delete (status "deleted," hidden, preserved in event log).
Note list footer: "{N} notes · {sort} · {N} archived."
### 6.15.15 Deep subfolders (R1.4)
**Visual indentation cap (R1.6):** Note folders display visual indentation up to depth 4. Beyond depth 4, a folder breadcrumb trail appears above the folder content with clickable segments: `Root > Folder A > Subfolder B > Sub-subfolder C > ...`. Folders beyond depth 4 are indented at depth 4 with the breadcrumb providing navigation context.
Folder tree supports **unlimited nesting depth**. The "+" hover icon appears on folders at all depths. Indentation: 14px per level.
Both **+Folder** and **+Note** buttons visible in header when folder view active:
- 📁+ creates root folder (inline input, Enter/Escape)
- 📄+ creates new note
### 6.15.15A Table support (R1.6)
Add `Table`, `TableRow`, `TableCell`, `TableHeader` to the Tiptap extension list. Tables are insertable via toolbar dropdown (Insert → Table) or `/table` slash command. Tables support column resize (drag handles), add/delete row/column, and header row toggle.
### 6.15.15B Note template creation (R1.6)
Any note can be saved as a template via context menu: "Save as Template." Templates are stored in a "Templates" folder. Creating a new note offers "Blank" or any saved template. Templates preserve block structure but clear content.
### 6.15.16 Notes toolbar additions (R1.4 — supplements §6.5.3)
New items added to the Notes editor toolbar:
| Item | Behavior |
|---|---|
| Copy (icon-only) | Copies full note plain text |
| Save As… ▾ | Export as Markdown/DOCX/PDF, separator, Save as Prompt |
| Ref (link icon + "Ref") | Copies `@[{title}](note:{id})` |
| Find (⌘F) | Inline find bar |
| Full Screen | Hides note list + right panel, max width |
| Comments (N) | Opens right panel to Comments tab |
| Markup/Clean toggle | When changes pending, toggle between marks and clean text |
| Review ▾ | Accept All / Reject All |
| Send to Agent | Opens right panel to Send to Agent tab |
**"Save as Prompt" modal UX (R1.6):** Clicking "Save as Prompt" in the Save As dropdown opens a lightweight modal:
- **Title** (pre-filled from note title)
- **Description** (optional text field)
- **Category** dropdown (populated from DOC17 category enum)
- **Tags** (optional, comma-separated or chip input)
On confirm, Q builds a `prompt_recipe.create` command (DOC17 §12.2) with `prompt_text` = current note body plain text, `saved_from_surface` = `"note"`, `source_prompt_ref.message_id` = note_id. EC routes to DOC17 command handler. Toast: "Saved as prompt: {title}." Add this modal to DOC22 Page 14.
### 6.15.17 Comment interaction wiring for Notes (R1.4)
**Per-comment ✨ Send wiring (R1.6):** The per-comment ✨ Send button on each comment card builds a `NoteReviewCommand` with `scope: "selected_comments"`, `selected_comment_ids: [comment_id]`, `output_mode: "respond_in_chat"`, and `instruction: ""` (empty — the comment body IS the instruction). EC packages the comment body, its anchor context, and the surrounding note paragraphs as the agent's input.
Same wiring as Document Viewer §6.16.14. All actions (Reply, Edit, Resolve, Reopen, Delete, Add via bubble, Add general) follow identical visual behavior, state transitions, and EC commands. The per-comment ✨ Send icon appears in the bottom action row of every comment card.
## 6.16 Document Viewer
### 6.16.1 Purpose
The Document Viewer is a universal read-and-review surface for **any viewable file type** — agent-generated artifacts, PDFs, Word documents, Markdown files, images, code files, and other content. It renders in the main content area when any viewable item is opened from the browser, chat pane, project Documents tab, or deep link.
The viewer is NOT a full editor. It is a preview + comment + AI review surface. For full editing, the user converts the content to a Note via the "To Note" toolbar button.
The viewer chrome (toolbar, title bar, tabbed right panel, metadata rail) is universal and wraps any content renderer. Only the content renderer swaps per file type.
### 6.16.2 Route
`/viewer/:id`
The `:id` parameter accepts any entity ID with viewable content. The viewer resolves the entity type and selects the appropriate renderer.
Launched from: browser double-click on any viewable item, inline artifact preview in the chat pane, project Documents > Generated subtab, BrowserQuerySkill deep link, Copy Reference link.
### 6.16.3 Layout
```text
┌──────────────────────────────────────────────────────────────────────────────┐
│ 📄 Document Title [Markdown] │
├──────────────────────────────────────────────────────────────────────────────┤
│ [📋] [Save As… ▾] | [To Note] [🔗 Ref] | [🔍] [↗] [🖨] [📌] [⛶] │
│ | [💬 3] | [👁 Markup] [✏ 3 ▾] | [🕐 v3 ▾] [✨ Send to Agent ▾] │
├─────────────────────────────────────────────┬────────────────────────────────┤
│ Document content │ [Comments] [Send to Agent] [×] │
│ (rendered by appropriate renderer) │ │
│ │ Tabbed right panel: │
│ │ Comments tab or Send tab │
├─────────────────────────────────────────────┴────────────────────────────────┤
│ ✨ Elnor in Henderson discovery · 🔵 Henderson v. DataCorp · v3 · 1d ago │
└──────────────────────────────────────────────────────────────────────────────┘
```
### 6.16.3A Document title bar (R4 — updated)
A slim bar between the toolbar and content showing:
- File icon (type-appropriate)
- Document title (read-only for artifacts/documents; editable for Notes when opened in Notes editor)
- Format badge (Markdown, PDF, DOCX, etc.)
- File size (optional)
- "Ref" button — quick copy of EC internal reference (R4)
**Title click behavior (R4):** Clicking the document title opens the file in the OS default application.
**Title right-click context menu (R4):**
| Action | Behavior |
|---|---|
| Show in Finder | Opens enclosing folder in macOS Finder |
| Open in Default App | Launches file in OS-registered default handler |
| Copy Ref | Copies EC internal reference string (`@[{title}]({type}:{id})`) |
| Copy Path | Copies full filesystem path |
| Copy File Name | Copies filename with extension |
### 6.16.4 Toolbar
Compact toolbar with responsive collapse:
| Group | Items | Notes |
|---|---|---|
| Copy/Export | Copy (icon-only), Save As… ▾ dropdown | Dropdown: Save As… (format), Export as DOCX, Export as PDF, Export as HTML, separator, Save as Prompt |
| Convert | To Note (icon + label) | Creates editable Note fork |
| Reference | Ref (link icon + "Ref" label) | Copies `@[{title}]({type}:{id})` — visually distinct from Copy via Link icon |
| Navigation | Find (⌘F), Open External, Print, Pin, Full Screen | Standard tools, all wired with toasts/toggles |
| Clip | Clip (scissors icon + "Clip" label) | Clips selected text or full document to active session clips note. Same behavior as web browser Clip (§6.19.17). (R3) |
| Comments | Comments (bubble icon + open count) | Toggles right panel to Comments tab |
| Review | Markup/Clean toggle, Review ▾ dropdown (Accept All / Reject All + pending count) | Only visible when pending changes exist |
| Version | v{N} ▾ dropdown | Version history with flush-left DItem components |
| Send | Send to Agent (accent button, right-aligned) | Opens right panel to Send to Agent tab |
**"Save as Prompt" modal UX (R1.6):** Same modal as Notes (§6.15.16): Title, Description, Category, Tags. `saved_from_surface` = `"viewer"`, `source_prompt_ref.message_id` = artifact_id. Add to DOC22 Page 15.
**"Open External" and "Print" annotation (R1.6):** These are Q-only client actions. They do not route through EC. "Open External" calls the OS default handler for the file. "Print" invokes `window.print()` on the rendered viewer content. No command payload, no EC log entry, no undo.
**"Save as Prompt"** — not "Prompt / Overlay." Clean label. Saves the document content as a DOC17 overlay/preset.
**Responsive collapse:**
- < 900px: Copy icon-only, Save as Prompt moves inside Save As, Ref icon-only
- < 700px: Accept All / Reject All collapse into Review dropdown, all nav buttons icon-only
- < 500px: overflow "More…" menu
**Naming:** All labels are agent-agnostic. Button reads "Send to Agent" not "Send to Elnor." The bubble menu reads "Ask {Agent}" where {Agent} is the currently selected agent name (dynamic).
**Toolbar Ask button simplification (R4):** Toolbar "Ask" buttons on note, web, and document surfaces have been simplified to just "Ask" (removed agent name to save horizontal space). The bubble menu retains the full "Ask {Agent}" label since it has more room.
### 6.16.5 Renderer registry
| Format | Renderer | Text selectable | Comment anchoring | AI actions |
|---|---|---|---|---|
| Markdown (.md) | MarkdownRenderer (react-markdown + remark-gfm + rehype-highlight) | Yes | Text-anchored | Full bubble menu |
| Plain text (.txt, .log, .csv) | Monospace text viewer with line numbers | Yes | Text-anchored | Full bubble menu |
| Code (.py, .ts, .js, .sh, etc.) | Syntax-highlighted code viewer (rehype-highlight) | Yes | Text-anchored | Full bubble menu |
| HTML (.html) | Sandboxed iframe or inline render | Yes (in source view) | Text-anchored (source view) | Source view only |
| JSON (.json) | Syntax-highlighted tree viewer with collapse/expand | Yes | Text-anchored | Full bubble menu |
| PDF (.pdf) | PDF.js embedded viewer | Limited | Page-level | Ask {Agent} only |
| DOCX (.docx) | mammoth.js → HTML preview (client-side) | Yes | Text-anchored | Full bubble menu |
| Images (.png, .jpg, .svg) | Native img/svg render with zoom/pan | No | x,y coordinate (future) | Ask {Agent} only |
| React/JSX | Sandboxed render | No (rendered output) | Unanchored only | Source view: full bubble menu |
| Other / unknown | "No preview available" + Download + Open External | No | Unanchored only | Ask {Agent} only |
For formats with both rendered and source views (HTML, React/JSX), a toggle switches between "Preview" and "Source." AI actions and text-anchored comments are available in source view.
### 6.16.5A RendererCapabilities (R1.6)
```ts
interface RendererCapabilities {
text_selectable: boolean
supports_tracked_changes: boolean
supports_inline_comments: boolean
supports_page_anchors: boolean
supports_line_anchors: boolean
supports_find: boolean
supports_copy: boolean
supports_print: boolean
editable: boolean
}
const RENDERER_CAPABILITIES: Record<string, RendererCapabilities> = {
tiptap: { text_selectable: true, supports_tracked_changes: true, supports_inline_comments: true, supports_page_anchors: false, supports_line_anchors: false, supports_find: true, supports_copy: true, supports_print: true, editable: true },
pdf: { text_selectable: false, supports_tracked_changes: false, supports_inline_comments: false, supports_page_anchors: true, supports_line_anchors: false, supports_find: false, supports_copy: false, supports_print: true, editable: false },
markdown: { text_selectable: true, supports_tracked_changes: false, supports_inline_comments: false, supports_page_anchors: false, supports_line_anchors: true, supports_find: true, supports_copy: true, supports_print: true, editable: false },
code: { text_selectable: true, supports_tracked_changes: false, supports_inline_comments: false, supports_page_anchors: false, supports_line_anchors: true, supports_find: true, supports_copy: true, supports_print: true, editable: false },
image: { text_selectable: false, supports_tracked_changes: false, supports_inline_comments: false, supports_page_anchors: false, supports_line_anchors: false, supports_find: false, supports_copy: false, supports_print: true, editable: false },
docx: { text_selectable: true, supports_tracked_changes: false, supports_inline_comments: false, supports_page_anchors: false, supports_line_anchors: false, supports_find: true, supports_copy: true, supports_print: true, editable: false }, // R2: mammoth.js HTML preview
unsupported: { text_selectable: false, supports_tracked_changes: false, supports_inline_comments: false, supports_page_anchors: false, supports_line_anchors: false, supports_find: false, supports_copy: false, supports_print: false, editable: false },
}
```
**Rule:** Toolbar and bubble menu actions check the active renderer's capabilities. Unsupported actions are **hidden** (not disabled). If `supports_tracked_changes` is `false`, the Markup/Clean toggle and Review dropdown are hidden. If `text_selectable` is `false`, the bubble menu never appears.
### 6.16.5B Iframe sandbox security (R2)
HTML, JSX/TSX, and unknown-format previews render in sandboxed iframes:
```html
<iframe sandbox="allow-scripts"
srcdoc="..."
style="width:100%;height:100%">
```
Rules:
- No `allow-same-origin` (prevents access to parent page data)
- No `allow-forms` (prevents form submission to external URLs)
- Communication between iframe and parent via `postMessage` only, with origin validation
- React/JSX preview must be transpiled in a web worker, not in the main frame
- CSP: `default-src 'self'; script-src 'unsafe-inline'`
### 6.16.6 Comment system
Reuses Notes comment architecture (§6.6) with a **tabbed right panel** instead of a standalone rail:
**Tabbed right panel:**
```text
[ 💬 Comments (3) ] [ ✨ Send to Agent ] [ × ]
```
- Two tabs: Comments and Send to Agent
- Both retain state when switching
- X closes entire panel
- Width: 260px (Comments), 300px (Send to Agent)
- Toolbar Comments button opens panel to Comments tab; Send to Agent button opens to Send tab
**Comment architecture:**
- CommentAnchor with quote_text + context_before/after
- Highlight-to-comment flow: select text → bubble menu → Comment → anchored comment
- Threading: one level of replies
- Author colors: same palette as Notes (§6.6.8, §6.10.7A)
- Actions: Reply, Resolve, Reopen, Edit, Delete — all fully wired per §6.16.14
**Per-comment send icon:**
Each comment card includes ✨ Send in the **bottom action row** (alongside Reply, Resolve, Edit, Delete), right-aligned via flex spacer. Full opacity in textSec (#5E6570), turns accentBtn on hover. Click sends that single comment + anchor + ~500 chars context to the active agent in the most recent project chat. Toast confirms.
```text
Reply · Resolve · Edit · Delete ✨ Send
```
**Differences from Notes:**
- PDF comments: anchored to page + optional region description
- Image comments: unanchored only (future: region-based)
- No tracked changes on viewed documents — AI revisions produce new versions
Storage: `comments_current.json` and `comments_events.jsonl` under entity storage path, using shared `target_id` + `target_type` pattern.
### 6.16.7 AI actions on documents
For text-based content with text selection, the floating **bubble menu** appears:
```text
[ Comment ] | [ 🔵 {Agent} ▾ ] [ Ask {Agent} ] [ Rewrite ] [ Expand ] [ Shorten ] [ × ]
```
Agent name is dynamic (not hardcoded). X button dismisses bubble and clears selection. Bubble dismisses on mouseDown outside (not onClick, which races with mouseUp creation).
All actions wired:
- **Comment** → opens new-comment input in Comments tab
- **Ask {Agent}** → sends selection + context to agent in most recent project chat. Toast confirms.
- **Rewrite/Expand/Shorten** → EC creates a new version with the change applied. Viewer shows inline diff preview (red strikethrough / green underline) with per-change Accept/Reject buttons. Accept creates new version; reject discards.
### 6.16.8 Send to Agent flow
The "Send to Agent" tab in the right panel contains:
**1. Agent selector** — dropdown with avatar + name. Flush-left DItem alignment. Selection updates agent name throughout UI (bubble menu, toolbar, metadata).
**2. Send in (chat selector)** — dropdown listing all open chats ordered by recency. Origin chat tagged with "origin" badge, listed first. "New chat" at bottom. Flush-left DItem alignment.
**3. Context scope** — three radio options:
- **Full document + all comments** — higher token cost
- **Comments & referenced portions** — ~500 chars before/after each anchor. Lower tokens.
- **Select comments & references** — checkboxes appear on comment list. User picks which to include.
**4. Comments summary** — list of open comments. When "Select comments" scope active, checkboxes appear. Header: "Comments (3)" or "Comments (2 selected)".
**5. Output mode — two-tier:**
Top level:
- **Respond in chat** — opens target chat with context. Instruction optional. Button: "Open in Chat."
- **Send with instructions** — agent processes instruction. Instruction required (red asterisk). Button: "Send to {Agent}."
When "Send with instructions" selected, **Result format** sub-section:
- **Revise artifact (new version)** — original preserved in history
- **Convert to note with changes** — Note with tracked changes + migrated comments
- **Respond in comments** — agent replies to each comment as threaded replies. No document mutation.
**6. Instruction field** — required for "Send with instructions," optional for "Respond in chat."
**7. Reference note** — "Reference: {type}:{id} will be included automatically." Always included so agent knows the document even in a new chat.
**8. Send + Cancel buttons.**
Request schema:
```ts
interface DocumentReviewRequest {
command: "document_review_request"
request_id: string
target_id: string
target_type: "artifact" | "document" | "note" | "code_file" | "pdf" | "image"
agent_id: string
chat_id: string
context_scope: "full" | "comments_only" | "selected_comments"
selected_comment_ids: string[] | null
comments: {
comment_id: string
anchor_text: string | null
context_before: string | null
context_after: string | null
body: string
replies: { author: string; body: string }[]
}[]
instruction: string | null
output_mode: "respond_in_chat" | "send_with_instructions"
result_format: "revise_artifact" | "convert_to_note" | "respond_in_comments" | null
}
```
Response schema:
```ts
type DocumentReviewResultDisposition =
| "revised_artifact" | "chat_response" | "note_created" | "comments_replied" | "error"
interface DocumentReviewResultEvent {
request_id: string
target_id: string
disposition: DocumentReviewResultDisposition
artifact_id: string // R2: stable root ID, NOT new_artifact_id
new_version_id: string | null // R2: new version within the artifact
created_note_id: string | null
chat_message_id: string | null
comments_addressed: string[]
comments_replied: { comment_id: string; reply_id: string }[] | null
error_code: string | null
completed_at: string
}
```
### 6.16.9 Convert to Note
**PDF-to-Note lossy migration (R1.6):**
Three anchor quality levels after conversion:
1. **Exact** — Tiptap character offsets preserved (HTML/Markdown sources)
2. **Approximate** — best-effort positional mapping, `anchor_status: "approximate"` (PDF sources)
3. **Lost** — no mapping possible, `anchor_status: "orphaned"` (image sources, badly structured PDFs)
Migration summary toast: "Converted to note. {N} comments migrated ({M} approximate, {K} orphaned). [View in comments]"
Page-boundary markers inserted as `<hr>` with data attribute: `data-source-page="{N}"`.
Creates a new Note from document content:
1. EC reads content
2. Markdown/text/code → Tiptap JSON body directly
3. DOCX → mammoth.js → HTML → Tiptap JSON
4. PDF → extracted text (formatting best-effort)
5. HTML → sanitized → Tiptap JSON
6. Other → plain text or "Content not convertible" error
7. EC creates Note with `project_id` inherited, revision kind `import`
8. Comments migrated to Note's comment rail with anchors remapped
9. Q opens new Note in Notes editor
Original document/artifact not modified or deleted. The Note is a fork.
### 6.16.10 Immutable versioning
**Bulk action versioning (R2):** "Accept All" and "Reject All" are single commands (`NoteTrackedChangeAcceptAllCommand` / `ArtifactDiffAcceptAllCommand`) that produce exactly ONE new version, not one per change. The command processes all pending changes in a single transaction.
**Version dropdown overflow (R2):** If total version count exceeds 50, show the 10 most recent versions + a "View all versions" link that opens a scrollable drawer with full history.
**Version major/minor differentiation (R1.6):**
**Major versions** are created by: `ArtifactCreateCommand`, `ArtifactCreateFromChatCommand`, `ArtifactConvertToNoteCommand`, `NoteImportCommand`, `NoteRestoreRevisionCommand`, or any command with `origin_type` in the creation event.
**Minor versions** are created by: `ArtifactDiffAcceptCommand` (individual change acceptance) and tracked-change accept-all that does not change the origin.
The version dropdown shows: all major versions + the 3 most recent minor versions. A "Show all versions" toggle reveals the complete list.
**Agent reply threading rule (R1.6):** Agent replies to viewer comments always create a top-level thread. Agents do not reply to individual replies within a thread.
**Pointer event normalization (R1.6):** All interaction handlers use `pointerdown` (not `mouseDown`) for cross-device compatibility. Desktop-first — touch optimization is deferred (DOC16).
Documents viewed in the Document Viewer are **immutable by default**. Accepting tracked changes does NOT modify the current version:
1. User sees 3 pending changes from Elnor
2. User accepts #1, #2, rejects #3
3. EC creates **new version** (v4) with #1/#2 applied, #3 discarded
4. v3 (with all 3 pending) remains accessible in version history
5. Viewer auto-switches to v4
Version history dropdown:
```text
v4 — User review · 2m ago ✓ (current)
v3 — Review revision · 10m ago
v2 — Review revision · 2h ago
v1 — Original generation · 1d ago
```
Each version has its own comment set. Clicking a version swaps rendered content.
```ts
interface ArtifactVersionSchema {
artifact_id: string
parent_artifact_id: string | null
version_number: number
title: string
artifact_kind: string
origin_type: "chat" | "task" | "panel" | "forum" | "automation" | "review_revision" | "user_review"
origin_id: string
review_request_id: string | null
path: string | null
created_at: string
}
```
### 6.16.11 Markup / Clean toggle
When pending tracked changes exist:
- **Markup** (default) — red strikethrough deletions, green underline insertions, per-change accept/reject buttons, author labels
- **Clean** — document renders as if all pending changes were accepted. Changes NOT accepted — just hidden. Toggle back to see marks.
Equivalent to Word's "Final" vs "Final Showing Markup."
### 6.16.12 Storage
```text
ELNOR_MEMORY/artifacts/
artifacts_current.json
artifacts_events.jsonl
by_id/
<artifact_id>/
content.<ext>
comments_current.json
comments_events.jsonl
metadata.json
```
### 6.16.13 Metadata rail
Compact bar at bottom of viewer:
- **Origin**: "{Agent} in {Chat}" with clickable link
- **Project**: project name with color dot
- **Version**: version number + pending change count
- **Age**: creation timestamp
- **Pinned**: 📌 indicator when pinned
### 6.16.14 Comment interaction wiring
Every comment action has explicit visual behavior. Coding agents must not guess.
**Reply:** Click Reply → textarea below replies → Enter submits (Shift+Enter = newline) → new reply appears → textarea hidden. Cancel closes. EC: `ArtifactCommentReplyCommand`
**Edit:** Click Edit (own comments only) → body becomes textarea → Save/Cancel → updates body. EC: `ArtifactCommentUpdateCommand`
**Resolve:** Click → comment moves to "Resolved" section at 60% opacity → "Resolve" becomes "Reopen" → anchor highlight removed. EC: `ArtifactCommentResolveCommand`
**Reopen:** Click on resolved → moves back to "Open" → full opacity → highlight restored. EC: `ArtifactCommentReopenCommand`
**Delete:** Click (own comments only) → "Delete" replaced by "Delete? **Yes** No" inline → Yes: soft-delete (hidden, preserved in event log) → No: restore text. No browser confirm() dialogs. EC: `ArtifactCommentDeleteCommand`
**Add via bubble:** Select text → bubble menu → Comment → right panel opens to Comments tab → new-comment input with anchor quote → Enter submits → appears in Open section. EC: `ArtifactCommentAddCommand`
**Add general:** Click "Add general comment…" at bottom → unanchored comment input. EC: `ArtifactCommentAddCommand` with null anchor.
**Comment IDs:** Use `useRef` monotonic counter (not array.length + 1) to prevent collisions after deletes.
---
## 6.17 Reserved
---
## 6.18 Unified Content Map — Storage, Discovery, and Lifecycle (R1.4)
### 6.18.1 Purpose
ELNOR manages many content types across many subsystems. Each subsystem defines its own storage, but no single place previously answered: "where is everything, how does the browser find it, how does Elnor know about it, and what happens when content is created, moved, or deleted?"
This section is the **unified content map**. It cross-references every content type, its owner spec, storage location, how it's indexed for browser discovery, and how Elnor becomes aware of it. It also defines the previously unspec'd artifact creation pipeline and attachment model.
### 6.18.2 Content type registry
**Maintenance rule:** This table is a **snapshot** regenerated during cross-document integration passes (see Integration Plan). Owner specs are the source of truth for their storage paths. The cross-doc review prompt template (check category #12) catches new or changed content types during pairwise reviews. If this table conflicts with an owner spec, the owner spec wins.
**Browsable** = appears in the browser results list. **Searchable** = Elnor can find/reference via DocIndex or QMD. **Internal** = system infrastructure, not user-facing.
#### User-facing content types (browsable in browser)
| Content type | Owner spec | Storage path | Browser type chip | Indexed by | Browsable | Searchable |
|---|---|---|---|---|---|---|
| Notes | DOC20 §6 | `ELNOR_MEMORY/notes/` | Note | notes_current.json + DocIndex | ✓ | ✓ |
| Generated artifacts | DOC20 §6.16 | `ELNOR_MEMORY/artifacts/by_id/{id}/` | Artifact | artifacts_current.json + DocIndex | ✓ | ✓ |
| External documents | EC §16, DocIndex EC-B9 | User filesystem (OneDrive, local, etc.) | Document | DocIndex + QMD + DOC18 | ✓ | ✓ |
| Chats / conversations | EC §6, §2.2 | `ELNOR_MEMORY/sessions/conversations/` | Chat | Conversation metadata | ✓ | ✓ |
| Projects | EC §2.2, DOC20 §4 | `ELNOR_MEMORY/projects/{id}/manifest.json` | Project | Project manifests | ✓ | ✓ |
| Agents | EC §6, §2.2 | `ELNOR_MEMORY/agents/` | Agent | Agent registry | ✓ | ✓ |
| Overlays | DOC17 §1.1 | `ELNOR_MEMORY/system/overlays/templates/` | Overlay | DOC17 index.json | ✓ | ✓ |
| Prompt recipes | DOC17 §1.1 | `ELNOR_MEMORY/system/prompt_recipes/recipes/` | Prompt | DOC17 index.json | ✓ | ✓ |
| Task templates | EC §2.2, §7 | `ELNOR_MEMORY/core/task_templates/` | Preset | EC task_profiles.json | ✓ | ✓ |
| Task chains | EC §8 | `ELNOR_MEMORY/core/task_chains/` | Preset | EC active_chains.json | ✓ | Partial |
| Rooms (multi-agent) | DOC12 §canoncial | `ELNOR_MEMORY/system/rooms/{id}/` | Panel | Room state registry | ✓ | Partial |
| Forum posts | EC §27 | `ELNOR_MEMORY/forum/` | Forum | Forum post index | ✓ | ✓ |
| Bucket files | DOC7 §2 | `ELNOR_MEMORY/system/context/buckets/` | Bucket | DOC7 files_index_current.json | ✓ | ✓ |
| Skills / capabilities | DOC3 §canoncial | `ELNOR_MEMORY/system/capabilities/manifests/` | Skill | Capability registry | ✓ | ✓ |
| Elnor outputs | EC §16.4 | `ELNOR_MEMORY/output/` | Document | DocIndex (on creation) | ✓ | ✓ |
| Attachments | DOC20 §6.18.4 | `ELNOR_MEMORY/attachments/by_id/{id}/` | (child of parent) | Attachment index | Via parent | ✓ |
| To-do lists | DOC20 §6.21 | `ELNOR_MEMORY/todo_lists/` | (via Notes scope toggle) | todo_lists_current.json | ✓ (via Notes scope) | ✓ |
| To-do items | DOC20 §6.21 | `ELNOR_MEMORY/todo_items/` | (child of list) | Via parent list | Via parent | ✓ |
| Calendar events | DOC20 §6.22 | `ELNOR_MEMORY/cal_events/` | (via Notes scope toggle) | cal_events_current.json | ✓ (via Notes scope) | ✓ |
| Quick command chats | DOC20 §6.20.30D | `ELNOR_MEMORY/sessions/conversations/` | Chat (⚡ indicator) | Conversation metadata | ✓ | ✓ |
#### Content types not yet implemented (deferred — DOC16)
| Content type | Owner spec | Storage path | Browser type chip | Status |
|---|---|---|---|---|
| Room presets / templates | DOC16 §A6 (deferred) | TBD (`system/rooms/presets/` proposed) | Preset | Deferred — schemas drafted in DOC16 but not in DOC12 yet |
| Litigation workbench presets | DOC16 §A6 (deferred) | TBD | Preset | Deferred — depends on room presets |
#### System-internal content (not browsable, not user-facing)
| Content type | Owner spec | Storage path | Purpose |
|---|---|---|---|
| User profile / preferences | EC §2.2 | `ELNOR_MEMORY/core/user_profile.json`, `preferences.json` | Memory extraction targets |
| Standing orders / corrections | EC §2.2 | `ELNOR_MEMORY/core/standing_orders.json`, `corrections.json` | Authority memory (always outranks heuristic) |
| Domain profiles | EC §2.2 | `ELNOR_MEMORY/core/domain_profiles/` | Per-domain extracted knowledge |
| Patterns / vocabulary | EC §2.2 | `ELNOR_MEMORY/core/patterns.json`, `shared/vocabulary.json` | Behavioral patterns, term definitions |
| Topic capsules | EC §2.2 | `ELNOR_MEMORY/core/topic_capsules/` | Living project summaries. QMD-searchable but not directly browsable |
| CIL signals / knowledge nodes | DOC15 §CIL | `ELNOR_MEMORY/system/cil/` | Learning signals, heuristic knowledge graph |
| CIL profiles | DOC15 §CIL | `ELNOR_MEMORY/system/cil/profiles/` | Context planning profiles |
| CRS manifests / checkpoints | EC §EC-B12 | `ELNOR_MEMORY/system/crs/` | Context resilience infrastructure |
| DocIndex | EC §EC-B9 | `ELNOR_MEMORY/doc_index.jsonl` | Document metadata registry (index, not content) |
| Doc skeletons | EC §EC-B9.4 | `ELNOR_MEMORY/doc_skeletons/` | Lossless structure cache |
| Process memory | EC §EC-B10 | `ELNOR_MEMORY/processes.jsonl` | Multi-step process tracking |
| DOC7 cache | DOC7 §5 | `ELNOR_MEMORY/system/context/buckets/cache/` | Extraction cache (LRU eviction) |
| DOC7 pasted text | DOC7 §1.5 | `ELNOR_MEMORY/system/context/buckets/pasted/` | Durable pasted text (never evicted) |
| Learning runtime | DOC3 §learning | `ELNOR_MEMORY/system/learning/runtime/` | Active learning state |
| Capability health / traces | DOC3 §cap | `ELNOR_MEMORY/system/capabilities/health_current.json`, `traces.jsonl` | Skill health monitoring |
| Teaching sessions | DOC3 §cap | `ELNOR_MEMORY/system/capabilities/teaching_sessions/` | Active teach-mode sessions |
| Prompt advisor config | DOC17 §1.1 | `ELNOR_MEMORY/system/prompt_advisor/` | Rewrite templates, advisor state |
| Prompt lab results | DOC17 §1.1 | `ELNOR_MEMORY/system/prompt_lab/` | Offline evaluation results |
| DOC17 feedback signals | DOC17 §1.1 | `ELNOR_MEMORY/signals/doc17/` | Overlay/recipe/advisor feedback |
| Room attachments | DOC12 | `ELNOR_MEMORY/system/rooms/{id}/attachments/` | Files shared in rooms (child of room) |
| Forum attachments | EC §27 | `ELNOR_MEMORY/forum/attachments/` | Files shared in forum (child of forum) |
| Chain run artifacts | EC §8.8 | `chain_runs/{id}/artifacts/` | Intermediate + final chain outputs. Promoted to artifacts/ on explicit save |
| Command queue | EC §2.2 | `ELNOR_MEMORY/system/commands/commands.jsonl` | Single-writer command serialization |
| Memory audit log | EC §2.2 | `ELNOR_MEMORY/system/memory_audit.jsonl` | All memory change audit trail |
| Fleet state | EC §2.2 | `ELNOR_MEMORY/system/fleet/` | Multi-instance coordination |
| LlamaIndex corpus bindings | DOC18 §canonical | `ELNOR_MEMORY/system/retrieval/llamaindex/` | Corpus config (index data in ELNOR_STATE/) |
| Module presets | DOC20 §6.2C | `ELNOR_MEMORY/task_system/module_presets/` | Feed and module preset definitions (R4) |
| Notification store | DOC20 §6.25 | `ELNOR_MEMORY/system/notifications/` | Notification cards, read/snooze state (R4) |
### 6.18.3 Artifact creation pipeline
**Artifact creation heuristics update (R1.6):**
- **Thresholds lowered:** Auto-creation triggers at **300 tokens / 15 lines** (was 500/20). Rationale: useful artifacts like short code snippets and brief analyses were falling through.
- **Near-threshold affordance:** For agent responses between 200–300 tokens (or 10–15 lines), Q shows a subtle affordance below the chat response: "Save as artifact?" with a single-click button. This does NOT auto-create — it offers the option.
- **Always-available manual save:** Every chat response with code blocks or structured content shows a "Save as Artifact" button in the response action bar, regardless of length. — NEW
**Problem:** When Elnor generates a document, code file, chart, or other work product in a chat conversation, how does that output become a durable artifact accessible from the browser, document viewer, and project?
**Current state:** EC §16.4 defines `ELNOR_MEMORY/output/` as the default output location for file transforms. DOC20 §6.16.12 defines `ELNOR_MEMORY/artifacts/` for artifact storage. But the pipeline connecting a chat response to a durable artifact is undefined.
**Defined pipeline:**
#### Step 1: Detection — what becomes an artifact?
Not every chat response is an artifact. EC detects artifactable content using these heuristics:
| Content pattern | Artifact? | Detection |
|---|---|---|
| Agent produces a complete document (brief, memo, report, analysis) | Yes | Content > 500 tokens AND structured (headings, sections) OR agent explicitly marks as document |
| Agent produces code (script, component, config file) | Yes | Code block > 20 lines OR agent explicitly marks as code output |
| Agent produces a chart, diagram, or visualization | Yes | Contains SVG/canvas/Mermaid/chart content |
| Agent produces a table or structured data set | Conditional | Only if > 10 rows or agent marks as deliverable |
| Agent answers a question conversationally | No | Short response, no structure |
| Agent provides a list, short summary, or explanation | No | Informational, not a deliverable |
Additionally, the user can explicitly save any chat content as an artifact via a "Save as Artifact" action on any chat message.
#### Step 2: Creation — how artifacts are persisted
When an artifact is detected or explicitly saved:
```ts
interface ArtifactCreateFlow {
// 1. EC assigns artifact_id (UUID)
artifact_id: string
// 2. EC determines artifact_kind from content
artifact_kind: "document" | "code" | "chart" | "data" | "image" | "mixed"
// 3. EC writes content file
// Path: ELNOR_MEMORY/artifacts/by_id/{artifact_id}/content.{ext}
content_path: string
// 4. EC writes metadata
// Path: ELNOR_MEMORY/artifacts/by_id/{artifact_id}/metadata.json
metadata: {
artifact_id: string
title: string // derived from content or agent label
artifact_kind: string
format: string // "markdown" | "typescript" | "python" | "html" | "svg" | etc.
origin_type: "chat" | "task" | "room" | "panel" | "forum" | "chain" | "manual_save"
origin_id: string // conversation_id, task_id, room_id, etc.
origin_message_id: string | null // specific message in the chat
agent_id: string // which agent produced it
project_id: string | null // inherited from chat's project scope
version_number: 1
parent_artifact_id: null // first version
created_at: string
size_bytes: number
estimated_tokens: number
tags: string[] // auto-generated from content
}
// 5. EC appends to artifacts_current.json (atomic write)
// 6. EC appends to artifacts_events.jsonl (append-only)
// 7. EC registers in DocIndex (ensureDocIndexEntry)
// 8. EC notifies Q via event bus → browser refreshes, chat shows artifact card
}
```
#### Step 3: Chat linkage — artifacts stay tied to their origin
Every artifact records its `origin_type` and `origin_id`. This creates a bidirectional link:
- **Chat → artifacts:** The conversation metadata (`sessions/conversations/{id}.json`) gains an `artifact_ids: string[]` array listing all artifacts generated in that conversation.
- **Artifact → chat:** The artifact metadata has `origin_id` pointing back to the conversation.
- **Browser:** When showing a chat in the browser results, the chat row can show artifact count. When showing an artifact, the metadata rail shows "Generated by {Agent} in {Chat}" with clickable link.
- **Project:** Artifacts inherit `project_id` from the chat's project scope. They appear in the project's Documents > Generated subtab.
#### Step 4: DocIndex registration — how Elnor knows about artifacts
When an artifact is created, EC calls `ensureDocIndexEntry()` (EC-B9) to register it in `ELNOR_MEMORY/doc_index.jsonl`:
```json
{
"doc_id": "artifact_msj_response_brief",
"filename": "content.md",
"paths": ["ELNOR_MEMORY/artifacts/by_id/a1b2c3/content.md"],
"type": "markdown",
"size_bytes": 4200,
"estimated_tokens": 1100,
"first_seen": "2026-03-16T10:00:00Z",
"last_accessed": "2026-03-16T10:05:00Z",
"access_count": 1,
"content_hash": "sha256:abc...",
"aliases": ["the motion brief", "MSJ response", "summary judgment brief"],
"summary": "Response brief opposing DataCorp's motion for summary judgment...",
"topics": ["henderson", "securities", "10b5", "scienter"],
"capsule_refs": ["capsule_henderson_v_datacorp"],
"usage_context": "Generated from Henderson discovery chat. Cited Vivendi, Omnicare.",
"open_method": "document_viewer",
"artifact_id": "a1b2c3",
"origin_chat_id": "conv-henderson-discovery"
}
```
This means Elnor can find artifacts via:
- **DocIndex alias resolution:** "Pull up the motion brief" → resolves to the artifact
- **QMD vector search:** semantic search over artifact content (indexed per EC §23)
- **DOC18 LlamaIndex:** optional corpus indexing if artifact is in a bound corpus
- **Browser read model:** direct listing with type/sort/filter
#### Step 5: Versioning — subsequent versions
When an artifact is revised (via Document Viewer "Send to Agent" → "Revise artifact (new version)"):
1. EC creates a new artifact entry with `parent_artifact_id` pointing to the original
2. New content file written to `ELNOR_MEMORY/artifacts/by_id/{new_id}/content.{ext}`
3. DocIndex entry updated with new version info
4. Conversation metadata updated to include the new artifact_id
5. Browser and Document Viewer show the version chain
### 6.18.4 Attachment model — NEW
**Problem:** Users drop files into chats, paste images into notes, and attach files to tasks. Where do these go?
**Principle: Reference, don't copy.**
For files that already exist on disk (PDFs, DOCX, etc. from OneDrive or local filesystem):
- Store a **reference** (path + content_hash), not a copy
- The reference lives in the parent entity's metadata (chat message, note body, task attachment list)
- DocIndex tracks the file at its original path
- If the file moves or is deleted, the reference goes stale → DocIndex marks it stale via freshness tracking (EC-B9.3)
For files with no existing disk path (pasted images, in-memory content, web clipboard):
- Store the binary in `ELNOR_MEMORY/attachments/{attachment_id}/`
- Create a DocIndex entry for discoverability
- Reference via `attachment:{attachment_id}` in the parent entity
```ts
interface AttachmentSchema {
attachment_id: string
filename: string
mime_type: string
size_bytes: number
storage_mode: "reference" | "stored"
// For storage_mode = "reference" (file on disk)
source_path: string | null // original filesystem path
content_hash: string // SHA-256 for freshness
// For storage_mode = "stored" (binary in ELNOR_MEMORY)
stored_path: string | null // ELNOR_MEMORY/attachments/{id}/{filename}
// Parent linkage
parent_type: "chat_message" | "note" | "task" | "room_message" | "forum_post"
parent_id: string
// Metadata
created_at: string
created_by: string // "user" | agent_id
project_id: string | null
doc_index_id: string | null // link to DocIndex entry if registered
}
```
**Storage layout:**
```text
ELNOR_MEMORY/attachments/
attachments_current.json ← atomic, all active attachments
attachments_events.jsonl ← append-only event log
by_id/
{attachment_id}/
{original_filename} ← the binary file (stored mode only)
```
**Note attachments specifically:**
When a user pastes an image into a Tiptap note:
1. Q uploads the image binary to EC
2. EC stores it at `ELNOR_MEMORY/attachments/by_id/{id}/{filename}`
3. EC returns an attachment_id
4. Tiptap inserts an image node referencing `attachment:{attachment_id}`
5. The note's autosave captures the reference in the Tiptap JSON body
6. When the note renders, Q resolves `attachment:{id}` to the stored file path
**Deduplication:**
Files are deduplicated by `content_hash`. If the same PDF is attached to 3 different chats:
- One DocIndex entry exists for the file (at its original path)
- Three attachment references exist (one per chat message), all pointing to the same `content_hash`
- No binary is duplicated — the references all resolve to the same path
- If the file is pasted (no original path), the first stored copy is canonical; subsequent references point to the same `attachment_id`
### 6.18.4A Stale reference UX (R1.6)
**Broken-reference badge:** When an attachment's `availability_status` is `"stale"` or `"missing"`, the browser result row and any inline reference show a broken-link badge (⚠ icon with tooltip: "File not found at {path}. Last verified: {date}.").
**Re-link file picker:** Clicking the badge opens a file picker modal: "This file has moved or been deleted. Select the new location." The picker starts at the file's last known directory. On selection, EC updates the attachment path and `content_hash`.
**Auto-detect by content hash:** On Place sync, EC compares `content_hash` of missing files against all files in the Place. If a match is found at a new path, EC auto-updates the reference and shows toast: "Found {filename} at new location."
**Inline placeholder in notes/viewer:** Stale attachments render as a placeholder block: "⚠ {filename} — file not found. [Re-link] [Remove reference]"
### 6.18.5 Browser discovery — how the browser finds everything
The browser read model (DOC20 §3, `BrowserQueryRequest/Response`) needs to aggregate content from multiple storage locations. The **BrowserResolver** (defined in R1.3 §3.9.8) queries:
| Source | Content types found | Index queried |
|---|---|---|
| `ELNOR_MEMORY/artifacts/artifacts_current.json` | Artifacts | Direct read |
| `ELNOR_MEMORY/notes/notes_current.json` | Notes | Direct read |
| `ELNOR_MEMORY/sessions/conversations/` | Chats | Conversation metadata |
| `ELNOR_MEMORY/system/context/buckets/registry.json` | Buckets | DOC7 registry |
| `ELNOR_MEMORY/projects/` | Projects | Project manifests |
| `ELNOR_MEMORY/agents/` | Agents | Agent registry |
| `ELNOR_MEMORY/system/overlays/index.json` | Overlays | DOC17 index |
| `ELNOR_MEMORY/system/prompt_recipes/index.json` | Prompts | DOC17 recipe index |
| `ELNOR_MEMORY/core/task_templates/` | Presets (task) | Task profiles |
| `ELNOR_MEMORY/system/rooms/` | Panels/Forums | Room state |
| `ELNOR_MEMORY/system/capabilities/` | Skills | Capability registry |
| `ELNOR_MEMORY/doc_index.jsonl` | External documents | DocIndex |
| `ELNOR_MEMORY/attachments/attachments_current.json` | Attachments (child items) | Attachment index |
The BrowserResolver reads these sources and projects them into the unified `BrowserItemReadModel` (DOC20 §7.12). Each source maps to a `BrowserItemType` per the type chip registry (§3.19).
**Performance:** The resolver does not scan all sources on every keystroke. It maintains a **materialized read model** (`ELNOR_MEMORY/system/browser/browser_read_model_current.json`) that is updated incrementally when any source changes. EC file watchers (EC §15A) detect changes and trigger partial re-derivation.
### 6.18.6 Elnor's content awareness — how Elnor knows what exists
Elnor discovers and references content through four layers:
**Layer 1: DocIndex (EC-B9)** — the primary content registry. Every artifact, document, note, and significant file gets a DocIndex entry with aliases, summary, skeleton, and topics. When a user says "pull up the motion brief," DocIndex alias resolution finds it. When Elnor needs to reference a file in a response, DocIndex provides the path, format, and open method.
**Layer 2: QMD vector search (EC §23)** — semantic search over ELNOR_MEMORY. Indexes all Tier 0 and Tier 1 content automatically. Elnor can search by meaning, not just name. "Find my analysis of the scienter argument" → QMD returns the artifact even if the title doesn't contain "scienter."
**Layer 3: DOC18 LlamaIndex (optional)** — deep semantic retrieval over selected corpora. For large document sets (e.g., all Henderson case files), LlamaIndex provides more precise retrieval than QMD alone.
**Layer 4: DOC15 CIL signals** — context relevance scoring. CIL doesn't store content but scores it for injection priority. When assembling context for a conversation, CIL determines which artifacts, notes, and documents are most relevant based on project scope, topic match, recency, and usage patterns.
**What Elnor knows about each content type:**
| Content type | Elnor can... | Via |
|---|---|---|
| Artifacts | Find, read, summarize, reference, review, version, comment on | DocIndex + QMD + Document Viewer skills |
| Notes | Find, read, write, comment on, add tracked changes, search | DOC3 NoteReadSkill/NoteWriteSkill |
| External docs | Find, read (if accessible), summarize, reference | DocIndex + file access skills |
| Overlays | Find, read, activate, suggest | DOC17 overlay skills |
| Prompts | Find, read, suggest, apply | DOC17 recipe skills |
| Chats | Read transcripts, reference, continue | Conversation session access |
| Tasks | Read, create, update, reference | DOC3 task skills |
| Bucket files | Read, reference, suggest additions | DOC7 bucket skills |
### 6.18.7 Lifecycle rules
**Archive/delete/trash model (R1.6):**
| Type | Archive | Delete | Undo window | Retention | Restore |
|---|---|---|---|---|---|
| Note | ✓ (NoteArchiveCommand) | ✓ (NoteDeleteCommand) | 5s toast | 30 days | From "Recently Deleted" view |
| Project | ✓ (ProjectArchiveCommand) | ✓ (ProjectDeleteCommand) | 5s toast | 30 days | From "Recently Deleted" view |
| Artifact | — | ✓ (ArtifactDeleteCommand) | 5s toast | 30 days | From "Recently Deleted" view |
| Folder | — | ✓ (FolderDeleteCommand) | 5s toast | Immediate (items unfiled) | — |
| Collection | — | ✓ (CollectionDeleteCommand) | 5s toast | Immediate | — |
| Place | — | ✓ (PlaceRemoveCommand) | 5s toast | Immediate | — |
| Saved View (user) | — | ✓ (SavedViewDeleteCommand) | 5s toast | Immediate | — |
**Undo toast:** All delete/archive actions show a 5-second toast: "{Item title} {archived/deleted}. [Undo]". Clicking Undo reverses the action. After 5 seconds, the action is committed.
**"Recently Deleted" saved view:** Built-in saved view filtering to `status: "deleted"` and `deleted_at > (now - 30 days)`. Items show "Restore" and "Delete permanently" actions.
**Recently Deleted UX (R2):**
- Empty state: "No recently deleted items. Deleted items appear here for 30 days."
- Per-item metadata: "Deleted X days ago · Permanently removed in Y days"
- Actions: [Restore] per item (calls NoteRestoreCommand / ProjectRestoreCommand / ArtifactRestoreCommand)
- Bulk action: [Empty Recently Deleted] with destructive confirmation modal: "Permanently delete all {N} items? This cannot be undone."
- Items appear when `status === "deleted"` AND `deleted_at >= now - 30 days`
**30-day retention:** Deleted items (notes, projects, artifacts) are soft-deleted: `deleted: true`, `deleted_at: timestamp`. After 30 days, EC permanently removes the data. Retention period configurable in ELNOR settings. — what happens on delete, archive, and project changes
| Action | Content behavior |
|---|---|
| **Delete a chat** | Chat archived to `sessions/archive/`. Artifacts generated in that chat survive — they are independent entities. Artifact `origin_id` still points to the archived chat. |
| **Delete a note** | Note soft-deleted (status "deleted"). Attachments survive (attachment references become orphaned but binaries preserved). DocIndex entry marked stale. |
| **Delete an artifact** | Artifact soft-deleted. All versions preserved in event log. DocIndex entry marked stale. Comments preserved in event log. |
| **Delete a project** | Per EC §4.7: detach or delete content. Detach: items lose project_id but survive. Delete: cascades to all project-scoped content. |
| **Archive a note** | Status "archived." Visible in browser with archive filter. DocIndex entry unchanged. |
| **Archive an artifact** | Status "archived." Same as note archive. |
| **Remove a Place** | Place alias removed. Files on disk untouched. DocIndex entries for files in that place are NOT removed (they were discovered independently). |
| **Delete a folder** | Virtual folder removed. Items inside become unfiled. No content is deleted. |
| **Orphaned attachments** | EC nightly maintenance scans for attachments whose parent entity has been deleted. Orphaned attachments are logged but NOT auto-deleted (user may want to recover). After 90 days, orphans are moved to `attachments/archive/`. |
### 6.18.8 Cross-spec obligations from §6.18
| Owner spec | Obligation | Status |
|---|---|---|
| EC Core | Add `artifact_ids: string[]` to conversation metadata schema. Implement artifact creation pipeline (§6.18.3 steps 1-5). Add `ensureDocIndexEntry()` call on artifact creation. | new |
| EC Core | Add `ELNOR_MEMORY/attachments/` to §2.2 directory structure. Implement attachment storage + deduplication. | new |
| EC Core | Implement browser read model materialization (`browser_read_model_current.json`) with incremental updates. | new |
| DOC7 | No changes needed — bucket files remain in DOC7 storage. Browser resolver reads DOC7 registry as one of many sources. | none |
| DOC17 | No changes needed — overlays/recipes remain in DOC17 storage. Browser resolver reads DOC17 index. | none |
| DOC3 | Register ArtifactWriteSkill: `create_artifact`, `save_chat_as_artifact`, `update_artifact_metadata`. Register AttachmentSkill: `upload_attachment`, `get_attachment`, `list_attachments`. | new |
| DOC15 | Add `artifact_reference` signal type alongside existing `note_reference`. Score artifacts by project scope, origin chat relevance, and access recency. | new |
| DOC16 | Track: attachment garbage collection policy, cross-device attachment sync (Phase 2), attachment versioning for overwritten files. | new |
### 6.18.9 "Save as Prompt" flow — connecting DOC20 to DOC17
When the user clicks "Save as Prompt" in the Document Viewer or Notes toolbar:
1. Q reads the document/note content as plain text
2. Q opens a Save Prompt dialog: title field (pre-filled from document title), description, optional tags
3. On save, Q sends `prompt_recipe.create command (DOC17 §12.2)` to EC
4. EC writes the recipe file to `ELNOR_MEMORY/system/prompt_recipes/recipes/{id}.md`
5. EC updates DOC17 recipe index
6. Toast confirms. Recipe appears in browser under Prompt type.
This bridges DOC20's UI surface to DOC17's storage.
### 6.18.10 "Save as Artifact" from chat — explicit artifact creation
When the user right-clicks a chat message and selects "Save as Artifact" (or clicks a save icon on an artifact-like message):
1. Q sends `ArtifactCreateFromChatCommand` to EC with `conversation_id` + `message_id`
2. EC extracts the message content
3. EC follows the artifact creation pipeline (§6.18.3)
4. Toast confirms. Artifact appears in browser and project Generated tab.
```ts
type ArtifactCreateFromChatCommand = ECCommandEnvelope<{
conversation_id: string
message_id: string
title?: string // user can override auto-detected title
project_id?: string | null // defaults to chat's project
}>
```
---
## 6.19 Embedded Workspace Browser (R2)
### 6.19.1 Overview and platform dependency
The Q Browser is an embedded web browser rendered as a new surface within Q's main content area. It uses Electron's `<webview>` tag to render full web pages with complete Chromium fidelity — sites look and behave identically to Chrome.
**Platform dependency:** This feature requires Q to run as an Electron desktop app. When Q runs as a web app in a standard browser (the current default architecture), the Q Browser surface is hidden — it does not appear in the sidebar, route table, or any menu. The toggle and route are available only when `window.electronAPI` is defined (indicating Electron runtime).
**Dual capability model:** Q maintains both deployment paths from the same codebase:
- **Web app (default):** React SPA at `localhost:3741`, accessed via any browser. All DOC20 surfaces except Q Browser are available. This is the starting configuration.
- **Electron desktop app (optional):** Same React code wrapped in an Electron shell. Adds Q Browser, system notifications, macOS Keychain access, full-screen window (no Chrome chrome), app launcher, Dock badge. Zero design changes to existing surfaces.
The Electron migration is an additive infrastructure step (~2 day lift). When executed, the web app continues to work alongside the desktop app for remote/mobile access via Tailscale (EC Core §15B).
### 6.19.2 Route and navigation
**Route:** `/browser` or `/browser?url={encoded_url}`
**Nav entry:** Globe icon in the left sidebar rail. Only visible when Electron runtime is detected. Position: below Notes, above Settings.
**Launch behavior:** Opening the Q Browser with no URL shows a start page with:
- URL input field (autofocused)
- Recent Pages list (last 20 visited URLs, stored in `ELNOR_MEMORY/browser/history_current.json`)
- Bookmarks list (user-saved URLs)
- Quick links: "PACER", "CM/ECF", "EDGAR", "Westlaw" (configurable in Settings)
### 6.19.3 Layout
```text
┌──────────────────────────────────────────────────────────────────────────┐
│ [Tab 1: PACER ×] [Tab 2: Westlaw ×] [Tab 3: CM/ECF ×] [+] 🔒 [⋯] │
├──────────────────────────────────────────────────────────────────────────┤
│ [←] [→] [↻] │ 🔒 Search or enter URL │ [☆] [⋯] │
│ │ URL / SEARCH BAR (full width, rounded) │ │
├──────────────┴────────────────────────────────────────────────────┬─────┤
│ │ │
│ │ C │
│ WEB PAGE CONTENT │ o │
│ (Electron <webview>) │ m │
│ │ m │
│ Full width, full height, scrollable │ e │
│ Renders identically to Chrome │ n │
│ │ t │
│ │ s │
│ │ │
├──────────────────────────────────────────────────────────────────┴─────┤
│ Status: ecf.cacd.uscourts.gov · 4,200 words · Paramount project 110% │
└───────────────────────────────────────────────────────────────────────┘
```
The tab bar sits at the top. Each tab shows: favicon, truncated title, close X. Active tab is visually prominent. Speaker icon on tabs playing audio. [+] button for new tab. Incognito tabs have a dark/purple background.
The web page content fills the entire content area — same dimensions as the Notes editor or Document Viewer. The URL bar adds ~38px of overhead above the page. The tab bar adds ~32px. The right panel (Comments / Ask Elnor) is the same shared `TabbedRightPanel` component used by Notes and Document Viewer, toggled via toolbar buttons.
The status bar shows: domain, word count, project association, and zoom level (when not 100%).
### 6.19.4 URL bar and navigation
The URL bar replaces the title bar that Notes and Document Viewer use. It contains:
| Element | Position | Behavior |
|---|---|---|
| Back button (←) | Left | Standard browser back. Disabled when no history. |
| Forward button (→) | Left | Standard browser forward. Disabled when no forward history. |
| Refresh button (↻) | Left | Reloads current page. |
| Lock icon | Inside URL field, left | Green lock = HTTPS. Gray globe = HTTP. |
| URL input | Center, flex:1 | Full-width rounded input. Dual-purpose: type a URL (with dots or protocol) to navigate directly, or type text (no dots) to search via the configured default search provider. Placeholder: "Search or enter URL." Shows current page URL when a page is loaded. Editable at any time — click to select all and type over. |
| Bookmark star (☆) | Right of URL field | Filled/gold = bookmarked. Click to toggle. Opens mini-editor: title, folder selector. |
| Page actions (⋯) | Right | Dropdown: Copy Page Text, Save As Artifact, Save As Note, Send to Chat, Reader Mode, Screenshot, Save Full Page as PDF, Open in Chrome, Print. |
**Search provider:** Configurable in browser settings (§6.19.28). Default: Google. Options: Google, DuckDuckGo, Brave Search, Bing, custom URL pattern with `{query}` placeholder.
**Zoom:** `Cmd+Plus` zooms in, `Cmd+Minus` zooms out, `Cmd+0` resets. Implemented via `webview.setZoomLevel()`. Zoom indicator appears in the status bar when zoom ≠ 100% — clicking it resets to default. Default zoom level configurable in settings (e.g., 110% for easier reading).
**Toolbar integration:** Below or merged with the URL bar:
| Element | Behavior |
|---|---|
| Copy | Copies full page text (extracted via webview IPC) |
| Save As… ▾ | Save as Artifact, Save as Note, Save as PDF (print-to-PDF) |
| Ref | Copies `@[{page_title}](web:{url})` |
| Comments ({N}) | Toggles right panel to Comments tab |
| Ask Elnor (accent) | Toggles right panel to Ask Elnor tab |
### 6.19.5 Webview renderer
The web page renders via Electron's `<webview>` tag:
```ts
// RendererCapabilities entry
web: {
text_selectable: true,
supports_tracked_changes: false,
supports_inline_comments: false, // web page DOM is not editable
supports_page_anchors: true, // comments anchor to selected text regions
supports_line_anchors: false,
supports_find: true, // webview has built-in find (Ctrl+F)
supports_copy: true,
supports_print: true,
editable: false,
}
```
**Session isolation:** The webview uses a dedicated `partition` (`persist:q-browser`) so its cookies, storage, and cache are isolated from Q's own session. Login credentials for PACER, Westlaw, etc. persist across Q restarts within this partition.
**Security:** The webview is process-isolated from Q's renderer. It cannot access Q's DOM, Node.js APIs, or ELNOR_MEMORY. Communication is via Electron IPC only.
### 6.19.6 Content extraction for Elnor context
Q extracts page content from the webview for Elnor context assembly:
```ts
interface WebPageContext {
url: string
title: string
domain: string
extracted_text: string // mozilla/readability clean text extraction
word_count: number
selected_text: string | null // user's current text selection
meta_description: string | null
extracted_at: string
}
```
**Extraction mechanism:** Q calls `webview.executeJavaScript()` to run a content extraction script (mozilla/readability or equivalent) inside the webview. This returns clean article text stripped of navigation, ads, and boilerplate. The extraction runs on demand — when the user opens the Ask Elnor panel or clicks a bubble menu action — not continuously.
**Context assembly:** When the user sends a page to Elnor, EC packages the `WebPageContext` alongside the standard ELNOR context: project bucket, topic capsule, standing orders, recent notes, conversation history. This is the core value of the Q Browser — Elnor has both the web page AND the full project context simultaneously. This is not possible when browsing in Chrome with the OpenClaw extension, which cannot access ELNOR_MEMORY, project buckets, or case notes.
### 6.19.7 Interaction surfaces
All interaction surfaces reuse existing DOC20 components:
**Bubble menu:** When the user selects text on the web page, the standard bubble menu appears (§6.6.6): Comment, Agent selector dot, Ask {Agent}, Summarize, X dismiss. The webview emits a `selectionchange` event via IPC; Q positions the bubble menu at the selection coordinates translated from webview-local to Q-window coordinates.
**Comment system:** Comments on web pages use `TextAnchor` with the page URL as the target. Anchors store the selected `quote_text` + `context_before` + `context_after` for relocating the anchor on revisit. If the page content changes between visits, anchors degrade gracefully per §6.6.8A (shifted/orphaned).
Comment storage: `ELNOR_MEMORY/browser/page_comments/{url_hash}/comments_current.json`. Comments are durable — they persist even if the page content changes.
**Ask Elnor panel:** The right panel "Ask Elnor" tab shows:
- Page context card (title, domain, word count, selected text if any)
- Instruction textarea ("e.g., Is this consistent with what they argued in Henderson?")
- Context inclusion checkboxes: page content + comments, active project context, recent case notes
- Ask Elnor send button (accent)
- Separator
- "Save as Artifact" / "Save as Note" quick actions
This routes through the existing `DocumentReviewCommand` (§8.6A) with `target_type: "web_page"` and the extracted `WebPageContext` as the content payload.
**Save as Artifact:** Extracts page content via readability, creates a durable artifact with `origin_type: "web_capture"`, `source_url`, and extracted text. The artifact is a clean text version of the page, stored permanently in ELNOR_MEMORY and browseable in the Browser column. Toast: "Saved as artifact: {title}."
**Save as Note:** Same extraction, but creates an editable Tiptap note with the extracted content converted to Tiptap JSON. The user can annotate, add task lists, create inline threads — the full workspace experience applied to captured web content. Toast: "Saved as note: {title}."
### 6.19.8 Browser history and visit tracking (R2.3 — rewritten)
**Visit log:** Stored at `ELNOR_MEMORY/browser/visits.jsonl` — append-only log of every page visit. Retained per retention policy (default 90 days). The most recent 500 entries are also materialized in `ELNOR_MEMORY/browser/history_current.json` for fast Q Browser start page search.
```ts
interface BrowserPageVisitSchema {
visit_id: string
url: string
url_hash: string // stable hash for dedup
title: string
domain: string
visited_at: string
dwell_start_at: string
dwell_end_at: string | null // updated on navigation away
dwell_seconds: number // computed on navigation away
project_id: string | null // auto-associated if domain matches known matter
is_m365_surface: boolean
m365_metadata: M365PageMetadataSchema | null
user_interactions: {
highlighted: boolean
commented: boolean
asked_elnor: boolean
saved_as_artifact: boolean
saved_as_note: boolean
clipped: boolean
}
tab_id: string
is_incognito: boolean // always false in the log (incognito pages are never logged)
schema_version: 1
}
```
**M365 enhanced metadata — DOM extraction, no LLM:**
When Q Browser detects an M365 web surface (matched by domain: `*.office.com`, `*.sharepoint.com`, `*.live.com`, `*.microsoft365.com`), it extracts additional metadata from the DOM:
```ts
interface M365PageMetadataSchema {
surface_type: "word_online" | "outlook_web" | "onedrive" | "sharepoint" | "excel_online" | "powerpoint_online" | "teams_web" | "other_m365"
document_id: string | null
sharepoint_path: string | null
direct_url: string | null // deep link to reopen this exact document
file_name: string | null
last_modified_at: string | null
last_modified_by: string | null
content_type: string | null // document, email, folder listing, etc.
schema_version: 1
}
```
This extraction is zero-cost (DOM scraping, no LLM) and runs synchronously on page load. It enables Elnor to learn document locations automatically — "the Henderson MTD draft is at this SharePoint URL" — without the user explicitly telling him.
**History search:** History is searchable from the Q Browser start page and via `WebBrowserReadSkill` (§6.19.27). History entries for excluded domains (§6.19.12) are still written (so you can navigate back) but are not emitted as signals to DOC72.
### 6.19.9 Bookmarks (R2.3 — rewritten with folders)
Bookmarks support nested folders. Stored at `ELNOR_MEMORY/browser/bookmarks_current.json`.
```ts
interface WebBookmarkSchema {
bookmark_id: string
url: string | null // null for folders
title: string
domain: string | null // null for folders
parent_folder_id: string | null // null = root level
is_folder: boolean
is_bar: boolean // R4: true for the special "Bookmarks Bar" folder
sort_order: number
project_id: string | null // optional project association
created_at: string
}
```
**Bookmarks Bar (R4):** A special folder with `is_bar: true` populates the thin bookmarks bar below the web browser toolbar (see §6.26). Items in this folder appear as favicon-style color badges with titles. Toggle visibility via settings (`bmBarVisible`). Only one folder can have `is_bar: true`.
**Bookmark sidebar** shows a tree with expand/collapse, drag to reorder, drag into folders. Right-click: Edit, Delete, New Folder, Move to Folder. Bookmarks appear on the Q Browser start page and sidebar.
**Bookmark management UI in browser column (R3):** The browser column's "Web" mode tab (§6.20.9) provides full Chrome-like bookmark management: folder CRUD (create, rename, delete, subfolder), bookmark CRUD (rename, delete), favicon squares (14px colored rounded squares with domain initial), search across all folders, and a separate "Recent Pages" section. This is the primary bookmark management surface.
When `project_id` is set, the bookmark also appears in the project's Documents tab under a "Web Bookmarks" section. Bookmarks are NOT Browser column items — they live within the Q Browser surface to avoid conflating web URLs with ELNOR entities.
**Import:** "Import Bookmarks" button in settings reads Chrome's exported bookmarks HTML file and creates matching entries. One-time operation.
Elnor can read the full bookmark tree via `WebBrowserReadSkill` — useful for queries like "what legal research sites do I have bookmarked?"
### 6.19.10 Tabs and tab groups (R2.3)
Multiple webview instances, one visible at a time. Inactive webviews remain alive in memory (`display: none`) to preserve session state.
**Tab bar:** Sits between the toolbar area and the page content (~32px height). Each tab shows: favicon, page title (truncated), close X button, speaker icon when audio is playing (click to mute/unmute). Active tab visually prominent (lighter background, no bottom border). Incognito tabs have a dark/purple background with mask icon.
**Tab actions:**
- `Cmd+T` — new blank tab (opens start page)
- `Cmd+W` — close current tab
- `Cmd+Shift+T` — reopen last closed tab
- Middle-click link — open in new tab
- Right-click link on any web page — "Open in New Tab" in context menu
- Drag tabs to reorder
**Right-click tab menu:** Close, Close Others, Close Tabs to Right, Duplicate, Pin (shrinks tab to favicon-only, stays at left), Mute/Unmute, Move to Group.
**Tab groups:** Select multiple tabs (Cmd+click), right-click → "Group Tabs." Assign a color label and optional name. Grouped tabs cluster together with a colored underline and collapsible group header. Click the group header to collapse/expand all tabs in the group. Groups are visual organization — no functional difference.
**Tab persistence:** Tab state persisted to `ELNOR_MEMORY/browser/tabs_current.json`. On Q restart, all tabs restore with their URLs (pages reload fresh — session state within the webview partition persists via cookies).
```ts
interface BrowserTabState {
tabs: {
tab_id: string
url: string
title: string
is_pinned: boolean
is_incognito: boolean
is_muted: boolean
group_id: string | null
}[]
active_tab_id: string
tab_groups: {
group_id: string
label: string | null
color: string
collapsed: boolean
}[]
}
```
**Audio:** Audio plays natively via Chromium. Speaker icon on tab when audio playing. `webview.setAudioMuted(true/false)` on click.
### 6.19.11 Privacy controls (R2.3)
#### 6.19.11A Domain exclusion list
DOC20 emits `BrowserPageVisitSchema` signals on page load for DOC72 to process. The exclusion list is a source-level filter — if a domain is excluded, DOC20 does NOT emit a signal. DOC72 never sees traffic from excluded domains.
```ts
interface BrowserExclusionListSchema {
excluded_domains: BrowserExcludedDomain[]
schema_version: 1
}
interface BrowserExcludedDomain {
domain: string // e.g., "espn.com"
added_at: string
reason: string | null // optional user note: "personal", "banking"
}
```
Storage: `ELNOR_MEMORY/browser/exclusion_list.json`.
**Matching rule:** Domain match is suffix-based. An entry for `espn.com` blocks `espn.com`, `www.espn.com`, `nba.espn.com`, and every subdomain and path under it. An entry for `mail.google.com` blocks Gmail but NOT `docs.google.com`. You can be as broad or narrow as needed.
**Emission behavior on excluded domains:**
- Does NOT emit `BrowserPageVisitSchema` to DOC72
- Does NOT write to `visits.jsonl`
- DOES still write to `history_current.json` (history is a browser function, not a knowledge function — you can still navigate back)
- Does NOT trigger M365 metadata extraction
**Decision hierarchy (evaluated in order):**
1. **Incognito active** → no emission, no history, no signal of any kind
2. **Domain on exclusion list** → no emission to DOC72, history only (for navigation)
3. **All other pages** → full emission (`BrowserPageVisitSchema` + `M365PageMetadataSchema` if applicable)
DOC72 receives the emitted signals and applies its own significance rules (skip/shallow/deep per DOC72 §20B.3 and §39). DOC20 does not decide extraction depth — it emits or doesn't emit.
Settings UI: table of excluded domains with domain, date added, reason, remove button. "Add domain" input at top. Note: "Blocking a domain blocks all its subdomains and pages."
#### 6.19.11B Incognito mode
Any tab can be opened as private. Private tabs use a non-persistent Electron webview partition (`partition="q-browser-private"` without `persist:` prefix) — cookies and storage exist in memory only, never written to disk. Switching between tabs does NOT clear the private session — the webview stays alive in memory. Session data disappears only when the specific private tab is closed or Q quits entirely.
When incognito is active for a tab:
- No history entries created
- No `BrowserPageVisitSchema` emitted to DOC72
- No visit log entries
- No comments persisted
- Ask Elnor still works (the query is processed but page context is not durably stored)
- Clippings unavailable from incognito tabs
- DOC3 demonstration mode unavailable
- Visual indicator: dark/purple tab background + mask icon on the tab
New incognito tab: `Cmd+Shift+N` or right-click the [+] tab button → "New Incognito Tab."
#### 6.19.11C Cookie controls
Settings section for cookie management:
- **Third-party cookies:** Block All / Allow All / Allow from Visited Sites. Implemented via Electron `session.webRequest` intercepting `Set-Cookie` headers with cross-origin domains.
- **Clear browsing data:** Button → modal with checkboxes: Cookies, History, Cache, Saved Form Data. Time range: Last Hour / Last Day / Last Week / All Time. Executes `session.clearStorageData()` with selected options.
- **Do Not Track:** Toggle that injects `DNT: 1` header on all outgoing requests via `session.webRequest.onBeforeSendHeaders()`. Note: most sites ignore this header, but it's the standard mechanism.
### 6.19.12 Downloads manager (R2.3)
Electron's `session.on('will-download')` intercepts all downloads.
**Settings:**
- Default download location (default: `~/Downloads`). Path picker in settings.
- "Always ask where to save" toggle.
- "Auto-save PDFs to downloads" toggle (in addition to opening in Document Viewer — see §6.19.21).
**Downloads tray:** Collapsible bar at the bottom of the browser (above the status bar) or a toolbar icon that expands. Shows active and recent downloads. Each entry: filename, file size, progress bar (for in-progress), and actions: Open, Show in Finder, Remove from List.
**Recent downloads list:** Last 50 entries stored at `ELNOR_MEMORY/browser/downloads_current.json`. Browsable from the downloads tray.
**ELNOR integration:** The configured downloads folder is registered as a Place in the Browser column (§3.12). Downloaded files automatically appear in browser results and are indexed by DocIndex. When you download a filing from PACER, it automatically appears in your Browser column and Elnor can read it.
### 6.19.13 Credential vault and autofill (R2.3)
#### 6.19.13A Credential vault
An encrypted credential store for website login credentials.
```ts
interface CredentialVaultEntry {
credential_id: string
site_domain: string // e.g., "pacer.uscourts.gov"
label: string // e.g., "PACER Login"
username: string
password_encrypted: string // AES-256 encrypted via Electron safeStorage
form_field_mappings: {
username_selector: string // CSS selector: "input[name='login']"
password_selector: string // CSS selector: "input[name='password']"
additional_fields?: {
selector: string
value: string
}[]
}
elnor_access: "allowed" | "denied" | "ask_each_time"
auto_fill: boolean // auto-fill on page load vs manual trigger
created_at: string
last_used_at: string
}
```
Storage: `ELNOR_MEMORY/credentials/vault.json`. Encrypted at rest via Electron's `safeStorage.encryptString()` / `safeStorage.decryptString()`. Encryption key is managed by macOS Keychain, tied to the user's macOS account. If someone copies `vault.json` to another machine, they get encrypted blobs they cannot decrypt.
**Elnor access permissions:** Three states per credential:
- `allowed` — Elnor can use this credential for autonomous actions (Phase 2 agent browsing). Available when Elnor needs to log into a service on your behalf.
- `denied` — only the user can trigger autofill. Elnor cannot use this credential.
- `ask_each_time` — Elnor requests a gate approval (per DOC23/EC Core §8.5) before using the credential. You approve or deny each use.
**Security model:**
```ts
interface CredentialVaultSecurity {
encryption: "electron_safeStorage" // macOS Keychain-backed
auth_required_for: [
"view_password", // eye icon reveal
"edit_credential", // modify username/password
"export_credentials", // bulk export
"delete_credential", // remove saved credential
"change_elnor_access" // modify Elnor permission
]
auth_method: "macos_password" | "touch_id"
autofill_trigger: "user_click_only" // never programmatic from web page
domain_matching: "exact_suffix" // pacer.uscourts.gov matches *.pacer.uscourts.gov only
credential_isolation: "per_domain" // pages can only trigger fill for own domain
}
```
Viewing, editing, exporting, or deleting passwords requires macOS user password or Touch ID re-authentication. Passwords display as `••••••••` with an eye toggle (auth-gated).
Websites cannot access the vault. The vault is in EC's Node.js process, isolated from the webview. Autofill injects values via IPC → `webview.executeJavaScript()`. The web page sees filled form values as if a user typed them. A page cannot query the vault, trigger fills for other domains, or read credentials for other sites. Phishing protection: domain matching is exact suffix — `pacer-login.fake.com` will NOT receive PACER credentials.
#### 6.19.13B Auto-save password prompt
When a form with a password field is submitted, Q shows a dropdown from the URL bar: "Save password for {domain}? [Save] [Never for this site]." If the site already has a saved credential and the user typed a different password: "Update password for {domain}? [Update] [Keep existing]."
"Never for this site" adds the domain to a suppression list stored in `ELNOR_MEMORY/credentials/never_save_domains.json`. Manageable in settings: "Sites that never save passwords" with remove buttons.
#### 6.19.13C Autofill behavior
When Q detects a login form (input fields with `type="password"` or `autocomplete="username"` / `autocomplete="current-password"`), it shows an autofill dropdown below the first field. Dropdown entries show: source icon (Q vault / Keychain), username, and site domain. Click to fill.
**Sources (in display order):**
1. Q Vault credentials matching the current domain
2. macOS Keychain credentials matching the current domain (via Electron `safeStorage` or `keytar`)
If `auto_fill: true` on the credential AND only one matching credential exists, Q fills automatically on page load without requiring a click. If multiple credentials match or `auto_fill: false`, the dropdown waits for user selection.
#### 6.19.13D Personal info autofill
Separate from passwords. A "Personal Info" section in settings for identity fields: name, email addresses, phone numbers, physical addresses (home/work).
Storage: `ELNOR_MEMORY/credentials/personal_info.json` (not encrypted — no secrets).
When Q detects form fields with standard HTML `autocomplete` attributes (`autocomplete="email"`, `autocomplete="street-address"`, `autocomplete="tel"`, etc.), it offers to fill from stored profile. Dropdown appears below the field showing the matching stored value.
Elnor can read personal info for context ("What's my work address?" type queries).
### 6.19.14 Extension support and ad blocking (R2.3)
#### 6.19.14A Chrome extension loading
Electron's `session.loadExtension()` loads unpacked Chrome extensions. Not all extensions are compatible — Manifest V3 service workers, `chrome.identity`, and Chrome-specific APIs may not work.
**Extension management settings page:**
- List of installed extensions with name, version, enabled/disabled toggle, Remove button.
- "Load Extension" button opens a folder picker. User selects the unpacked extension directory. Q loads via `session.loadExtension(path)`.
- Compatibility warning on install: "This extension may have limited compatibility with Q Browser."
- Extensions persist across Q restarts (paths stored in `ELNOR_MEMORY/browser/extensions.json`).
#### 6.19.14B Native ad blocking
Independent of the extension system. Q loads EasyList filter rules (same list used by uBlock Origin, freely available) and blocks matching network requests via Electron `session.webRequest.onBeforeRequest()`.
Toggle in browser settings: "Block ads and trackers: On / Off." Default: Off (user must opt in).
This runs at the Electron session level, covering all tabs including incognito tabs. More reliable than extension-based blocking because it operates at the network layer.
### 6.19.15 Reader mode (R2.3)
Toggle in the toolbar: open-book icon switching between "Page View" and "Reader View."
Reader mode extracts page content via mozilla/readability and renders it in Q's own typography — same fonts, line height, and spacing as notes. The page chrome (navigation, ads, sidebars, boilerplate) is stripped.
Reader mode content is:
- **Commentable** — full comment rail works on the cleaned text
- **Highlightable** — bubble menu works on selections
- **Exportable** — Save As… dropdown works: PDF, DOCX, Markdown, Save as Artifact, Save as Note
- **Sendable to Elnor** — the cleaned text is what EC packages as context (fewer tokens, more relevant than raw HTML)
Reader mode is the preferred extraction path for Send to Chat (§6.19.16) because the cleaned text is dramatically smaller and more relevant.
### 6.19.16 Send to Chat (R2.3)
Toolbar button (or item in page actions dropdown): "Send to Chat."
**Flow:**
1. Q extracts page content via readability (reader mode pipeline)
2. Opens the current project chat (or most recent chat, or creates a new one)
3. Inserts a reference card: page title + URL + "Page content attached" badge
4. Places cursor in chat input
5. User types question ("summarize the holding," "compare to Henderson") and presses Enter
6. Elnor sees the full cleaned page text as attached context alongside project bucket, topic capsule, standing orders, and conversation history
The page content is cached so Elnor doesn't re-fetch the URL. One click from browsing to asking.
**URL auto-extraction in chat:** When a URL is pasted into any chat message (not just Q Browser — regular chat too), EC detects the URL pattern and auto-extracts page content using the readability pipeline. If Q Browser has the page cached from a recent visit, uses cache (instant). If not, EC fetches and extracts on demand. A URL is "read this specific page" — EC routes to content extraction, not to a search agent.
### 6.19.17 Clippings to Note (R2.3)
While browsing, the user highlights text and clicks "Clip" (scissors icon in bubble menu, or `Cmd+Shift+C`). Each clip appends to a running clippings note.
**Target selection:** First clip in a session prompts: "Clip to: [New scratch note] [Select existing note]." Subsequent clips in the same session append to the selected note silently.
Each clip entry includes:
- Quoted text (as a blockquote in the note)
- Source URL and page title (as a citation line)
- Timestamp
When done collecting, the clippings note is a regular note — editable, commentable, sendable to Elnor, exportable. Elnor can see clippings as they accumulate and offer synthesis when asked ("Based on what I've collected, what are the key themes?").
"Create Note from Clippings" also available from the browser toolbar dropdown for a quick new note from all clips collected in the session.
### 6.19.18 Pin as Context (R2.3)
Right-click a tab or use toolbar action: "Pin as Context." Two options:
- **Pin to project bucket:** Extracts page content and creates a context bucket entry (DOC7) in the active project. Persistent — stays in the project until removed. Available in DOC7 context assembly for all conversations and tasks in that project. Toast: "Page pinned to {project_name} context."
- **Pin as session context:** Holds extracted page text in memory for the current work session. Available to Elnor in all conversations while pinned. Ephemeral — disappears when unpinned or Q quits. A small pin icon + count in the status bar shows how many pages are session-pinned. Toast: "Page pinned as session context (1 of 3)."
### 6.19.19 Screenshot and full-page capture (R2.3)
**Viewport screenshot:** `Cmd+Shift+S` captures the current viewport as a PNG.
Quick modal: "Copy to clipboard" (default) or "Save to downloads." Uses `webview.capturePage()` — one API call, instant.
**Full-page PDF:** "Save Full Page as PDF" in the page actions dropdown. Uses `webview.printToPDF()`. Captures the entire scrollable page as a PDF. The PDF opens in Document Viewer (§6.16) with full comment/review/Send to Elnor integration. Also saved to downloads folder.
### 6.19.20 Context menus (R2.3)
The webview's `context-menu` event provides: clicked element type, link URL, selected text, image URL, page URL. Q renders a custom context menu replacing the browser default:
| Context | Menu Items |
|---|---|
| On a link | Open, Open in New Tab, Copy Link, Copy as Reference (`@[title](web:url)`), Send Link to Elnor, Clip Link Text |
| On selected text | Ask Elnor, Comment, Clip to Note, Copy, Search for "{text}" (uses configured search provider) |
| On an image | Save Image, Copy Image, Ask Elnor about Image |
| On page (no selection) | Back, Forward, Reload, Reader Mode, Save Page as Artifact, Save Page as Note, Send to Chat, Screenshot, Save Full Page as PDF, View Page Source |
### 6.19.21 PDF interception (R2.3)
When the webview navigates to a URL returning `Content-Type: application/pdf`, Q intercepts the navigation. Instead of rendering the PDF in the webview (which would use Chrome's built-in viewer without Elnor integration), Q:
1. Downloads the file to a temp location (and to downloads folder if "auto-save PDFs" is enabled)
2. Opens it in the Document Viewer (§6.16) with full comment rail, Ask Elnor, tracked changes, version history, and Send to Agent
3. Shows a "Back to web page" button (or the browser back button works) to return to the previous URL
This means PDFs from PACER, CM/ECF, or Westlaw get the full ELNOR treatment — comments, send to Elnor for analysis, save as artifact — without leaving Q.
### 6.19.22 Signal emission and DOC72 integration (R2.3)
Q Browser emits `BrowserPageVisitSchema` (§6.19.8) on every page load, subject to the exclusion list (§6.19.11A) and incognito mode (§6.19.11B). This is DOC20's emission contract — the signal boundary between the browser surface and the intelligence layer.
**What DOC20 emits:**
- `BrowserPageVisitSchema` — every non-excluded, non-incognito page load. Includes URL, title, domain, dwell time, user interaction flags, M365 metadata if applicable.
- Dwell time is computed on navigation away (switch tabs, close tab, navigate to new URL) and written as an update to the visit record.
- User interaction flags are updated in real-time as the user comments, highlights, asks Elnor, saves, or clips.
**What DOC72 does with it (owned by DOC72 §39 and §20B.3):**
- Significance assessment (skip/shallow/deep) based on dwell time, user interactions, domain relevance, return visits
- Entity extraction during idle processing
- Knowledge graph promotion via the standard pipeline
DOC20 does not decide extraction depth. DOC20 emits; DOC72 decides.
**Cross-doc note:** DOC72 §39.4 (Domain Consent Model) currently uses an opt-in allowlist. Per user preference, this should be revised to an opt-out exclusion model: DOC20 filters at the source via the exclusion list; DOC72 processes all received signals. Relevant DOC72 sections to revise: §39.4, §39.2, §20B.3, §20A.2. See §6.19.29 cross-doc obligations.
### 6.19.23 Elnor browser awareness — WebBrowserReadSkill (R2.3)
Elnor has read access to Q Browser state via a new skill registered in DOC3:
```ts
type WebBrowserReadSkillOp =
| { op: "list_open_tabs" }
| { op: "list_bookmarks"; folder_id?: string }
| { op: "search_history"; query: string; days_back?: number }
| { op: "read_current_page" } // extracts active tab's content via readability
| { op: "read_page_by_url"; url: string } // extracts a specific tab's content (must be open)
| { op: "list_downloads"; limit?: number }
| { op: "list_credentials" } // labels and sites ONLY — never passwords
| { op: "list_clippings" } // current session clippings
| { op: "list_pinned_context" } // session-pinned pages
```
Elnor can answer: "What tabs do I have open?", "Find my Westlaw bookmark," "What did I browse yesterday related to Henderson?", "What's in my clippings?", "Which credentials do I have saved for PACER?"
Elnor NEVER has direct access to passwords. `list_credentials` returns `{ credential_id, site_domain, label, username, elnor_access }` — no password field. Access to actual credentials for autonomous login is Phase 2, gated by the `elnor_access` permission model (§6.19.13A).
### 6.19.24 Browser settings page (R2.3)
A new section in Q Settings, accessible from the gear icon in the browser toolbar or from Settings → Browser.
| Setting | Control Type |
|---|---|
| **Search & Navigation** | |
| Default search provider | Dropdown: Google, DuckDuckGo, Brave, Bing, Custom URL |
| Default zoom level | Number input (50%–200%) |
| Homepage | URL input or "Start page" |
| **Downloads** | |
| Default download location | Path picker |
| Always ask where to save | Toggle |
| Auto-save PDFs to downloads | Toggle |
| **Privacy** | |
| Block ads and trackers | Toggle (default: off) |
| Third-party cookies | Dropdown: Block All / Allow All / Allow Visited |
| Do Not Track | Toggle |
| Domain exclusion list | Table with add/remove |
| Clear browsing data | Button → modal |
| **Credentials** | |
| Saved passwords | Table: site, username, Elnor access, edit/delete (auth-gated) |
| Sites that never save passwords | List with remove |
| Personal info | Name, emails, phones, addresses form |
| **Extensions** | |
| Installed extensions | List with enable/disable/remove |
| Load extension | Button → folder picker |
| **Signal Emission** | |
| Emit browsing signals to ELNOR knowledge graph | Toggle (default: on) |
| M365 enhanced intake | Toggle (default: on) |
| **Quick Links** | |
| Start page quick links | Editable URL list (shown on new tab start page) |
### 6.19.25 Profile export/import (R2.3)
**Export:** Packages all Q Browser data into a single `.elnor-browser-profile` zip file:
- `browser/bookmarks_current.json`
- `browser/exclusion_list.json`
- `browser/tabs_current.json`
- `browser/extensions.json`
- `browser/downloads_current.json` (list only, not the files themselves)
- `credentials/vault.json` (encrypted — requires same macOS Keychain on target machine to decrypt)
- `credentials/personal_info.json`
- `credentials/never_save_domains.json`
- `system/browser_intake_settings.json`
- Browser settings (search provider, zoom, download location, privacy toggles)
Does NOT include: visit history/log (too large, machine-specific), cached pages, extension files (must be reinstalled).
**Import:** Opens the `.elnor-browser-profile` file, extracts and replaces local data. Confirmation modal: "This will replace your current browser settings, bookmarks, and credentials. Existing data will be backed up to {path}. Continue?"
**Use case:** Moving to a new computer. Export on old machine, import on new machine. Credentials require the same macOS user account (or re-entry) because the encryption key is Keychain-bound.
### 6.19.26 Commands — complete registry (R2.3)
```ts
// === BOOKMARKS ===
type WebBookmarkAddCommand = ECCommandEnvelope<{
url: string
title: string
parent_folder_id?: string | null
project_id?: string | null
}>
type WebBookmarkUpdateCommand = ECCommandEnvelope<{
bookmark_id: string
title?: string
url?: string
parent_folder_id?: string | null
project_id?: string | null
}>
type WebBookmarkRemoveCommand = ECCommandEnvelope<{ bookmark_id: string }>
type WebBookmarkFolderCreateCommand = ECCommandEnvelope<{
title: string
parent_folder_id?: string | null
}>
type WebBookmarkFolderRemoveCommand = ECCommandEnvelope<{ bookmark_id: string }> // folders use same ID namespace
type WebBookmarkImportCommand = ECCommandEnvelope<{ html_content: string }> // Chrome bookmarks HTML
// === PAGE ACTIONS ===
type WebPageSaveAsArtifactCommand = ECCommandEnvelope<{
url: string
title: string
extracted_text: string
project_id?: string | null
}>
type WebPageSaveAsNoteCommand = ECCommandEnvelope<{
url: string
title: string
extracted_text: string // converted to Tiptap JSON by EC
project_id?: string | null
}>
type WebPageSendToChatCommand = ECCommandEnvelope<{
url: string
title: string
extracted_text: string
chat_id: string | null // null = create new chat
project_id?: string | null
}>
type WebPageClipCommand = ECCommandEnvelope<{
url: string
page_title: string
quote_text: string
target_note_id: string | null // null = create new scratch note
}>
type WebPagePinAsContextCommand = ECCommandEnvelope<{
url: string
title: string
extracted_text: string
pin_mode: "project_bucket" | "session"
project_id?: string | null // required when pin_mode = "project_bucket"
}>
// === EXCLUSION LIST ===
type BrowserExclusionAddCommand = ECCommandEnvelope<{
domain: string
reason?: string
}>
type BrowserExclusionRemoveCommand = ECCommandEnvelope<{ domain: string }>
// === CREDENTIALS ===
type CredentialSaveCommand = ECCommandEnvelope<{
site_domain: string
label: string
username: string
password: string // encrypted by EC before storage
form_field_mappings: {
username_selector: string
password_selector: string
additional_fields?: { selector: string; value: string }[]
}
elnor_access: "allowed" | "denied" | "ask_each_time"
auto_fill: boolean
}>
type CredentialUpdateCommand = ECCommandEnvelope<{
credential_id: string
label?: string
username?: string
password?: string
form_field_mappings?: {
username_selector: string
password_selector: string
additional_fields?: { selector: string; value: string }[]
}
elnor_access?: "allowed" | "denied" | "ask_each_time"
auto_fill?: boolean
}>
type CredentialDeleteCommand = ECCommandEnvelope<{ credential_id: string }>
type CredentialNeverSaveDomainCommand = ECCommandEnvelope<{ domain: string }>
// === PERSONAL INFO ===
type PersonalInfoUpdateCommand = ECCommandEnvelope<{
name?: string
emails?: string[]
phones?: string[]
addresses?: {
label: string // "home", "work"
street: string
city: string
state: string
zip: string
country: string
}[]
}>
// === DOWNLOADS ===
type DownloadLocationUpdateCommand = ECCommandEnvelope<{ default_path: string }>
// === PROFILE ===
type BrowserProfileExportCommand = ECCommandEnvelope<{ export_path: string }>
type BrowserProfileImportCommand = ECCommandEnvelope<{ import_path: string }>
// === SETTINGS ===
type BrowserSettingsUpdateCommand = ECCommandEnvelope<{
default_search_provider?: string
default_zoom_level?: number
block_ads?: boolean
third_party_cookies?: "block_all" | "allow_all" | "allow_visited"
do_not_track?: boolean
auto_save_pdfs?: boolean
always_ask_download_location?: boolean
emit_browsing_signals?: boolean
m365_enhanced_intake?: boolean
}>
```
**Error table:**
| Command | Possible Errors |
|---|---|
| `WebBookmarkAddCommand` | `UNKNOWN_ERROR` |
| `WebBookmarkRemoveCommand` | `BOOKMARK_NOT_FOUND` |
| `WebBookmarkFolderCreateCommand` | `UNKNOWN_ERROR` |
| `WebPageSaveAsArtifactCommand` | `UNKNOWN_ERROR` |
| `WebPageSaveAsNoteCommand` | `UNKNOWN_ERROR` |
| `WebPageSendToChatCommand` | `UNKNOWN_ERROR` |
| `WebPageClipCommand` | `NOTE_NOT_FOUND` |
| `WebPagePinAsContextCommand` | `PROJECT_NOT_FOUND` (when pin_mode = project_bucket) |
| `BrowserExclusionAddCommand` | `UNKNOWN_ERROR` |
| `CredentialSaveCommand` | `UNKNOWN_ERROR` |
| `CredentialUpdateCommand` | `CREDENTIAL_NOT_FOUND` |
| `CredentialDeleteCommand` | `CREDENTIAL_NOT_FOUND` |
| `BrowserProfileExportCommand` | `UNKNOWN_ERROR` |
| `BrowserProfileImportCommand` | `UNKNOWN_ERROR` |
### 6.19.27 Non-goals (deferred to future iterations)
- **Agent-driven browsing.** Elnor cannot autonomously navigate, click, or fill forms in Q Browser. The user browses; Elnor assists via Ask/Comment/Send/Clip. Autonomous browsing (Playwright/Puppeteer headless) is tracked in DOC16.
- **M365 / Gmail deep interaction.** For structured interaction with M365 (sending email, editing documents, managing calendar), use the dedicated API integrations in DOC16 Entry 16.7, not browser automation. Gmail/Google Workspace API integration is a separate DOC16 future item. Q Browser reading M365 web pages provides passive intake (§6.19.8); DOC16 provides active structured access.
- **Bookmarks bar.** Bookmarks are in the sidebar and start page. No persistent bar below the URL bar (preserves page space).
- **Download manager with resume/retry.** Downloads use Electron's default behavior. No partial download resume, no concurrent download management.
- **Screenshot annotation.** Viewport capture is instant PNG. No in-app drawing, arrows, or text overlay tools.
- **Web notifications API.** Websites requesting notification permission is blocked by default. Future: allow per-site with user approval.
### 6.19.28 Cross-doc obligations (R2.3)
| Target | Obligation |
|---|---|
| DOC16 | Track: "Electron desktop shell for Q" as infrastructure enabler. Track: "Agent-driven browsing (Playwright/headless)" as future capability. Track: "Gmail / Google Workspace API integration" as future DOC16 entry parallel to Entry 16.7. |
| DOC21 | Add Q Browser surface description with tab bar, URL/search bar, privacy controls, credentials. Globe icon nav item (Electron-only). |
| DOC22 | Add PAGE 16: Q Browser with full layout, tab bar, URL bar, webview, Ask Elnor panel, bookmark sidebar, start page, downloads tray, incognito indicator. |
| DOC72 | §39.4 Domain Consent Model: revise from opt-in allowlist to opt-out exclusion list. DOC20 filters at source via exclusion list; DOC72 processes all received signals. Revise §39.2 (remove "approved domain" qualifier), §20B.3 (remove "non-approved domain → skip" rule), §20A.2 (remove "approved domains" from dwell timer description). |
| DOC72 | §20B.3: Add `M365PageMetadataSchema` as enhanced intake signal for M365 web surfaces. |
| DOC72 | §39.3: Credential management — Elnor knows about credentials (domains, usernames, labels, permissions) but defers secret storage to Q Browser vault with macOS Keychain encryption. Read access via `WebBrowserReadSkill.list_credentials`. |
| DOC3 | Register `WebBrowserReadSkill` with operations listed in §6.19.23. Future: `WebBrowseSkill` for agent-driven browsing (Phase 2). |
| DOC3 | Cross-reference Q Browser as DOC3 R11.3 demonstration mode target with CDP as preferred observation mechanism for web-based workflow learning. |
| DOC7 | "Pin to project bucket" (§6.19.18) creates a DOC7 context bucket entry from extracted web page content. Verify bucket creation contract alignment. |
## 6.20 Q Unified Workspace Shell (R3)
### 6.20.1 Overview
The Q Unified Workspace is the primary interface for the entire Q app. It evolves the workspace from a "three-view" model (notes, documents, web) into a universal shell where any content type can be opened in tabs. The browser column becomes the main navigation surface, replacing the traditional left sidebar menu.
The workspace layout from left to right:
1. **Left nav rail** (44px fixed) — minimal toggle bar
2. **Browser column** (200–450px, resizable) — 4-mode navigation and search
3. **Main content area** (flex) — tabbed workspace with toolbars
4. **Ask Agent / Comments panel** (240–450px, togglable, resizable) — right panel within workspace
5. **Right chat column** (320px, togglable) — independent persistent chat at app edge
### 6.20.2 Left nav rail
The left nav rail contains exactly 6 items (R4 — updated):
```
┌──────┐
│ Q │ ← Q logo (26×26px, accentBtn bg, white text)
│ │
│ ⎕ │ ← Browser column toggle (⌘B) — Sidebar icon
│ ⫼ │ ← Split View toggle — Columns icon. See §6.24.
│ ⚡ │ ← Quick Command / Floating Palette toggle (⌥Space) — Zap icon. See §6.20.30.
│------│ ← Divider (20px wide, #ffffff15)
│ ⚙ │ ← Settings — opens utility tab
│ │
│ flex │ ← Spacer
│------│ ← Divider
│ 💬 │ ← Right chat column toggle (bottom)
└──────┘
```
- Width: 44px fixed
- Background: bgSidebar (#131820)
- Icons: 15px, color #777 inactive / #fff active
- Buttons: 32×32px, borderRadius sm, no border
- Quick Command button tooltip shows `(⌥Space)`
Everything else is accessed through the browser column's Nav tab or the [+] dropdown.
### 6.20.3 Browser column modes
The browser column has **four** mode tabs at the top:
1. **Nav** (compass icon) — app navigation menu. See §6.20.4–6.20.7A.
2. **Browser** (search icon) — full scope/type/collection browser. Unchanged from §3.
3. **Notes** (file-text icon) — note folder tree + all-notes list with scope toggles (Notes/To Do/Cal). See §6.20.8.
4. **Web** (globe icon) — bookmarks + recent pages. See §6.20.9.
**Browser column tab styling (R4):**
- Label font: 11.5px
- Active tab: fontWeight 700, panel background, subtle border outline, rounded top corners, negative margin overlap with content area
- Inactive tab: lighter background, no border, lower weight
**Auto-switching:** When the active tab changes, the browser mode auto-switches:
- Note or clips tab → Notes mode
- Web tab → Web mode
- Chat or utility tab → Nav mode
- Doc tab → Browser mode
- To-do tab → Notes mode (with To Do scope active) (R4)
The user can always manually switch to any mode. Auto-switch is a convenience, not a lock.
### 6.20.4 Nav tab — Chats section (R4 — renamed from "Conversations")
At the top of the Nav tab, show a "CHATS" header (uppercase, 10px, textTer) with a list of 5–8 recent chats and rooms.
Each conversation row shows:
- Star icon (warn color) if starred — sorts to top
- Color dot (6px) matching conversation color
- Conversation name (11.5px, fontWeight 400)
- Time ago (9px, textTer)
Clicking a conversation opens it as a persistent chat tab. Below the list: "See all →" link (10.5px, accentBtn color) that opens the Chats management page as a utility tab (see §6.20.18).
### 6.20.5 Nav tab — Activity link
Below Chats, show an "Activity & Notifications" row with bell icon, label (12px, fontWeight 500), and red badge with count. Clicking opens Activity as a utility tab.
### 6.20.6 Nav tab — Pages section
Below Activity, show a "PAGES" header with a vertical list of 9 navigation links:
| Page | Icon | Opens as |
|---|---|---|
| Tasks | Subtask (Tabler) | Utility tab |
| Projects | Folder | Utility tab |
| Knowledge | Database | Utility tab |
| Forums & Panels | MsgCircle | Utility tab |
| Agents | User | Utility tab |
| Skills & Connectors | Plug (Lucide) | Utility tab |
| Overlays & Prompts | Layers | Utility tab |
| Context Buckets | Bucket (Tabler) | Utility tab |
| Settings | Settings (gear) | Utility tab |
Each row: 6px 10px padding, 12px fontSize, fontWeight 400, icon 13px (SVG via `NavIcon` component). Hover: bgInput background.
### 6.20.7 Nav tab — Open Tabs section
Below Pages, show an "OPEN TABS ({count})" header with a list of all currently open tabs:
- Tab icon (SVG via `TabIcon` component, 10px)
- Tab title (11px, fontWeight 400 / 600 if active)
- Active tab: accentBtn+"08" background, accentBtn color
Clicking switches to that tab. This is a quick tab switcher for when the tab bar has too many tabs to read.
### 6.20.7A Nav tab — Tab Groups section (R4)
Below Open Tabs, show a "TAB GROUPS ({count})" header, collapsed by default. Click to reveal saved tab groups with count badge. Each group row shows the group color dot, group name, and tab count. Click to restore/open that group. Tab groups auto-persist on creation and modification — no explicit "Save Group" action.
### 6.20.8 Notes browser mode (R4 — updated from R3)
The Notes browser mode has scope toggles and a two-section layout.
**Scope toggles (R4):** Three toggle buttons below the mode tabs: **Notes** | **To Do** | **Cal**. State tracked in `noteBrowserScope: Set<"notes"|"todo"|"cal">` — multiple scopes can be active simultaneously.
- When "Notes" is active: shows notes in the folder tree and flat list (default behavior).
- When "To Do" is active: shows to-do lists in the flat list with a teal "To Do" type badge.
- When "Cal" is active: shows calendar items in the flat list with an amber "Cal" type badge.
- Type badges are small colored labels (transparent background, text colored by type).
**Draggable splitter (R4):** Between the folder tree and the flat item list. State: `noteSplitterPos` (default 140px). User drags to resize.
**Comment count indicators removed (R4).** Note list items no longer show comment counts.
**Top: Folder tree** (bounded by splitter position, scrollable, flexShrink 0)
- Folder rows: chevron + folder icon + title (11.5px, fontWeight 600) + item count
- Hover reveals: + (subfolder), ✏️ (rename inline), 🗑 (delete with Yes/No)
- Folders are draggable for reorder
- Deleting a folder removes the alias only — notes are kept (moved to unfiled)
- New root folder button in header: "+ New"
- Subfolder creation: inline input appears indented below parent
**Divider: "All Notes · {count}" + sort dropdown**
- Independent sort dropdown (Modified / Alphabetical / Created)
- This sort is independent of the folder area sort
**Bottom: Flat all-notes list** (flex 1, scrollable)
- Single-line rows, 30px height
- Format: [pin icon] title (11.5px, fontWeight 400) → [time]
- Right-click: Archive, Delete
- Click: opens note/to-do/calendar in tab
Search bar at top with clear button. "+ New" note button in header.
### 6.20.9 Web browser mode — Bookmarks (R3 — updated from §6.19.9)
The Web browser mode provides full Chrome-like bookmark management.
**Layout:**
- Search bar (top) with clear button — filters across all folders
- "Bookmarks" header with "+ Folder" button
- Folder tree (scrollable)
- Draggable splitter between folder tree and items (`bmSplitterPos`, default 180px) (R4)
- "Recent Pages" section (separate from saved bookmarks)
- Footer: bookmark count + folder count
**Bookmark folders:**
- Expand/collapse with chevron
- Hover reveals: + (subfolder), ✏️ (rename inline), 🗑 (delete with Yes/No confirmation)
- Item count badge
**Bookmark items:**
- Favicon squares: 14px colored rounded squares (3px borderRadius) with domain initial letter in white
- Color derived from domain (production: extracted from actual favicon or hash-generated)
- Hover reveals: ✏️ (rename inline), ✕ (delete)
- Click opens in web tab
**Recent Pages section:**
- Below bookmarks, separated by border
- Shows recently visited pages with favicon square, title, time ago
- Not editable — auto-populated from browsing history
### 6.20.10 Tab system — content types
Tabs can hold any content type. The tab system recognizes (R4 — icons migrated from emoji to SVG string IDs via `TabIcon` component, colors updated):
| Type | Icon ID | SVG Source | Color | Behavior |
|---|---|---|---|---|
| `note` | `note` | FileText (Heroicons) | #31588c | Full note toolbar + block editor |
| `doc` | `doc` | File (Heroicons) | #2E8B57 | Full document viewer toolbar |
| `web` | `web` | Globe (Heroicons) | #64748B (R4 — was #D97706) | URL bar + web content. Favicon in tab when page loaded. |
| `clips` | `clips` | Scissors (Lucide) | #7C3AED | Same as note (unified renderer) with purple accent |
| `chat` | `chat` | MsgCircle (Heroicons) | #1a5276 (R4 — was #2E8B57) | Chat conversation with message list + input |
| `room` | `room` | Users (Heroicons) | #6366F1 | Multi-agent room (same layout as chat) |
| `task` | `task` | List (Heroicons) | #D97706 | Task detail view |
| `todo` | `todo` | ListTodo (Lucide) | #0891B2 (R4 — new) | Standalone to-do list. See §6.21. |
| `utility` | varies | varies | #6B7280 | Transient configuration/management pages |
**Tab icon format (R4):** The `icon` field in `initTabs` and `openTab()` calls uses string IDs (e.g., `"note"`, `"doc"`, `"web"`) consumed by the `TabIcon` component via `tabIconMap` lookup. This replaces the former emoji string format. This is a data format change that affects tab serialization.
**Web tab favicons (R3):** Web page tabs show a favicon — a colored square with the domain initial letter, matching the bookmark favicon system. Globe icon is the fallback for tabs that have not loaded a page. In production, actual favicons will be extracted.
### 6.20.11 Tab system — transient utility tabs
When a Pages item from the Nav tab (or [+] → Open) is clicked, it opens as a **transient utility tab**.
Transient utility tab rules:
1. **Visual distinction:** Blue outline border (`1px solid accentBtn` at 30% opacity inactive, 60% active). No colored type underline.
2. **Leftmost insertion:** New utility tabs are inserted at position 0 in the tab array (leftmost). User can drag to reorder.
3. **Persist on navigate:** Utility tabs do NOT auto-close when the user switches to other tabs. This is a deliberate correction from earlier design exploration.
4. **One at a time:** Only one utility tab can exist at a time (unless pinned). Opening a new utility page replaces the current unpinned utility tab.
5. **Pinning:** Right-click → "Pin (keep open)" converts a utility tab to a persistent tab. Pinned utility tabs are no longer replaced.
6. **No "transient" label:** The blue outline is the only visual indicator. No badge in the content area.
### 6.20.12 Tab system — duplicate prevention
When `openTab()` is called:
1. Check if a tab with the same `title` and `type` already exists
2. If yes: switch to the existing tab, show toast "Already open — switched to tab"
3. If no: create the new tab normally
### 6.20.13 Tab bar styling (R4 — Chrome-style redesign)
The tab bar was completely redesigned in R4 to match Chrome's visual language.
```
Tab bar background: #e8eaed
Tab bar gap: 0
Tab bar padding: 4px 4px 0 4px
Active tab bg: c.bgPanel (white)
Active tab border: borderRadius "8px 8px 0 0"
Active tab bottom: 2px solid c.bgPanel (white — seamless connection to content)
Active tab zIndex: 2
Active tab minWidth: 28
Inactive tab bg: #dfe2e6
Inactive tab bottom: 2px solid ${c.border}50
Inactive tab right: 1px solid ${c.border}30 (faint separator)
Inactive text color: #585c60
Inactive marginRight: 1
All tabs flex: "1 1 0" (proportional shrinking)
All tabs padding: "0 2px 0 5px"
All tabs height: 32
All tabs gap: 3
Selected tab bg: #d0d5db (Cmd+click multi-select)
Incognito active bg: #4a4a4a
Incognito inactive bg: #555555
Utility tab border: 1px solid accentBtn (30% inactive, 60% active)
```
**Key design principle:** Active tabs are white and seamlessly connected to the content area below — no bottom border or gap. Inactive tabs are slightly darker gray with subtle right-side separators. All tabs (grouped and ungrouped) are direct flex children of the tab bar, ensuring uniform proportional shrinking when the window narrows.
**Browser toggle button:** Sidebar panel icon (vertical divider in rectangle). Flush left, no background, 26×36px, 3px marginRight to first tab. Same background as tab bar.
### 6.20.14 Tab groups (R4 — Chrome-style redesign)
Tab groups use Chrome-style visual language:
**Architecture:** Group wrappers use `display: "contents"` so all tabs (grouped and ungrouped) are direct flex children of the tab bar. This ensures uniform proportional shrinking regardless of grouping.
**Group label:** Tab-shaped container with inner colored pill (`borderRadius: 4`, white text, 11px, fontWeight 450). `flex: "0 0 auto"`, `paddingRight: 3` (border extends through padding to connect with adjacent tab borders). Same shape whether collapsed or expanded.
**Grouped inactive tabs:** Background `group.color + "12"` (very light tint), `2px solid ${group.color}` bottom border, `1px solid ${group.color}25` right border.
**Grouped active tab:** `2px solid ${group.color}` on top, left, and right edges; `2px solid c.bgPanel` bottom (white). Creates a continuous colored outline (bottom → up left → across top → down right) that visually frames the active tab.
**Collapse/expand:** Click group label to toggle. Collapsed groups show only the colored label pill.
**Group context menu (right-click group label):**
- Inline rename input field
- 10-color picker dots
- New Tab in Group
- Ungroup All
- Close Group
- (separator)
- Delete Group
**Auto-persistence (R4):** Tab groups auto-persist on creation and any change. No explicit "Save Group" action. Saved groups appear in the Nav tab's Tab Groups section (§6.20.7A) and at bottom of [+] dropdown under "Saved Groups."
**Create:** Cmd+click to multi-select tabs → right-click → "Group Selected Tabs…" → name prompt → auto-color from palette. Or via tab context menu → "Add to Group" submenu with existing groups + "New Group."
### 6.20.15 Tab context menu (R4 — updated)
Right-click any tab shows:
| Action | Condition | Behavior |
|---|---|---|
| Close | Always | Closes tab |
| Close Others | Always | Closes all except this tab |
| Close to Right | Always | Closes all tabs to the right (R4) |
| Duplicate | Always | Creates a copy |
| Pin | Always | Pins tab |
| Copy | Always | Writes tab title to clipboard |
| Rename | Note or clips tab | Makes title editable inline |
| Pin (keep open) | Utility tab, not pinned | Converts to persistent |
| Move to Split Pane | Split view active (§6.24) | Moves tab to other pane (R4) |
| Add to Group | Always | Submenu: existing groups + "New Group" (R4) |
| Group Selected Tabs… | Multi-select active | Groups selected tabs |
| Remove from Group | Tab is in group | Removes from group |
Tab drag-and-drop for reordering is supported.
### 6.20.16 [+] dropdown (R4 — updated)
The [+] button at the right end of the tab bar opens a dropdown with three sections:
```
┌──────────────────────┐
│ CREATE │
│ [FileText] New Note │
│ [ListTodo] New To Do │ ← R4
│ [MsgCircle] New Chat │
│ [Globe] New Web Tab │
│ [🕶] Incognito Tab │
│──────────────────────│
│ OPEN │
│ [MsgCircle] Chats │
│ [Subtask] Tasks │
│ [Folder] Projects │
│ [Database] Knowledge │
│ [User] Agents │
│ [Settings] Settings │
│──────────────────────│
│ SAVED GROUPS │ ← only if savedGroupsList.length > 0
│ ● Paramount Research │
│ ● Brooge Review │
└──────────────────────┘
```
Each option shows an SVG icon (via `I` component) + label + type color. Create items open persistent tabs. Open items open utility tabs. The dropdown opens leftward (`right: 0`) when the button is far right to prevent overflow.
### 6.20.17 Right chat column
The right chat column is a **separate column** from the Ask Agent panel. It renders at the app's far-right edge.
- Width: 320px, collapsible via left nav rail toggle
- Header: "Chat" label with MsgCircle icon and close button
- Chat selector: dropdown showing current conversation name with color dot
- Message area: scrollable, messages with avatar (18px), author name, time, body
- Input bar: text input + send button (accentBtn, Spark icon)
The Ask Agent panel opens WITHIN the workspace content area (between main content and chat column). The chat column is always at the app edge. Both can be open simultaneously.
### 6.20.18 Chats management page
The Chats page opens as a utility tab. It provides chat list management.
**Layout:**
- Search bar (top) — searches chat text
- Filter pills: All | Starred | Active | Archived
- Conversation list: star toggle, color dot, name (13px fontWeight 550), preview text (11px textTer), timestamp, project badge
Clicking a conversation opens it as a persistent chat tab. Starring makes it appear in Nav tab's Chats section.
### 6.20.19 Ask Agent panel (R4 — updated)
The Ask Agent panel is the unified interface for interacting with the agent about the current tab's content. It opens as a tab in the right panel alongside Comments.
**Layout (top to bottom, R4 — simplified):**
1. **Context card** (collapsible) — tab icon, title, context reference line. Collapses after first send.
2. **Agent selector** — compact dropdown with letter-badge avatar, agent name, chevron. Same row as "Send In" selector.
3. **Send In** — Inline / New Chat / Existing Chat (R4 — replaces prior Output mode + Send in split)
4. **"Send with"** (R4 — replaces "Context scope") — Document only / All comments + referenced portions / Select comments
5. **Selected comments** — when "Select comments" is chosen, checkboxes per comment
6. **Additional instructions** — single optional textarea (R4 — simplified from dual Instruction/Output mode)
7. **Include session clips** — single checkbox (R4)
8. **Attachment** — Paperclip icon + "Attach" button
9. **Send button** — primary
10. **Inline agent response** — appears below send button after agent responds
**Per-tab right panel state (R4):** Right panel state resets on tab switch. Utility tabs: panel auto-closes. Content tabs: panel closes, can reopen fresh.
**Inline agent response:**
After the user sends a question, the agent's response appears below the send button:
- Agent avatar (20px) + name + timestamp
- Response text (12.5px, lineHeight 1.6)
- Divider
- "Continue in full chat →" link — opens a chat tab with the exchange as starting context
### 6.20.20 Clips = Notes unification (R3)
Clips are notes. There is no separate clips renderer, toolbar, or block system.
**What makes a clips note different from a regular note:**
- Purple accent color (#7C3AED) instead of blue
- Auto-populated with clip blocks when content is clipped from web or document tabs
- Title follows session naming convention (see §6.20.22)
- Auto-created on first clip in a session (see §6.20.23)
**Clip blocks:** Each clip is a styled block within the note:
- Purple left border (3px solid purple)
- Purple-tinted background (purple+"04")
- Header: scissors icon + source name (e.g., "Sanli Report.pdf") + time + delete (X) button
- Body: clipped text content
**Clipping sources:** Clipping works from both web tabs (Clip button in toolbar + bubble menu) and document tabs (Clip button in toolbar + bubble menu). The doc toolbar must include a Clip button (§6.16.4).
All note features work on clips: toolbar, block modules (+Module picker), tracked changes, comments, Send to Agent, export, search, folder assignment.
### 6.20.21 Incognito tabs (R3 — updated from §6.19.11)
Incognito tab visual treatment:
- **Tab:** Dark gray background (#4a4a4a active, #555 inactive). Mask icon instead of page favicon. White text.
- **Page content:** Renders identically to regular pages. No purple/dark theming.
- **Banner:** Subtle gray bar at top: "Incognito — no history, no signals emitted" with Mask icon. Gray background (#f0f0f0), textSec color.
- **URL bar:** Normal styling. Mask icon instead of Lock icon.
### 6.20.22 Session naming convention
Session clip notes use the pattern `Clips: {M.D}-{N}`:
- M = month number (no leading zero)
- D = day number (no leading zero)
- N = sequence number for that day (starting at 1)
- Period between date parts, dash before sequence number
Examples: "Clips: 4.3-1" (first session April 3rd), "Clips: 4.3-2" (second that day), "Clips: 4.4-1" (first April 4th).
### 6.20.23 Session lifecycle
**Auto-create:** When Q launches, if no active session exists, one is created automatically. No user action needed. A clips note is created in the Session Notes folder and opens as a tab when the first clip is made.
**Close triggers:**
1. Quit Q
2. Click "New Session" in the status bar
3. 2 hours of idle (no mouse/keyboard activity)
**On close:** The clips note persists forever as a regular note in the Session Notes folder. It is never deleted. Starting a new session creates a new clips note with an incremented sequence number.
### 6.20.24 Status bar (R4 — updated)
The status bar at the bottom of the workspace shows:
```
[clock] 0h 47m · [scissors] 2 clips · 5 tabs · [download] 2 · Saved 2m ago · ──────── New Session · [−][━━━━][+] 100%
```
- Font: 11px
- Padding: 5px
- Status bar spans full workspace width (not under right panel)
Items:
- Session timer (time since session started)
- Clip count (clips in current session)
- Tab count
- Downloads count (clickable, shows downloads tray)
- "Saved Xm ago" (green, only for note/clips tabs)
- "New Session" link (accentBtn, right-aligned)
- **Zoom slider (R4):** Word-style — minus button, range slider, plus button, percentage label. Click the percentage label → reset to 100%. Range: 50% to 200%. Implementation: CSS `transform: scale()` on content area with width compensation to fill available space.
**No project indicator** in the status bar. Browsing does not auto-assign to projects.
### 6.20.25 Downloads tray
A collapsible bar above the status bar showing active/completed downloads:
- Download icon + file entries: name, progress bar or ✓, size, "Open" link
- Close button (X) to collapse
- When collapsed, shows as count in status bar
### 6.20.26 Design tokens — font
Working/mockup font: **IBM Plex Sans** (Google Fonts, weights 300–700).
Production font: **Söhne** (Klim Type Foundry, licensed) or final selection TBD.
Fallback chain: `'IBM Plex Sans', 'Helvetica Neue', -apple-system, BlinkMacSystemFont, sans-serif`
Font weight mapping:
- 400: regular text, list items, labels
- 500: medium emphasis
- 550: active/selected items
- 600: headers, active states, bold labels
- 650: section headers, strong emphasis
- 700: titles, primary headings
### 6.20.27 Bubble menu positioning
The bubble menu (appears on text selection in notes, documents, web) is positioned relative to the content area container (position: relative on editorRef). It is clamped to stay within the content area boundaries and cannot overflow into the browser column or right panel.
### 6.20.28 Workspace mockup inventory (R4 — updated)
| File | Lines | Size | Key content |
|---|---|---|---|
| **Q_UNIFIED_WORKSPACE_V7_11.jsx** | ~700+ | ~200KB+ | **Current operative mockup.** Chrome-style tab bar, tab groups with `display:contents`, floating palette V2 with sidebar, unified to-do system, calendar module, SVG icon system, split view, notification inbox, pin toggle, zoom slider, bookmarks bar, per-tab right panel state. (R4) |
| Q_UNIFIED_WORKSPACE_V7_3.jsx | 654 | 174KB | Prior R3 baseline. Superseded by V7.11. |
### 6.20.29 Cross-doc obligations (R4 — updated)
| Target Doc | Obligation |
|---|---|
| DOC21 (Master UI Spec) | Add: Icon system spec (Ic/I/TabIcon/NavIcon components), Chrome-style tab bar spec, tab group spec (display:contents, colored outlines), notification card spec, sidebar panel spec (palette), calendar module spec, split view spec, zoom slider spec, bookmarks bar spec. Update: left nav rail (split view + palette buttons), tab types (todo added). |
| DOC22 (Page Inventory) | Add page entries for: Notices (palette sidebar view), Activity (palette sidebar view), Key Commands (utility page). Remove: "⌘ Command" tab entry. Update: all icon references from emoji to SVG icon IDs. |
| DOC72 (Hyper Intelligence Overlay) | **Surface intake contracts (R4.1 — expanded).** (1) To-do intake (§6.21.7): `obligation` nodes from tasks with deterministic structured fields + entity resolution on list names and task text; `work_product` nodes from lists; `subtask_of`, `belongs_to_list`, `references_document` edges; trigger on every CRUD command. (2) Calendar intake (§6.22.8): `obligation` nodes from events with `event_type` categorization, entity resolution on title/location/participants/notes, `synced_to_calendar` edges to calendar source `world_entity` nodes; trigger on every CRUD and Outlook sync. (3) Notes intake (§6.22.9 — gap): LLM-assisted extraction contract needed — trigger, significance gate, extraction prompt, output nodes. (4) Browser intake (§6.19.22 + separate proposal): two-tier pipeline with significance filtering. See DOC72 Proposal: Surface Intake Contracts for full specification. |
| DOC24 (Unified Knowledge/Capability) | "Ask" button wiring: to-do list → chat context → EC → agent. Notification delivery architecture: EC notification store → palette inbox rendering. |
| DOC23 (Task System) | Tasks open as tabs. Task detail view layout. Task management utility page. |
| DOC12 (Inter-Agent Communication) | Chats and rooms open as tabs. Chat tab content layout must align with DOC12 conversation model. |
| DOC16 (M365 Integration) | Calendar module Outlook sources connect via DOC16 Entry 16.7. Calendar discovery, sync, shared calendar indicators. |
| DOC3 (App Skills) | "Watch" quick action in palette sidebar triggers DOC3's demonstration/observation mode. |
| DOC20 §6.18.2 (Content Registry) | Register new content types: `todo_list`, `calendar_event`, `quick_command_chat`. Register module presets under `ELNOR_MEMORY/task_system/module_presets/`. |
| Q Dashboard Spec | Update for: Chrome-style tabs, SVG icon system, split view, floating palette, notification system, zoom slider, IBM Plex Sans font. |
### 6.20.30 Floating Palette (R4 — new)
The floating palette is a dark-themed overlay window that functions as a complete mini control surface. It is designed so a user working in another app can press one hotkey, check notifications, fire off a quick chat, check their to-do list, and dismiss it — without ever opening the full Q Dashboard.
#### 6.20.30A Palette — Window and Toggle
- **Toggle:** ⚡ button in left nav rail, or ⌥Space keyboard shortcut
- **Electron:** Separate `BrowserWindow`, `alwaysOnTop: true` (toggleable via pin), `frame: false`
- **Base dimensions:** 560px wide main panel. When sidebar is open: 730px total (170px sidebar + 560px main).
- **Theme:** Dark (#1a1d23 background), 12px border-radius corners, heavy box shadow
- **Close:** X button on far right of tab bar
#### 6.20.30B Palette — Tab Bar
The palette tab bar contains (left to right):
```
[☰] [Chat] [Note] [To Do] [📌] [⚙] [X]
```
- **☰ Sidebar toggle** (far left): Opens/closes the sidebar. Red badge when unread notifications exist.
- **Chat / Note / To Do tabs:** Pill-style buttons with accent highlight on active tab. States are preserved across tab switches — switching tabs never reloads content.
- **📌 Pin button** (Lucide pin/thumbtack): Toggles Electron `win.setAlwaysOnTop(true/false)`. When pinned: icon highlights with accent color + tinted background. (R4 — future: opacity reduction when unfocused + pinned.)
- **⚙ Settings gear:** Shows model/think level.
- **X Close button** (far right): Closes/hides the palette.
#### 6.20.30C Palette — Sidebar
The sidebar slides out from the LEFT edge of the palette. Main panel stays anchored at 560px; total width becomes 730px.
**Sidebar container:** Own bordered panel, `borderRadius: "12px 0 0 12px"`, left shadow, `flexShrink: 0`, 170px width.
**Sidebar contents (top to bottom):**
1. **Command bar** — text input, placeholder "Command…", no icon. Searches commands and quick actions.
2. **Section links:**
- **Notices** (Bell icon, with red unread badge) — opens notification inbox in main pane (§6.25)
- **Activity** (Spark icon) — opens activity feed in main pane (§6.20.30G)
3. **Divider**
4. **Quick Actions header** (+ "customize" button):
- Watch — triggers DOC3 demonstration mode
- Run procedure — launches standing procedure selector
- New note — creates a new note and opens in palette Note tab
- Search knowledge — opens DOC72 knowledge search
- Key Commands — opens key commands utility page
5. **Bottom:** Mute toggle (Bell icon + "Mute"/"Silent" label) + hotkey indicator showing current shortcut
**When a section is selected (Notices or Activity):** Content renders in the main pane area (same space as tab content). Tab bar tabs temporarily become inactive but their state is preserved. Click any tab → dismiss section view, return to tab content.
#### 6.20.30D Palette — Chat Tab
**Chat selector dropdown** at top of chat pane — follows the **universal content selector pattern** (§6.20.30J):
- Up to 8 recent chats, ordered by recency
- Starred chats pinned to top (star icon, warn color)
- Each row: color dot + chat title + time ago
- "New chat" option at top (creates new chat, loads in palette)
- "Browse all…" link at bottom — opens Chats management page as utility tab in full workspace
- Chat renaming inline (click title → editable, Enter to save)
**State:** `fpSelectedChatId` tracks which chat is loaded in the palette. Changing selection loads that chat's messages.
**Message rendering:**
- Avatar component (`Av`) + author name + timestamp + message body
- Attach button (+) for files
- Send arrow button (I.Send icon)
- Screen capture toggle: red camera icon, shows "Screen capture on" text indicator when active
- "Open in Q ↗" link — opens chat in full workspace tab
**Chat persistence:** `chat_type: "quick_command"`, visible in Q chat list with ⚡ indicator. Screen capture: DOC3 §4.3 screenshot adapter, single-frame mode, ephemeral 24h.
**R4 cleanup:** "New Thread" link has been removed (new chats available via the chat selector dropdown). Empty footer div removed. Screen capture indicator only shows when capture is active. Input padding reduced.
#### 6.20.30E Palette — Note Tab
**Note selector dropdown** at top — follows the **universal content selector pattern** (§6.20.30J):
- Up to 8 recent notes, ordered by last modified
- Each row: note icon + title + time ago
- "Create new…" option at top (blank note or from template)
- "Browse all…" link at bottom — switches to Notes browser mode in full workspace
- Note renaming inline (click title → editable, Enter to save)
**Toolbar:** Bold, Italic, Underline buttons.
**Auto-save indicator.**
**"Ask" button** at bottom — sends note content to agent.
#### 6.20.30F Palette — To Do Tab
**To-do list selector dropdown** at top — follows the **universal content selector pattern** (§6.20.30J):
- Up to 8 recent to-do lists from shared `fpTodoLists` pool (§6.21), ordered by last modified
- Each row: ListTodo icon + list name + task count + time ago
- "Create new list" option at top (name input inline)
- "Browse all…" link at bottom — switches to Notes browser mode with To Do scope toggle active in full workspace
- List renaming inline (click title → editable, Enter to save)
**State:** `fpActiveTodoList` tracks which list is selected.
**Task list rendering:** Same rendering as standalone to-do tab (§6.21) — checkboxes, expand toggles, subtasks, done section, date/reminder popover. The rendering is identical between the palette and the main workspace — same component, same data pool, same interactions.
**Footer:** "Ask" button — sends full list as context to agent. See §6.21.5 for wiring.
#### 6.20.30G Palette — Activity Feed
Activated from sidebar → Activity link. Renders in main pane.
- Chronological feed: icon + description + timestamp per entry
- Sources: agent actions, system events, calendar flags, task completions
- Click any entry → "Open in Q" (navigates to relevant tab in full workspace)
- Read-only awareness surface
#### 6.20.30H Palette — Key Commands System
- Key capture mode: press to record shortcut combo for palette toggle
- `fpCapturedKey`: stores current palette toggle shortcut (default: "⌘Space" or "⌥Space")
- `fpKeyCapture`: boolean for capture overlay state
- Capture overlay: "Press your shortcut combo" + example display + Cancel button
- Quick Actions list shows current shortcut next to toggle command
- Footer shows hotkey hint
#### 6.20.30I Palette — Silent Mode
- Bell icon at sidebar bottom, "Mute"/"Silent" label
- Suppresses: sounds, glow, auto-show behavior. Badge counts still update.
- Visual indicators when active: red bell icon in sidebar, "Silent" text in Notices section header
#### 6.20.30J Universal Content Selector Pattern (R4)
A consistent dropdown selector pattern is used wherever the user picks a chat, note, or to-do list — in both the floating palette and the main workspace. The pattern is the same regardless of surface:
```
┌──────────────────────────────┐
│ + Create new {type} │ ← creates new item, loads it in the current surface
│──────────────────────────────│
│ [icon] Recent item 1 2m │ ← up to 8 most recent items
│ [icon] Recent item 2 1h │
│ [icon] Recent item 3 3h │
│ ... │
│──────────────────────────────│
│ Browse all… │ ← opens full browser/management page in workspace
└──────────────────────────────┘
```
**Components:**
1. **"Create new…"** at top — creates a new item (chat/note/to-do list) and immediately loads it in the selector's surface. For notes: offers blank or from template. For to-do lists: inline name input.
2. **Recent items** — up to 8 items, ordered by last modified. Each row shows: type icon + title + time ago. Starred/pinned items sort to top. Click to load in current surface.
3. **"Browse all…"** at bottom — navigates to the appropriate full management surface in the main workspace:
- Chats → opens Chats management page as utility tab (§6.20.18)
- Notes → switches browser column to Notes mode (§6.20.8)
- To-do lists → switches browser column to Notes mode with To Do scope toggle active (§6.20.8)
**Where this pattern applies:**
- Palette Chat tab selector (§6.20.30D)
- Palette Note tab selector (§6.20.30E)
- Palette To Do tab selector (§6.20.30F)
- Standalone to-do tab header selector (§6.21.4)
- +Module → Link Existing To Do picker (§6.2A.6)
- Note canvas to-do module header selector (when switching which list is embedded)
**Rendering consistency:** The underlying content (task lists, note body, chat messages) renders identically regardless of whether it appears in the palette or the main workspace. Same components, same data pool, same interactions. The palette is a viewport into the same data, not a separate lightweight version.
---
## 6.21 To-Do System (R4 — new)
### 6.21.1 Overview
The To-Do system is a first-class data type in the Q workspace. It follows a unified data architecture where a single shared data pool is rendered across three view surfaces.
### 6.21.2 Unified Data Model
```ts
interface TodoList {
id: string
name: string // list title — semantically significant, used for entity resolution
noteId: string | null // links to note this list was created in (for embedded modules)
project_id: string | null // organizational project association (independent of matter/case)
tags: string[] // user-applied labels (optional)
created_at: string
updated_at: string
tasks: TodoTask[]
}
interface TodoTask {
id: string
text: string // task description — primary input for entity resolution
done: boolean
done_at: string | null // when completed (null if not done)
due_date: string | null
due_time: string | null
reminder: ReminderPreset | null
attachments: TodoAttachment[] // document references (future — see §6.21.8)
created_at: string // R4.1: needed for DOC72 temporal metadata
updated_at: string // R4.1: needed for DOC72 temporal metadata
sub: TodoSubtask[]
}
interface TodoSubtask {
id: string
text: string
done: boolean
done_at: string | null
}
interface TodoAttachment {
attachment_id: string
ref_type: "note" | "artifact" | "document" | "place" | "url"
ref_id: string // ID of the referenced item
title: string // display title
}
type ReminderPreset = "none" | "at_time" | "5m" | "15m" | "30m" | "1h" | "1d"
```
**List name as semantic signal:** The list name (e.g., "Henderson MTD Prep", "February 8", "Home Renovation") provides contextual framing for entity resolution — temporal grouping ("February 8" tells the system when the tasks are relevant), matter scoping ("Henderson MTD Prep" associates all tasks with that matter), or domain context. However, the list name is **secondary** to individual task text for entity resolution.
**Task text as primary entity signal:** Individual task items are the richest semantic units in the to-do system. "Prepare expert report in Paramount" contains: an action (prepare), a document type (expert report), and a case/matter reference (Paramount). Each task becomes its own `obligation` node in DOC72, and entity resolution runs primarily on the task `text` field — resolving matter references, people, document types, actions, and implied deadlines. The list name provides ambient context that strengthens confidence in entity matches (a task mentioning "expert report" in a list named "Paramount Trial Prep" has higher confidence for Paramount linkage than the same task in an unlabeled list).
**Entity resolution hierarchy:**
1. **Task `text`** — primary. Each task is resolved independently. "Prepare expert report in Paramount" → matter entity (Paramount), document type entity (expert report), action (prepare).
2. **List `name`** — contextual. Strengthens entity resolution confidence on task text. "February 8" adds temporal context; "Henderson MTD Prep" adds matter context.
3. **Task `due_date`** — temporal. Combined with list name temporal context for deadline reasoning.
4. **Task `attachments`** — relational. Creates explicit `references_document` edges.
5. **Subtask `text`** — secondary entities. "Draft motion sections 1-3" within a parent task "File Henderson MTD" inherits the parent's matter context.
This hierarchy applies regardless of which surface created the task (§2.4).
**State:** `fpTodoLists: TodoList[]` — Q-local selection state referencing EC-owned durable data (see §2.4 surface independence). `fpActiveTodoList: string | null` — currently selected list ID.
### 6.21.3 Three View Surfaces
All three surfaces read/write the same `fpTodoLists` data. Changes in any surface propagate to all others immediately (single state, no sync).
1. **Floating Palette To Do tab** (§6.20.30F) — list selector, full task rendering, "Ask" button
2. **Note canvas to-do modules** — embedded in notes via +Module → To Do or +Module → Link Existing To Do. Module renders tasks from the shared list. The `noteId` field links a list back to its source note.
3. **Standalone to-do tabs** — opened from browser (Notes scope → To Do toggle) or from palette "Open in Q." Tab type: `"todo"`, color: `#0891B2` (teal).
### 6.21.4 Standalone To-Do Tab
Opening a to-do list from the browser or palette creates a standalone tab:
**Tab header:** ListTodo icon + **list selector dropdown** (universal content selector pattern, §6.20.30J — up to 8 recent lists, "Create new list", "Browse all…") + editable title + "Defaults" button + "Pop Out" button + "Ask" button
**Rendering consistency:** The task list renders identically in the standalone tab, the palette To Do tab, and note canvas to-do modules. Same components, same data pool (`fpTodoLists`), same interactions. A change in any surface propagates instantly to all others.
**Task list rendering:**
- Per-task: checkbox, editable text (click to edit), expand toggle (for subtasks), calendar/date button
- Per-subtask: checkbox, editable text, delete button
- Add task input at bottom of active section
- Add subtask input (appears when task is expanded)
- Done section (collapsible) — completed tasks grouped at bottom
**Per-task date/reminder popover (R4):**
- State: `taskDatePopover: {blockId, taskId}` — tracks which task has popover open
- Popover contents: date input, time input, reminder preset selector ("None", "At time", "5m before", "15m before", "30m before", "1h before", "1 day before")
- Accent border when open, subtle background tint
- Available in both note canvas to-do modules and standalone to-do tabs
**Defaults panel (collapsible):** Default reminder timing, agent assignment.
**Auto-switches browser** to "nav" mode when tab becomes active.
### 6.21.5 Ask Button Wiring
**Simplified (R4):** Per-task and per-subtask spark/agent icons have been removed. A single list-level "Ask" button exists on the standalone tab header and the palette To Do footer.
**Behavior:** "Ask" sends the full to-do list as serialized context to the selected agent. The user types a specific question in the resulting chat. The agent receives the list contents as structured context and can respond with analysis, suggestions, or modifications.
**DOC24 integration point:** The "Ask" button payload serializes the to-do list into the chat context packet. This follows DOC24's knowledge-to-LLM delivery architecture for contextual packet assembly.
### 6.21.6 Two-Layer Data Architecture
**Layer 1 (operational):** To-do data is stored in EC application tables:
- `ELNOR_MEMORY/todo_lists/` — list metadata
- `ELNOR_MEMORY/todo_items/` — individual tasks and subtasks
Q reads and writes these via EC commands. This is the fast, structured, CRUD-oriented layer.
**Layer 2 (semantic):** When to-do items are created or modified, EC extracts relevant entities and creates/updates nodes in DOC72's entity graph. See §6.21.7 for the extraction contract.
### 6.21.7 Signal Emission and DOC72 Integration (R4.1)
The to-do system emits signals to DOC72 on every create, update, and delete — regardless of which surface originated the mutation (§2.4 surface independence). The extraction trigger is the EC command, not the surface.
**Extraction is hybrid — mostly deterministic, with entity resolution. Task text is the primary semantic signal (see §6.21.2 entity resolution hierarchy):**
| Data source | Extraction type | DOC72 output | Priority |
|---|---|---|---|
| Task `text` ("Prepare expert report in Paramount") | Entity resolution — **primary signal** | `obligation` node. Resolves matter/case, people, document types, actions from text. Each task becomes its own node. | 1 — highest |
| List `name` ("February 8" or "Henderson MTD Prep") | Entity resolution — **contextual signal** | Edges to matched `world_entity` nodes. Strengthens confidence on task entity matches. Provides temporal or matter framing. | 2 — context |
| Subtask `text` ("Draft motion sections 1-3") | Entity resolution — inherits parent task context | Child `obligation` nodes. Entity resolution inherits parent's matter/case context for higher confidence. | 3 |
| Task `due_date`, `due_time` | Deterministic | `obligation` node temporal fields — enables "what's due this week" queries | — |
| Task `done` / `done_at` | Deterministic | `obligation` node status — completed obligations remain in graph with completion timestamp | — |
| Subtask structure | Deterministic | `subtask_of` edges preserving full hierarchy | — |
| List membership | Deterministic | `belongs_to_list` edge from task `obligation` to list `work_product` | — |
| `project_id` | Deterministic | Edge from list/task to project entity (if project exists in graph) | — |
| `attachments` | Deterministic | `references_document` edges from `obligation` to `work_product` nodes for attached documents | — |
| `tags` | Deterministic | Tag metadata on nodes — enables tag-based retrieval | — |
**What does NOT need LLM extraction:** All structured fields. The to-do data model is already structured — no need to run an LLM over a to-do item to extract its due date when the due date is a first-class field.
**What uses entity resolution (cheap, no full LLM call):** Task text (primary), list names (contextual), and subtask text (inherited context). "Prepare expert report in Paramount" is matched against known entities in DOC72 — "Paramount" resolves to the Paramount Contractors case `world_entity`, "expert report" resolves to a document type. The list name "February 8" adds temporal context; "Henderson MTD Prep" adds matter context that boosts confidence on ambiguous task text.
**What could use LLM-assisted extraction (deferred, idle-time):** Goal inference ("this task list is probably part of trial preparation" → edge to `goal` node), action classification ("prepare" → document creation action vs "review" → read action), and category inference for ambiguous tasks. These are low-priority and can run during idle processing alongside browser extraction.
**Trigger:** On every `TodoListCreateCommand`, `TodoItemCreateCommand`, `TodoItemUpdateCommand`, `TodoItemDeleteCommand`, `TodoListDeleteCommand`. EC processes the command, writes to Layer 1, then synchronously updates Layer 2 for deterministic fields and queues entity resolution for text fields. No significance gate — all to-do mutations are significant by definition (the user explicitly created them, satisfying the "user action demonstrates intent" invariant).
**Cross-doc contract:** DOC72 must define the full `intake.todo` surface-specific intake contract specifying: entity resolution prompt for task text and list names (with the hierarchy above), `obligation` node schema with all required fields, edge types and confidence rules, context inheritance for subtasks, and dedup/merge behavior when the same task is updated multiple times. See DOC72 Proposal: Surface Intake Contracts.
### 6.21.8 Attachments (Future)
To-do tasks will support document attachments via the `TodoAttachment` schema. Each attachment is a reference (not a copy) to an existing content item — a note, artifact, document, place, or URL.
**UI:** Attachment button on each task row (not yet in mockup). Click opens a picker showing recent documents + search. Attached items show as compact pills below the task text with icon + title + remove button.
**DOC72 integration:** When a document is attached to a task, EC creates a `references_document` edge from the task's `obligation` node to the document's `work_product` node. This enables queries like "what documents are related to the Henderson MTD filing task?"
---
## 6.22 Calendar Module (R4 — new)
### 6.22.1 Overview
The calendar module is a structured content surface insertable as a note canvas module (via +Module → Calendar) or openable as a standalone tab. Both surfaces read/write the same `calEvents` data pool — changes in one propagate to the other. Calendar-related notifications (deadline alerts, event reminders) flow through the notification system (§6.25) via the palette's Notices section.
### 6.22.2 Calendar Views
Four view modes selectable via pill buttons in the calendar header (`calModes`):
| Mode | Rendering |
|---|---|
| **Month** | Grid with day numbers, event dots, click day to filter. Standard month grid layout. |
| **Week** | Day columns with hourly rows. (Placeholder — not fully built in mockup.) |
| **Day** | Hour-by-hour timeline with event blocks positioned at start time. |
| **List** | Chronological event list with configurable max items and day range. |
### 6.22.3 Calendar Header
```
[← Month Year →] [Month][Week][Day][List] [🔍] [Custom Views ▾] [+] [⚙] [Ask]
```
- Month/year display with ← → navigation arrows
- View mode selector (pill buttons)
- Search button (toggles search input for filtering events)
- Custom Views dropdown (saved filter configurations)
- [+] New event button
- Settings gear (opens settings panel, §6.22.6)
- Ask button (sends calendar context to agent)
### 6.22.4 Event Data Model
```ts
interface CalendarEvent {
id: string
title: string // primary input for entity resolution
date: string // ISO date
time: string | null // "HH:MM" or null for all-day
endTime: string | null // "HH:MM" or null
cal: string // calendar source ID
color: string // display color (from calendar source)
event_type: CalendarEventType // categorization — affects how the system reasons about the event
location: string | null // free text — entity-resolved against known locations
participants: string[] // free text names — entity-resolved against known people/entities
notes: string | null // free text — secondary input for entity resolution
project_id: string | null // organizational project association (independent of matter/case)
attachments: TodoAttachment[] // shared attachment schema with to-do system
tags: string[] // user-applied labels (optional)
reminders: EventReminder[]
source: "local" | "outlook" | "manual" // how the event was created
external_id: string | null // Outlook event ID for synced events (DOC16)
created_at: string
updated_at: string
}
type CalendarEventType =
| "hearing" // court hearing — hard deadline, blocks full day
| "deposition" // deposition — hard deadline, specific duration
| "filing_deadline" // filing/submission deadline — hard, time-sensitive
| "client_meeting" // meeting with client
| "internal_meeting" // firm-internal meeting
| "external_meeting" // meeting with opposing counsel, expert, etc.
| "task_deadline" // soft deadline for a task or group of tasks
| "vacation" // blocks availability, personal
| "personal" // non-work event
| "other" // uncategorized
interface EventReminder {
method: "notification" | "email" | "sms"
timing: "5m" | "15m" | "30m" | "1h" | "1d"
}
```
**Entity resolution inputs:** EC uses `title`, `location`, `participants`, `notes`, and `event_type` to resolve entity linkages in DOC72. A "Henderson MTD hearing at LASC" resolves to: Henderson case entity (via title), Los Angeles Superior Court entity (via location), and creates an `obligation` node with `event_type: "hearing"`. This resolution uses DOC72's entity graph for matching — no LLM call needed for structured fields, LLM-assisted resolution for ambiguous free-text fields during idle processing.
**`project_id` vs matter association:** `project_id` links the event to a DOC20 project (organizational grouping). Matter/case association is resolved independently by DOC72 entity resolution on the event's text fields. An event can be in a project AND relate to a matter, relate to a matter without being in a project, or be in a project unrelated to any matter (e.g., "Team offsite" in a "Firm Operations" project).
**State:** `calEvents: CalendarEvent[]` — Q-local selection state referencing EC-owned durable data (see §2.4 surface independence). Events linked to calendars via `cal` field.
### 6.22.5 Event Editor (Inline)
Clicking an event or the [+] button opens an inline editor:
- Title input
- Event type selector (dropdown with CalendarEventType options)
- Date input
- Time / end time inputs
- Calendar selector dropdown (which calendar this event belongs to)
- Location input
- Participants input (comma-separated names or entity references)
- Notes textarea
- Attachments section (add/remove document references)
- Project selector (optional — dropdown of active projects)
- Tags input (optional)
- Reminders section: method × timing, add/remove buttons
- Copy Ref button (EC internal reference)
- Ask agent about event button
- Save / Delete / Cancel buttons
### 6.22.6 Calendar Settings Panel
Collapsible panel with 4 tabs:
**1. Instructions** — Agent standing instructions textarea, model selector (Gemini 2.5 Pro, Claude, etc.), think level (Quick/Standard/Deep).
**2. Sources** — Outlook calendars list with checkboxes for include/exclude:
- `outlookCals: [{id, name, color, checked, shared}]`
- Shared calendar indicator
- "Exclude from master" per-calendar toggle
- "Connect new calendar" button
- New Calendar creation (name + color picker)
- Integration via DOC16 Entry 16.7
**3. Sync** — Master calendar toggle, exclude specific calendars from master, auto-refresh settings.
**4. Notices** — Calendar-specific notification rules, deadline advance warning settings.
**Agent governance:** Agent active toggle (green dot indicator), agent instructions textarea, model selection dropdown, think level selector.
### 6.22.7 Two-Layer Data Architecture
**Layer 1 (operational):** `ELNOR_MEMORY/cal_events/` — event CRUD.
**Layer 2 (semantic):** EC extracts to DOC72 entity graph. See §6.22.8 for the extraction contract.
### 6.22.8 Signal Emission and DOC72 Integration (R4.1)
Calendar events emit signals to DOC72 on every create, update, and delete — regardless of source (user creation, Outlook sync via DOC16, or any rendering surface per §2.4).
**Extraction is hybrid — structured fields are deterministic, text fields use entity resolution:**
| Data source | Extraction type | DOC72 output |
|---|---|---|
| `title` ("Henderson MTD Hearing") | Entity resolution | `obligation` node + edges to matched case/matter/person entities |
| `event_type` ("hearing", "deposition", etc.) | Deterministic | `obligation` node `event_category` field — affects reasoning (hearings are hard deadlines; vacations block availability) |
| `date`, `time`, `endTime` | Deterministic | `obligation` temporal fields — enables "what's on my calendar this week" and deadline queries |
| `location` ("LASC Dept. 47") | Entity resolution | Edge to `world_entity` for known locations (courthouses, offices) |
| `participants` (["Dr. Sanli", "J. Henderson"]) | Entity resolution | Edges to `world_entity` nodes for known people — enables "when am I next meeting with Sanli?" |
| `notes` (free text) | Entity resolution + optional LLM | Secondary entity extraction from event notes — may reference cases, documents, goals |
| `project_id` | Deterministic | Edge to project entity |
| `attachments` | Deterministic | `references_document` edges to `work_product` nodes |
| `cal` (calendar source) | Deterministic | `synced_to_calendar` edge to calendar `world_entity` — tracks which Outlook calendar, shared status |
| `source` ("outlook" vs "local") | Deterministic | Provenance metadata on node — distinguishes user-created from synced events |
| `tags` | Deterministic | Tag metadata on node |
**Calendar-specific entity graph relationships:**
- **Event → matter/case:** "Henderson MTD Hearing" → edge to Henderson `world_entity`. Resolved from title and participants.
- **Event → people:** "Deposition of Dr. Sanli" → edge to Sanli `world_entity`. Enables cross-referencing schedules with people.
- **Event → location:** "LASC Dept. 47" → edge to courthouse `world_entity`. Enables "what do I have at LASC this month?"
- **Event → tasks:** A hearing date creates a natural relationship with preparation tasks. DOC72 can infer `deadline_for` edges from to-do lists that reference the same matter and have due dates before the event.
- **Event → goals:** Trial dates, filing deadlines, and discovery cutoffs relate to strategic `goal` nodes. LLM-assisted inference during idle processing.
- **Calendar → calendar:** The calendar source itself (`world_entity`) carries metadata: shared calendar for a case team, personal calendar, firm-wide calendar. Enables "what's on the Henderson team calendar?"
**Trigger:** On every `CalendarEventCreateCommand`, `CalendarEventUpdateCommand`, `CalendarEventDeleteCommand`, and on Outlook sync events from DOC16. Same hybrid extraction as to-do: deterministic for structured fields, entity resolution for text fields, optional LLM for inference during idle time.
**Cross-doc contract:** DOC72 must define the full `intake.calendar` surface-specific intake contract. See DOC72 Proposal: Surface Intake Contracts.
### 6.22.9 Notes Surface Intake Gap (R4.1 — cross-doc obligation)
Notes are free-form rich text — unlike to-do and calendar data, they are not structured. Knowledge extraction from notes requires LLM-assisted extraction similar to the browser's Tier 2 pipeline (§6.19.22).
DOC20 does not currently specify signal emission for notes. The gap:
- **When** does a note emit an extraction signal? (On auto-save? After a significance threshold — e.g., >100 words and >30 seconds of editing? On note close?)
- **What** extraction prompt is used? (Entity extraction, summary generation, obligation detection?)
- **What** nodes result? (`work_product` for the note itself, `world_entity` for mentioned people/cases, `obligation` for mentioned deadlines?)
This is a DOC72 intake contract question. DOC72 §20A defines surface-specific intake contracts and should include a `intake.notes` contract. DOC20's obligation is to emit the right signals (note content, note metadata, edit timestamps) to EC at the right time. The extraction pipeline is DOC72's responsibility.
**Cross-doc obligation:** DOC72 must define `intake.notes` specifying: extraction trigger, significance gate (to avoid extracting from trivial scratchpads), extraction prompt, output node types, and confidence/promotion rules. DOC20 will implement the signal emission side once DOC72 defines what signals it needs.
---
## 6.23 Icon System (R4 — new)
### 6.23.1 Overview
All emoji icons throughout the Q workspace have been replaced with SVG icon components. This provides consistent sizing, strokeWidth, and visual weight across all surfaces.
### 6.23.2 Icon Component Architecture
```
Ic (base SVG renderer)
└─ I (icon object: 60+ named icons → Ic-based components)
├─ TabIcon (tab-specific rendering)
│ └─ uses tabIconMap + navPageIcons
└─ NavIcon (nav page icon rendering)
└─ uses navPageIcons
```
**`Ic` base component:** `strokeWidth: 1.5`, `strokeLinecap: "round"`, `strokeLinejoin: "round"`, `24px viewBox`, `fill: "none"`.
**`I` object:** Maps 60+ icon names to `Ic`-based components. Used throughout the workspace for all icon rendering.
**`TabIcon({type, icon, size})`:** Renders tab icons from `tabIconMap` lookup. Handles utility tabs via `navPageIcons` fallback.
**`NavIcon({id, size})`:** Renders nav page icons from `navPageIcons` lookup.
### 6.23.3 Icon Source Libraries
| Library | Usage | Icons |
|---|---|---|
| **Heroicons** (outline) | Primary icon set | FileText, File, Globe, MsgCircle, Users, List, Settings, Database, Layers, Bell, User, Search, ChevronDown, Plus, X, Eye, EyeSlash, Star, Lock, Grid, Columns, ArrowRight, etc. |
| **Lucide** | Selective swaps | Scissors (clips), Spark (activity), Folder, Globe, Plug (skills/connectors), ListTodo (to-do), Save (floppy disk), Pin (thumbtack) |
| **Tabler** | Two specialized icons | Bucket (Context Buckets — bucket shape), Subtask (Tasks nav page — hierarchical boxes) |
### 6.23.4 Icon Mapping Tables
**`tabIconMap`** (tab type → icon):
| Tab type | Icon | Source |
|---|---|---|
| `note` | FileText | Heroicons |
| `doc` | File | Heroicons |
| `web` | Globe | Heroicons (fallback; favicons in production) |
| `clips` | Scissors | Lucide |
| `chat` | MsgCircle | Heroicons |
| `todo` | ListTodo | Lucide |
| `room` | Users | Heroicons |
| `task` | List | Heroicons |
| `panel` | Grid | Heroicons |
| `forum` | MsgCircle | Heroicons |
| `utility` | Settings | Heroicons |
**`navPageIcons`** (nav page ID → icon):
| Page ID | Icon | Source |
|---|---|---|
| `activity` | Bell | Heroicons |
| `tasks` | Subtask | Tabler |
| `projects` | Folder | Heroicons |
| `knowledge` | Database | Heroicons |
| `forums` | MsgCircle | Heroicons |
| `agents` | User | Heroicons |
| `skills` | Plug | Lucide |
| `overlays` | Layers | Heroicons |
| `buckets` | Bucket | Tabler |
| `settings` | Settings | Heroicons |
---
## 6.24 Split View (R4 — new)
### 6.24.1 Overview
Split view provides two independent side-by-side panes for multitasking. Toggle via the Columns icon in the left nav rail (§6.20.2).
### 6.24.2 Layout
- Split icon at far right of tab bar (Columns icon) or via left nav rail
- Two side-by-side panes with draggable vertical divider
- Each pane: own tab bar, own active tab, own content area
- Left pane keeps browser column toggle; right pane does not
- Each pane independently supports its own right panel (Ask Agent, Comments)
### 6.24.3 Behavior
- **Split focus:** Clicking in a pane sets it as the active/focused pane. Keyboard shortcuts and commands apply to the focused pane.
- **Close split:** Close button on right pane → returns to single view + "tabs merged" toast. All tabs from the right pane are appended to the left pane's tab list.
- **Move tab:** Right-click tab → "Move to Split Pane" moves the tab to the other pane.
- **Tab independence:** Each pane maintains its own active tab state. Switching tabs in one pane does not affect the other.
---
## 6.25 Notification System (R4 — new)
### 6.25.1 Overview
The notification system provides a Linear-style inbox for managing alerts from multiple sources. Notifications are accessed via the floating palette's sidebar → Notices link (§6.20.30C).
### 6.25.2 Notification Inbox
**Location:** Renders in the floating palette's main pane when "Notices" is selected from the sidebar.
**Inbox controls:**
- Filter pills: All / Unread / Snoozed
- "Mark all read" button
- Unread count display
- "Silent" indicator (when silent mode active)
### 6.25.3 Notification Card Anatomy
```
┌─────────────────────────────────────────┐
│ ▎ [●] Source Label 12:34 [X] │ ← colored accent bar (unread), source dot + label, timestamp, dismiss
│ ▎ Title text (12px bold) │
│ ▎ Description text (10.5px) │
│ ▎ [View] [Dismiss] [🕐 Snooze ▾] │ ← action buttons
└─────────────────────────────────────────┘
```
**Source colors:**
| Source | Color | Hex |
|---|---|---|
| Calendar | Amber | #D97706 |
| Agent | Purple | #7C3AED |
| Email | Blue | #3B82F6 |
| System | Gray | #6B7280 |
**Card states:**
- **Unread:** Left accent bar visible (source color)
- **Read:** No accent bar
- **Snoozed:** Dimmed appearance
- **Dismissed:** Removed from inbox
### 6.25.4 Snooze Options
Snooze dropdown (clock icon + "Snooze" label + chevron):
- 15 minutes
- 1 hour
- 4 hours
- Tomorrow
- Custom (date/time picker — deferred)
### 6.25.5 Notification Delivery Matrix
Configurable per source in Settings page (NOT in sidebar):
| Source | Badge | Glow | Sound | Popup | Auto-show |
|---|---|---|---|---|---|
| Agent completions | ✅ | ❌ | ❌ | ❌ | ❌ |
| Calendar alerts | ✅ | ✅ | ✅ | ✅ | ❌ |
| Email flags | ✅ | ✅ | ❌ | ✅ | ❌ |
| Gate approvals | ✅ | ✅ | ✅ | ✅ | ✅ |
| System errors | ✅ | ✅ | ✅ | ✅ | ❌ |
**Sound picker:** Chime / Tap / Bell / Drop / None.
**Focus hours:** Time range during which sound/popup/auto-show are suppressed.
**Badge position:** Menu bar / Palette border / Both.
### 6.25.6 Silent Mode
Activated via sidebar bottom toggle (§6.20.30I). Suppresses sounds, glow, and auto-show behavior. Badge counts continue to update. Visual indicators: red bell icon in sidebar, "Silent" text in Notices header.
---
## 6.26 Bookmarks Bar (R4 — new)
### 6.26.1 Overview
A thin horizontal bar below the web browser toolbar, only visible on web tab type.
### 6.26.2 Layout and Behavior
- Populated from a special "Bookmarks Bar" folder (`isBar: true` flag on the folder)
- Each bookmark: favicon-style color badge + title text
- "⋯" button at right for editing/managing bookmarks bar items
- Toggle visibility via settings (`bmBarVisible: boolean`)
- Only renders when the active tab is a web tab
---
## 6.27 Places Scope — Collection Filtering (R4 — new)
### 6.27.1 Collection Filter Dropdown
When the Places scope is active in the browser, a collection filter dropdown appears:
- Colored dot per collection, folder icon for "All"
- Collection data example: `[{name:"All",color:null},{name:"Active Cases",color:"#31588c"},{name:"Research",color:"#2E8B57"},{name:"Templates",color:"#D97706"}]`
- Dropdown trigger shows selected collection's dot or folder icon
- X (delete/remove) button works on ALL place items including those with missing status
### 6.27.2 Saved Views Overhaul (R4)
- "Current" and "No Project" built-in views have been **removed**
- Default views: Recent (built-in), Deadlines this week (user), Active matters (user)
- User views: Edit (pencil) and Delete (X) icons on hover
- "Save current view": subtle text link with save icon (not prominent button)
- Save popover: text input for view name, Enter to save, Escape to cancel
---
## 6.28 Chat Tab Rendering (R4 — updated)
### 6.28.1 Full Workspace Chat Tab
Chat tabs in the full Q workspace (not the palette) have been updated:
- **Avatar component** (`Av`) for message authors
- **Author name + timestamp** per message
- **Send button** with dedicated primary style (I.Send icon)
- **Input field placeholder:** `Message ${agent.name}…` (dynamic agent name)
- **Chat title editable** in header (click to edit, Enter to save)
- **Attach button** (+) for file attachment
---
## 6.29 Skills & Connectors Page (R4.3 — new)
**Source:** Skills & Connectors Page Spec V1 (SKILLS_AND_CONNECTORS_PAGE_SPEC_V1.md)
### §6.29.1 Purpose
The Skills & Connectors page is the single management surface for everything Elnor can do and how to extend it. It answers: what can Elnor do (browse all capabilities), how do I add more (install skills, connect MCP servers, configure APIs, import SKILL.md files, install OpenClaw plugins), how do I manage what exists (configure, enable/disable, update, remove, inspect provenance), how do I expose Elnor to external systems (reverse MCP server), and how do I teach Elnor new things (surfaces DOC3 Learn flows).
This page does NOT own: agent configuration (→ Agents page), protocol-level settings (→ Settings), or deep entity graph inspection (→ Knowledge Manager / DOC72).
### §6.29.2 Page registration
```ts
{
content_type: "skills_and_connectors",
display_name: "Skills & Connectors",
icon: "puzzle-piece",
nav_position: "primary",
route: "/skills",
supports_tabs: true,
tab_count: 6,
}
```
### §6.29.3 Tab structure
| Tab | Label | Purpose | Backend owners |
|---|---|---|---|
| 1 | My capabilities | Unified browse of everything Elnor can do | DOC24, DOC72, DOC11, DOC3 |
| 2 | Connectors & accounts | Outbound MCP servers, API integrations, authenticated accounts | DOC16 Entry 16.7, DOC24 |
| 3 | OpenClaw tools | Browse/install/update OpenClaw skills, plugins, tool catalog from ClawHub (13,700+) | DOC11 |
| 4 | Expose Elnor | Reverse MCP server for external LLMs (Claude, ChatGPT, Cursor) | DOC11, DOC16 Entry 16.7 |
| 5 | Learn & teach | DOC3 learning flows — demonstrate, coach, import, review | DOC3 |
| 6 | Hub | Community skills, curated packs, third-party plugins | Phase 2 |
### §6.29.4 Tab 1 — My Capabilities
#### §6.29.4.1 CapabilityCardSchema (read-model)
The unified read-model normalizes all capability types into one browsable shape:
```ts
export const CapabilityCardSchema = z.object({
capability_id: z.string().max(160),
capability_name: z.string().max(120),
capability_type: z.enum([
"learned_procedure", // DOC3 → DOC72 procedure node
"native_skill", // SKILL.md in OpenClaw workspace
"mcp_tool", // Tool from connected MCP server
"api_integration", // Custom API endpoint (Phase 2)
"system_tool", // Built-in OpenClaw tool (exec, browser, etc.)
"plugin_tool", // Tool registered by a plugin
"standing_procedure", // DOC72 standing procedure node
"memory_directive", // DOC72 memory directive node
]),
source: z.object({
origin: z.enum([
"demonstration", "coaching", "skill_mining", "imported_skill",
"openclaw_bundled", "openclaw_managed", "clawhub_installed",
"mcp_discovered", "api_configured", "plugin_registered", "system_builtin",
]),
origin_detail: z.string().max(240).optional(),
created_at: z.string().datetime().optional(),
}),
app_context: z.string().max(80).optional(),
status: z.enum([
"active", "degraded", "stale", "disabled", "experimental",
"pending_review", "unavailable", "update_available",
]),
confidence: z.number().min(0).max(1).optional(),
install_lane: z.enum(["experimental_private", "approved_workspace", "shared_promoted"]).optional(),
description: z.string().max(500),
tags: z.array(z.string().max(40)).default([]),
usage_summary: z.object({
total_invocations: z.number().int().default(0),
successful_invocations: z.number().int().default(0),
last_used: z.string().datetime().optional(),
last_outcome: z.enum(["success", "failure", "partial", "aborted"]).optional(),
}).optional(),
available_actions: z.array(z.enum([
"edit", "disable", "enable", "archive", "delete", "promote", "demote",
"export", "configure", "uninstall", "update", "inspect",
"link_to_settings", "link_to_connector", "link_to_plugin",
])).default([]),
schema_version: z.literal(1),
});
```
Data composition: `GET /api/skills-connectors/capabilities` queries DOC72 entity graph (procedure nodes, standing procedures, memory directives), DOC3 AbilityAvailabilitySnapshot (usability status), DOC24 capability registry (MCP tools, tool packs, registered actions), and DOC11 gateway (native skill inventory, built-in tools, plugin tools). Merges into `CapabilityCardSchema[]` with deduplication by capability_id. Read-only composition — no writes.
#### §6.29.4.2 Action matrix by capability type
| Capability type | Edit | Disable | Delete/Archive | Promote/Demote | Export | Update | Configure via |
|---|---|---|---|---|---|---|---|
| Learned procedure | Full edit via DOC3 bundle editor (§9.2) | Toggle availability | Archive (recoverable) or permanent delete | experimental ↔ approved ↔ shared | Export as SKILL.md | — | Detail drawer |
| Standing procedure | Edit trigger conditions, action steps | Toggle | Archive or delete | — | — | — | Detail drawer |
| Memory directive | Edit scope, priority, applies_when | Toggle | Archive or delete | — | — | — | Detail drawer |
| Native skill | Edit SKILL.md content | Disable via config | Uninstall (workspace only) | — | — | If ClawHub source | Tab 3 |
| MCP tool | Read-only | Disable individual tool | Disconnect connector | — | — | — | Tab 2 |
| System tool | Read-only | Via Settings | Cannot remove | — | — | — | Settings |
| Plugin tool | Read-only | Disable plugin | Uninstall plugin | — | — | If npm/ClawHub | Tab 3 |
#### §6.29.4.3 Detail drawer (7 sections)
The detail drawer renders inline below the selected card (no overlay). Content organized in tabbed sections:
1. **Identity & status** — name, type badge, status badge, description, app context, install lane, created date
2. **Contract** (learned/standing/memory) — steps with intent/detail/nature/parameters (inline-editable), trigger phrases (editable tag list), use/non-use conditions, preconditions, postconditions, constraints. For native skills: rendered SKILL.md. For MCP/system/plugin: input schema table (read-only).
3. **Confidence & performance** (learned procedures only) — current score with α/β breakdown, sparkline chart, confidence event history with deltas
4. **Usage history** — summary metrics (total uses, success rate, last used), chronological invocation timeline with outcomes, dispatch IDs, agent identity, context hints. Data source: `ProcedureExecutionOutcomeEvent` records from DOC72.
5. **Provenance** — creation story (demonstration session, fidelity, app version, review status), edit history with timestamps
6. **Linked entities** (learned/standing) — goals served (via `serves_goal` edge), required procedures (via `requires_preceding_procedure`), apps spanned (via `spans_application`), conflicts, related memory directives
7. **Actions** (sticky bottom) — primary (Edit/Inspect), secondary (Promote/Export/Update), danger (Archive/Delete/Disable), navigation (Settings/Connector/Plugin links)
Edit flow: drawer switches to edit mode → fields become interactive → changes tracked as `BundleEditOperation[]` batch → save submits to `POST /api/skills-connectors/capabilities/:capabilityId/edit` → EC validates against shared contract schema → writes via `entity_knowledge_write` → provenance annotation appended → confidence unaffected by edits.
#### §6.29.4.4 Card descriptions
Description content source varies by type: learned procedures get LLM-generated descriptions during DOC3 interpretation (ADJ-09 added `description` to the prompt), editable by user; native skills use SKILL.md frontmatter `description` field; MCP tools use the tool description from MCP server discovery; system tools use hardcoded descriptions from OpenClaw; plugin tools use descriptions from `registerTool()` calls.
#### §6.29.4.5 Routes
```http
GET /api/skills-connectors/capabilities
GET /api/skills-connectors/capabilities/:capabilityId
GET /api/skills-connectors/capabilities/:capabilityId/usage
GET /api/skills-connectors/capabilities/:capabilityId/provenance
POST /api/skills-connectors/capabilities/:capabilityId/edit
POST /api/skills-connectors/capabilities/:capabilityId/promote
POST /api/skills-connectors/capabilities/:capabilityId/demote
POST /api/skills-connectors/capabilities/:capabilityId/archive
POST /api/skills-connectors/capabilities/:capabilityId/restore
DELETE /api/skills-connectors/capabilities/:capabilityId
GET /api/skills-connectors/capabilities/:capabilityId/export
POST /api/skills-connectors/capabilities/:capabilityId/disable
POST /api/skills-connectors/capabilities/:capabilityId/enable
```
### §6.29.5 Tab 2 — Connectors & Accounts
Manages outbound MCP servers, API integrations, and authenticated accounts (Gmail, M365, etc.). See DOC16 Entry 16.7 for backend lifecycle.
```ts
export const ConnectorCardSchema = z.object({
connector_id: z.string().uuid(),
connector_type: z.enum(["mcp_server", "api_integration", "account_connection", "custom_tool"]),
service_name: z.string().max(80),
connection_status: z.enum(["connected", "degraded", "auth_expired", "disconnected", "error"]),
authenticated_identity: z.string().max(120).optional(),
scope_summary: z.string().max(240).optional(),
discovered_tools: z.array(z.object({
tool_name: z.string().max(80),
tool_description: z.string().max(240),
enabled: z.boolean().default(true),
})).default([]),
last_health_check: z.string().datetime().optional(),
health_check_result: z.enum(["healthy", "degraded", "failed"]).optional(),
created_at: z.string().datetime(),
schema_version: z.literal(1),
});
```
**Connection flows:**
- **Connect MCP Server:** URL input → probe for discovery → review discovered tools → register with DOC24
- **Connect Account (OAuth):** service picker → OAuth popup → account/mailbox selector → scope configuration → connected
- **Add Custom Tool (Phase 2):** endpoint URL + auth + input schema + output parsing + semantic description → EC registers as DOC24 action
Multi-account supported: same service can have multiple connector cards, each with independent auth.
**Routes:**
```http
GET /api/connectors
POST /api/connectors/mcp
GET /api/connectors/mcp/:connectorId
DELETE /api/connectors/mcp/:connectorId
POST /api/connectors/mcp/:connectorId/probe
POST /api/connectors/accounts
GET /api/connectors/accounts/:connectorId
PATCH /api/connectors/accounts/:connectorId
DELETE /api/connectors/accounts/:connectorId
POST /api/connectors/accounts/:connectorId/refresh
```
### §6.29.6 Tab 3 — OpenClaw Tools
Visual equivalent of the OpenClaw CLI for skill/plugin management. Three sections:
**Section A: Installed Skills** — all skills in OpenClaw workspace across all tiers (bundled, managed, workspace, agent-specific). Cards show: name, tier badge, version, requirements status, update available badge. Actions: update, disable, uninstall (workspace only), configure, inspect.
**Section B: Installed Plugins** — native and bundle plugins currently loaded. Cards show: name, type (native/bundle), registered capabilities (tools, channels, providers), slot assignment. Actions: enable, disable, update, uninstall, configure.
**Section C: Browse & Install** — ClawHub catalog (13,700+ skills) rendered in Q with vector search proxied through DOC11. Results show: name, description, install count, star count, VirusTotal scan status. Actions: install, inspect (preview SKILL.md).
```ts
export const OpenClawToolCatalogEntrySchema = z.object({
entry_id: z.string().max(160),
entry_type: z.enum(["skill", "plugin_native", "plugin_bundle"]),
name: z.string().max(80),
description: z.string().max(500),
version_installed: z.string().max(20).optional(),
version_latest: z.string().max(20).optional(),
install_status: z.enum([
"installed_active", "installed_disabled", "available",
"update_available", "incompatible",
]),
tier: z.enum(["bundled", "managed", "workspace", "agent", "external"]).optional(),
category: z.string().max(40).optional(),
requirements_met: z.boolean().optional(),
security_scan: z.enum(["clean", "flagged", "not_scanned"]).optional(),
schema_version: z.literal(1),
});
```
**Routes:**
```http
GET /api/gateway/tool-catalog
POST /api/gateway/skills/:slug/install
POST /api/gateway/skills/:slug/update
POST /api/gateway/skills/:slug/uninstall
PATCH /api/gateway/skills/:slug/config
POST /api/gateway/plugins/:pluginId/install
POST /api/gateway/plugins/:pluginId/update
POST /api/gateway/plugins/:pluginId/uninstall
POST /api/gateway/plugins/:pluginId/enable
POST /api/gateway/plugins/:pluginId/disable
PATCH /api/gateway/plugins/:pluginId/config
GET /api/gateway/clawhub/search
GET /api/gateway/clawhub/inspect/:slug
```
Badge on tab label when N skills have updates available.
### §6.29.7 Tab 4 — Expose Elnor
Configure Elnor as a reverse MCP server that external LLMs can connect to. See DOC16 Entry 16.7 for backend lifecycle.
```ts
export const ExposureConfigSchema = z.object({
enabled: z.boolean().default(false),
server_url: z.string().max(240).optional(),
auth_token: z.string().max(240).optional(),
capability_filter: z.object({
install_lanes: z.array(z.enum(["approved_workspace", "shared_promoted"])).default(["approved_workspace"]),
capability_types: z.array(z.enum(["learned_procedure", "native_skill", "system_tool"])).default(["learned_procedure", "native_skill"]),
excluded_capability_ids: z.array(z.string().max(160)).default([]),
}),
knowledge_scope: z.enum(["personal_only", "firm_shared", "both"]).default("personal_only"),
action_permissions: z.enum(["read_only", "execute"]).default("read_only"),
audit_enabled: z.boolean().default(true),
schema_version: z.literal(1),
});
```
Rules: `experimental_private` capabilities NEVER exposed. `firm_shared` knowledge scope requires explicit warning about privilege implications. `execute` permission requires explicit warning about remote action risks. Audit log records every remote invocation with caller identity, capability accessed, knowledge scope used, timestamp.
Setup flow: enable → configure scope → get connection URL + token → paste into external LLM's MCP settings. Connection instructions formatted per external LLM.
### §6.29.8 Tab 5 — Learn & Teach
Surfaces DOC3's existing learning flows. No new routes — wraps DOC3 §11.1.
**Start Learning** action cards: "Watch Me Do This" (demonstration), "Let Me Coach You" (coaching, Phase 2), "Import a Skill" (SKILL.md import via DOC3 §25.6), "Import from Clipboard."
**Active Sessions** — in-progress LearnSession instances in non-terminal states. Card shows: session name, target app, state, event count. Actions: resume, review, cancel.
**Pending Abilities** — extracted but not installed items. Card shows: name, type, confidence. Actions: review, quick promote, dismiss.
**Recently Installed** — last 10 abilities installed through DOC3. Card shows: name, lane, confidence, usage count. Actions: inspect (→ Tab 1), promote, archive.
### §6.29.9 Tab 6 — Hub (Phase 2)
Placeholder: "Coming soon. The Hub will let you browse community-built capabilities, install curated packs, and discover plugins from the ELNOR ecosystem."
### §6.29.10 UI states (all tabs)
| State | What the user sees |
|---|---|
| Loading | Skeleton cards with shimmer |
| Empty (Tab 1) | "No capabilities found. Start by teaching Elnor something or connecting a service." CTAs: Teach → Tab 5, Connect → Tab 2, Browse → Tab 3 |
| Empty (Tab 2) | "No connectors configured." CTAs for each connection type |
| Populated | Content as specified per tab |
| Filtered empty | "No capabilities match your filters." Clear-filters button |
| Error | "Couldn't load capabilities. Some data sources may be unavailable." Retry button. Partial results shown if some backends responded |
| Detail drawer open | Drawer renders inline below selected card. Selected card highlighted. |
| Edit mode | Fields become editable. Save/Cancel buttons. Unsaved changes trigger discard confirmation on close |
| Installing (Tab 3) | Card shows progress spinner + "Installing {name}..." |
| OAuth in progress (Tab 2) | "Connecting to {service}..." with spinner and cancel |
### §6.29.11 Shared components
**SearchBar** — debounced (300ms). Tab 1: searches name, description, tags. Tab 3: proxied to ClawHub vector search.
**StatusBadge** — reusable for all connection/capability statuses. Green: active/connected. Amber: degraded/stale/experimental/auth_expired/update_available/pending. Red: error/unavailable. Gray: disabled/disconnected.
**ConfirmationDialog** — for destructive actions (disconnect, uninstall, archive, delete) with destructive flag for red confirm button.
---
### 7.0 Common enums
```ts
type BrowserPageHint =
| "notes" | "projects" | "context" | "tasks"
| "panels" | "forums" | "agents" | "skills"
| "documents" | null
type BrowserPrimaryScopeFamily = "project" | "bucket" | "places" | "folders" | "notes" | "saved_view" | null // R1.7: added "notes"
type BrowserScopeFamily = "collection" | BrowserPrimaryScopeFamily
type BrowserItemType =
| "document" | "chat" | "preset" | "skill" | "task"
| "agent" | "panel" | "forum" | "overlay" | "note"
| "bucket" | "project" | "automation"
| "artifact" | "place" | "prompt"
// R1.6: renamed context_bucket→bucket, generated_artifact→artifact, added prompt
// "automation" excluded from Browser type chips. Automation items are projected from DOC10 orchestration state.
// The schema is owned by DOC10; DOC20 consumes the BrowserItemSchema projection only. (internal-only). Appears in results only when explicitly filtered.
type BrowserSortKey = "updated_at" | "title" | "item_type" | "last_used_at" | "created_at"
// R1.6: UI label "Running" renamed to "Last Used" everywhere. No enum change — last_used_at already sorts by recency.
type BrowserItemStatus = "active" | "running" | "waiting" | "paused" | "completed" | "failed" | "archived" | "deleted"
// R1.6: Replaces loose status: string | null across all schemas.
type BrowserFilterKey =
| "open" | "in_progress" | "scheduled" | "completed" | "failed"
| "unread" | "recent" | "generated" | "pinned" | "errors"
| "preset" | "archived" | "todo" | "unread_comments"
| "system" | "has_context" | "has_unread"
type BrowserCapability =
| "open" | "open_in_new_tab" | "rename" | "duplicate" | "delete"
| "add_to_collection" | "assign_project" | "remove_from_project"
| "move_to_project" | "show_in_finder" | "open_with_default_app"
| "reveal_source_bucket" | "attach_to_bucket" | "export"
| "pin" | "archive" | "cancel" | "copy_link" | "copy_reference" |
type ProjectStatus = "active" | "paused" | "archived"
type ProjectKind = "standard" | "global"
type ProjectMemberType = "chat" | "panel" | "forum" | "task" | "automation" | "note" | "artifact" // R2: renamed from generated_artifact
type NoteKind = "standard" | "today" // R1.7: "todo" removed (tasks are blocks), "today" added
type NoteStatus = "active" | "archived" | "deleted"
// R2: SavedView system_view_key extended
// system_view_key?: "current" | "recent" | "no_project" | "recently_deleted"
type LinkedFolderMode = "browse_only" | "sync_to_project_bucket"
type LinkedFolderSyncStatus = "idle" | "syncing" | "ok" | "warning" | "error"
type NoteRevisionKind =
| "autosave" | "manual_save" | "ai_apply"
| "tracked_change_accept" | "tracked_change_reject"
| "restore" | "metadata_update" | "import"
// R1.6: Canonical enum lock. §7.0 is the single authoritative source for all DOC20 enums.
// Any enum defined elsewhere that contradicts §7.0 is superseded.
type ArtifactKind = "document" | "code" | "chart" | "data" | "image" | "mixed"
// R1.6: Authoritative. Supersedes any inline definitions in §7.13, §7.19, §8.6B.
type ArtifactOriginType = "chat" | "task" | "room" | "panel" | "forum" | "chain" | "manual_save" | "note_conversion"
// R1.6: Added note_conversion. Authoritative. Supersedes §7.19, §8.6B.
// R2: Structural type for ProseMirror/Tiptap JSON content
type TiptapJSONContent = {
type: string
attrs?: Record<string, unknown>
content?: TiptapJSONContent[]
marks?: { type: string; attrs?: Record<string, unknown> }[]
text?: string
}
type DOC20ErrorCode =
| "NOTE_NOT_FOUND" | "NOTE_REVISION_CONFLICT" | "NOTE_WRITE_QUEUE_BUSY"
| "NOTE_LOCKED" | "NOTE_DELETED" | "NOTE_EXPORT_FAILED"
| "COMMENT_NOT_FOUND" | "COMMENT_ANCHOR_ORPHANED" | "COMMENT_THREAD_INVALID"
| "TRACKED_CHANGE_NOT_FOUND" | "PENDING_CHANGE_OVERLAP"
| "ARTIFACT_NOT_FOUND" | "ARTIFACT_VERSION_CONFLICT" | "ARTIFACT_DIFF_INVALID"
| "PROJECT_NOT_FOUND" | "PROJECT_BUDGET_EXCEEDED"
| "FOLDER_NOT_FOUND" | "FOLDER_DEPTH_EXCEEDED"
| "BROWSER_QUERY_TIMEOUT" | "BROWSER_RESOLVER_DEGRADED"
| "ATTACHMENT_NOT_FOUND" | "ATTACHMENT_STALE"
| "PLACE_NOT_FOUND" | "PLACE_SYNC_FAILED"
| "IDEMPOTENT_REPLAY"
| "PRESET_NOT_FOUND" | "PRESET_READONLY"
| "BLOCK_TYPE_MISMATCH"
| "FEED_REFRESH_FAILED"
| "TEMPLATE_NOT_FOUND"
| "BOOKMARK_NOT_FOUND"
| "CREDENTIAL_NOT_FOUND"
| "UNKNOWN_ERROR"
```
## 7.1 Collection
```ts
interface CollectionSchema {
collection_id: string
title: string
color_token: string
show_in_quick_row: boolean
archived: boolean
created_at: string
updated_at: string
}
interface CollectionEdgeSchema {
collection_id: string
item_type: BrowserItemType
item_id: string
created_at: string
}
```
## 7.2 Saved view
```ts
interface SavedViewSchema {
saved_view_id: string
title: string
scope_family: BrowserPrimaryScopeFamily
scope_detail_ids: string[]
collection_ids: string[]
type_ids: BrowserItemType[]
filters: Partial<Record<BrowserFilterKey, boolean>>
sort_key: BrowserSortKey
sort_dir: "asc" | "desc"
search_query: string
search_mode: "this_view" | "everywhere"
include_archived: boolean
created_at: string
updated_at: string
pinned: boolean
system_view: boolean
system_view_key?: "current" | "recent" | "no_project"
}
```
## 7.3 Place alias
```ts
interface PlaceAliasSchema {
place_id: string
title: string
path: string
kind: "folder"
availability_status: "ok" | "missing" | "needs_access"
last_checked_at: string | null
pinned: boolean
created_at: string
updated_at: string
}
```
## 7.4 Linked folder alias
```ts
interface LinkedFolderAliasSchema {
linked_folder_id: string
project_id: string
place_id: string
mode: LinkedFolderMode
sync_status: LinkedFolderSyncStatus
include_globs: string[]
exclude_globs: string[]
last_scan_at: string | null
last_sync_at: string | null
created_at: string
updated_at: string
}
```
## 7.5 Project
**R1.6:** `ProjectUpdateCommand` (§8.2) must use `ProjectUpdatableFields` (defined in §8.2), not `Partial<ProjectSchema>`. Immutable fields (`project_id`, `project_kind`, `slug`, `primary_project_bucket_id`, `created_at`, `created_by`) are excluded from updates.
```ts
interface ProjectSchema {
project_id: string
project_kind: ProjectKind
title: string
slug: string
description: string
color_token: string
status: ProjectStatus
primary_project_bucket_id: string
default_agent_id: string | null
default_model_id: string | null
fallback_chain_id: string | null
think_level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
default_overlay_ids: string[]
output_folder_path: string | null
budget_enabled: boolean
budget_amount_usd: number | null
budget_period: "day" | "week" | "month" | "quarter" | null
budget_alert_threshold_pct: number | null
show_budget_in_header: boolean
created_at: string
updated_at: string
created_by: string
updated_by: string
archived_at: string | null
deleted: boolean
deleted_at: string | null
}
interface ProjectBucketAttachmentSchema {
project_id: string
bucket_id: string
role: "primary" | "attached"
order_index: number
created_at: string
created_by: string
}
interface ProjectMembershipEventSchema {
event_id: string
member_type: ProjectMemberType
member_id: string
previous_project_id: string | null
next_project_id: string | null
reason: "create" | "user_attach" | "user_detach" | "move" | "project_delete_detach" | "origin_inherit"
created_at: string
created_by: string
}
```
## 7.6 Note metadata
See §6.2 for `NoteMetadataSchema`.
## 7.6A Block schemas (R1.7)
Block data is stored within the Tiptap JSON document as custom node extensions. The schemas below define the structured data within each block node. See §6.2A for rendering and interaction rules.
```ts
type NoteBlockType = "text" | "task_list" | "activity_feed" | "inline_thread" | "bar"
interface NoteBlockBase {
block_id: string
block_type: NoteBlockType
position: number
collapsed: boolean
title: string | null
created_at: string
updated_at: string
}
interface TaskListBlockData {
block_type: "task_list"
title: string
tasks: {
task_id: string
text: string
done: boolean
due_date: string | null
linked_note_id: string | null
subtasks: { subtask_id: string; text: string; done: boolean }[]
created_at: string
updated_at: string
}[]
}
interface ActivityFeedBlockData {
block_type: "activity_feed"
title: string
preset_id: string | null // R1.8: links to ModulePreset (§6.2C)
source: { // R1.8: replaces auto_updating + hardcoded sections
source_type: "system_events" | "agent_instruction"
event_filters?: { event_types: string[]; agent_id?: string; chain_id?: string; watcher_id?: string; forum_id?: string; project_id?: string }
instruction?: string
agent_id?: string
refresh_interval_minutes?: number
}
max_items: number // R1.8: display cap, default 25
max_visible_height: number
dormancy_days: number // R1.8: default 15, 0 = never
last_updated_at: string // R1.8: ISO timestamp
dormant: boolean // R1.8: true if expired
sections: {
section_id: string
label: string
icon: string
collapsed: boolean
items: { item_id: string; text: string; time: string; accent_color: string | null; created_at: string }[]
}[]
}
interface InlineThreadBlockData {
block_type: "inline_thread"
context_quote: string | null
display_mode: "inline"
messages: {
message_id: string
author: string
body: string
created_at: string
edited: boolean
edited_at: string | null
}[]
}
interface BarBlockData {
block_type: "bar"
title: string
accent_color: string | null
time: string | null
elnor_ref: string | null
}
```
## 7.6B Today note schema (R1.8 — updated)
```ts
interface TodayNoteTemplate {
template_id: string
blocks: {
block_type: NoteBlockType
title: string | null
preset_id: string | null // R1.8: for activity_feed blocks, links to a ModulePreset
dormancy_days: number // R1.8: 0 for Today note feeds (never dormant)
config: Record<string, unknown>
}[]
rollover_time: string // HH:MM local time, default "00:00"
auto_generate: boolean // default true
}
```
The Today template is stored at `ELNOR_MEMORY/settings/today_template.json`. EC reads this on Today note generation (§6.2B). ModulePreset storage: `ELNOR_MEMORY/module_presets/presets_current.json`.
## 7.7 Note body
**R1.6 annotation:** `body_tiptap_json` is ProseMirror JSONContent, validated by Tiptap on load. Invalid JSON is a hard error — EC rejects the save with `NOTE_REVISION_CONFLICT`.
See §6.3.2 for `NoteBodyCurrentSchema`.
## 7.8 Note revision event
```ts
interface NoteRevisionEventSchema {
revision_id: string
note_id: string
base_revision_id: string // R1.6: enables audit trail reconstruction
kind: NoteRevisionKind
body_tiptap_json: TiptapJSONContent
plain_text: string
excerpt: string
word_count: number
author: string
summary: string | null
created_at: string
}
```
## 7.9 Note comment
See §6.6.2 for `NoteCommentCurrentSchema` and `CommentAnchor`.
```ts
type NoteCommentEventOp = "add" | "reply" | "edit" | "resolve" | "reopen" | "delete"
interface NoteCommentEventSchema {
event_id: string
note_id: string
comment_id: string
op: NoteCommentEventOp
thread_root_id: string
parent_comment_id: string | null
anchor: CommentAnchor | null
body: string | null
author: string
created_at: string
}
```
## 7.10 Tracked change
See §6.10.3 for `TrackedChangeSetSchema` and `TrackedChangeOpSchema`.
## 7.11 Note link
```ts
interface NoteLinkSchema {
source_note_id: string
target_type: BrowserItemType
target_id: string
link_text: string
position: number
created_at: string
}
```
## 7.12 Browser item read model
```ts
interface BrowserItemSchema {
item_type: BrowserItemType
item_id: string
title: string
subtitle: string | null
excerpt: string | null
path: string | null
project_id: string | null
folder_id: string | null
collection_ids: string[]
pinned: boolean
unread: boolean
status: BrowserItemStatus | null // R2: typed from canonical enum
is_generated: boolean
updated_at: string
created_at: string
last_used_at: string | null
deep_link: { route: string; params: Record<string, string> }
}
```
## 7.13 Generated artifact
```ts
interface GeneratedArtifactSchema {
artifact_id: string
title: string
artifact_kind: "docx" | "pdf" | "markdown" | "html" | "json" | "text" | "other"
origin_type: "chat" | "task" | "panel" | "forum" | "automation"
origin_id: string
project_id: string | null
folder_id: string | null
path: string | null
created_at: string
updated_at: string
}
```
## 7.14 Folder
**R2 addition:** Add `item_count: number` to `FolderSchema`. EC updates this materialized count on `FolderMemberAddCommand` / `FolderMemberRemoveCommand`.
See §3.15.2 for `BrowserFolderSchema` and `BrowserFolderMembershipSchema`.
## 7.15 Artifact version
**R2: Artifact version model clarification.** Artifacts have ONE stable `artifact_id` (the root). Versions are children with unique `version_id`. The version dropdown shows versions within a single artifact root. `DocumentReviewResultEvent` returns `artifact_id` (unchanged root) + `new_version_id`, NOT `new_artifact_id`.
```ts
interface ArtifactVersionSchema {
artifact_root_id: string // stable across all versions
version_id: string // unique per version
parent_version_id: string | null // null for first version
version_number: number // 1, 2, 3...
version_type: "major" | "minor" // from R1.6 major/minor rules
artifact_kind: ArtifactKind
format: string
origin_type: ArtifactOriginType
origin_id: string
size_bytes: number
content_hash: string
created_at: string
created_by: string
}
```
See §6.16.10 for `ArtifactVersionSchema`.
## 7.16 Artifact review
See §6.16.8 for `ArtifactReviewRequest` and `DocumentReviewResultEvent`.
## 7.17 Artifact comments
Artifact comments reuse the same `NoteCommentCurrentSchema` and `NoteCommentEventSchema` from §6.6.2, with `note_id` generalized to a `target_id` + `target_type` pattern:
```ts
interface CommentTargetRef {
target_type: "note" | "artifact"
target_id: string // note_id or artifact_id
}
```
Storage follows the same pattern: `comments_current.json` + `comments_events.jsonl` under the artifact's `by_id/` directory.
## 7.18 Attachment (R1.4)
**R2 schema split:** The attachment model uses two schemas:
- `AttachmentBlobSchema` — physical file identity (blob_id, content_hash, stored_path, size_bytes, mime_type, availability_status, stale_reason, last_verified_at). One record per unique file. Deduped by content_hash.
- `AttachmentRefSchema` — parent linkage (ref_id, blob_id, parent_type, parent_id, project_id, status, created_at, created_by, doc_index_id). Many refs can point to one blob. Deleting a ref does NOT delete the blob if other refs exist.
Storage: `ELNOR_MEMORY/attachments/blobs_current.json` and `ELNOR_MEMORY/attachments/refs_current.json`.
**R1.6 clarification:** The file on disk is shared (dedup by `content_hash`). Each `AttachmentReferenceSchema` is a separate record pointing to the file. Multiple references may share the same `content_hash` and `file_path`. The file is not duplicated on disk. Each reference is independently deletable without affecting other references to the same file. `parent_entity_id` identifies which entity owns that reference, not the underlying file.
**R1.6 additions to AttachmentReferenceSchema:** Add `availability_status: "available" | "stale" | "missing"`, `stale_reason?: string`, `last_verified_at: string`.
```ts
interface AttachmentSchema {
attachment_id: string
filename: string
mime_type: string
size_bytes: number
storage_mode: "reference" | "stored"
source_path: string | null // for reference mode
content_hash: string
stored_path: string | null // for stored mode
parent_type: "chat_message" | "note" | "task" | "room_message" | "forum_post"
parent_id: string
created_at: string
created_by: string // "user" | agent_id
project_id: string | null
doc_index_id: string | null
status: "active" | "orphaned" | "archived"
}
```
Storage: `ELNOR_MEMORY/attachments/attachments_current.json` (atomic) + `attachments_events.jsonl` (append-only) + `by_id/{id}/{filename}` (binaries for stored mode).
## 7.19 Artifact creation event (R1.4)
```ts
interface ArtifactCreationEvent {
event_type: "artifact_created"
artifact_id: string
title: string
artifact_kind: ArtifactKind // R1.6: use canonical enum from §7.0
format: string
origin_type: ArtifactOriginType // R1.6: use canonical enum from §7.0
origin_id: string
origin_message_id: string | null
agent_id: string
project_id: string | null
version_number: number
parent_artifact_id: string | null
size_bytes: number
estimated_tokens: number
content_hash: string
doc_index_registered: boolean
correlation_id: string // R1.6: links to originating command_id or conversation turn
source_message_id?: string // R1.6: for chat-originated artifacts
created_at: string
}
```
---
## 8. Commands and write flows
### 8.0 Command envelope
All EC commands use this envelope:
```ts
interface ECCommandEnvelope<T> {
command_id: string
command_type: string
timestamp: string
source: "q_ui" | "agent" | "system" | "cli"
actor: string
idempotency_key?: string // R1.6: when present, EC deduplicates by key
payload: T
}
// R1.6: Every EC command returns ECCommandResult<T>
interface ECCommandResult<T> {
command_id: string
status: "ok" | "error" | "idempotent_replay"
data: T | null
error: {
code: DOC20ErrorCode
message: string
details?: Record<string, unknown>
} | null
timestamp: string
}
```
### 8.1 Browser / collection commands
```ts
type CollectionCreateCommand = ECCommandEnvelope<{ title: string; color_token: string; show_in_quick_row: boolean }>
type CollectionUpdateCommand = ECCommandEnvelope<{ collection_id: string; updates: Partial<CollectionSchema> }>
type CollectionDeleteCommand = ECCommandEnvelope<{ collection_id: string }>
type CollectionEdgeAddCommand = ECCommandEnvelope<{ collection_id: string; item_type: BrowserItemType; item_id: string }>
type CollectionEdgeRemoveCommand = ECCommandEnvelope<{ collection_id: string; item_type: BrowserItemType; item_id: string }>
type SavedViewCreateCommand = ECCommandEnvelope<SavedViewSchema>
type SavedViewUpdateCommand = ECCommandEnvelope<{ saved_view_id: string; updates: Partial<SavedViewSchema> }>
type SavedViewDeleteCommand = ECCommandEnvelope<{ saved_view_id: string }>
```
### 8.2 Project commands
**R1.6:** `ProjectUpdateCommand` uses `ProjectUpdatableFields`, not `Partial<ProjectSchema>`:
```ts
interface ProjectUpdatableFields {
title?: string
description?: string
color_token?: string
status?: ProjectStatus
default_agent_id?: string | null
default_model_id?: string | null
fallback_chain_id?: string | null
think_level?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"
default_overlay_ids?: string[]
output_folder_path?: string | null
budget_enabled?: boolean
budget_amount_usd?: number | null
budget_period?: "day" | "week" | "month" | "quarter" | null
budget_alert_threshold_pct?: number | null
show_budget_in_header?: boolean
background_instructions?: string | null // R2
}
```
```ts
type ProjectCreateCommand = ECCommandEnvelope<{ title: string; description?: string; color_token?: string; default_agent_id?: string }>
type ProjectUpdateCommand = ECCommandEnvelope<{ project_id: string; updates: ProjectUpdatableFields }>
type ProjectArchiveCommand = ECCommandEnvelope<{ project_id: string }>
type ProjectDeleteCommand = ECCommandEnvelope<{ project_id: string; content_action: "detach" | "delete" }>
type ProjectBucketAttachCommand = ECCommandEnvelope<{ project_id: string; bucket_id: string }>
type ProjectBucketDetachCommand = ECCommandEnvelope<{ project_id: string; bucket_id: string }>
type ProjectMemberAddCommand = ECCommandEnvelope<{ project_id: string; member_type: ProjectMemberType; member_id: string }>
type ProjectMemberRemoveCommand = ECCommandEnvelope<{ project_id: string; member_type: ProjectMemberType; member_id: string }>
type ProjectMemberMoveCommand = ECCommandEnvelope<{ member_type: ProjectMemberType; member_id: string; from_project_id: string | null; to_project_id: string }>
```
### 8.3 Place commands
```ts
type PlaceAddCommand = ECCommandEnvelope<{ path: string; title?: string }>
type PlaceRefreshCommand = ECCommandEnvelope<{ place_id: string }>
type PlaceRemoveCommand = ECCommandEnvelope<{ place_id: string }>
type LinkedFolderAddCommand = ECCommandEnvelope<{ project_id: string; place_id: string; mode: LinkedFolderMode }>
type LinkedFolderRemoveCommand = ECCommandEnvelope<{ linked_folder_id: string }>
type LinkedFolderSyncCommand = ECCommandEnvelope<{ linked_folder_id: string }>
```
### 8.4 Folder commands
```ts
type FolderCreateCommand = ECCommandEnvelope<{ title: string; parent_folder_id?: string | null }>
type FolderRenameCommand = ECCommandEnvelope<{ folder_id: string; title: string }>
type FolderMoveCommand = ECCommandEnvelope<{ folder_id: string; new_parent_folder_id: string | null }>
type FolderDeleteCommand = ECCommandEnvelope<{ folder_id: string }>
type FolderMemberAddCommand = ECCommandEnvelope<{ folder_id: string; item_type: BrowserItemType; item_id: string }>
type FolderMemberRemoveCommand = ECCommandEnvelope<{ folder_id: string; item_type: BrowserItemType; item_id: string }>
```
### 8.5 Note commands
```ts
type NoteCreateCommand = ECCommandEnvelope<{ title: string; project_id?: string | null; note_kind?: NoteKind; content?: string; template_id?: string | null // R2: when present, EC reads template body as initial content
}>
type NoteSaveRevisionCommand = ECCommandEnvelope<{ note_id: string; base_revision_id: string; body_tiptap_json: TiptapJSONContent; plain_text: string; excerpt: string; word_count: number }>
type NoteMetadataUpdateCommand = ECCommandEnvelope<{ note_id: string; updates: { title?: string; project_id?: string | null; pinned?: boolean; note_kind?: NoteKind; folder_id?: string | null } }>
type NoteArchiveCommand = ECCommandEnvelope<{ note_id: string }>
type NoteDeleteCommand = ECCommandEnvelope<{ note_id: string }>
type NoteRestoreRevisionCommand = ECCommandEnvelope<{ note_id: string; target_revision_id: string }>
type NoteImportCommand = ECCommandEnvelope<{ filename: string; format: "docx" | "markdown"; title: string; project_id?: string | null; body_tiptap_json: TiptapJSONContent; plain_text: string }>
type NoteCommentAddCommand = ECCommandEnvelope<{ note_id: string; anchor: CommentAnchor; body: string }>
type NoteCommentReplyCommand = ECCommandEnvelope<{ note_id: string; parent_comment_id: string; body: string }>
type NoteCommentUpdateCommand = ECCommandEnvelope<{ note_id: string; comment_id: string; body: string }>
type NoteCommentResolveCommand = ECCommandEnvelope<{ note_id: string; comment_id: string }>
type NoteCommentReopenCommand = ECCommandEnvelope<{ note_id: string; comment_id: string }>
type NoteCommentDeleteCommand = ECCommandEnvelope<{ note_id: string; comment_id: string }>
type NoteTrackedChangeAcceptCommand = ECCommandEnvelope<{ note_id: string; change_set_id: string; base_revision_id: string }>
type NoteTrackedChangeRejectCommand = ECCommandEnvelope<{ note_id: string; change_set_id: string; base_revision_id: string }>
type NoteTrackedChangeAcceptAllCommand = ECCommandEnvelope<{ note_id: string; base_revision_id: string }>
type NoteTrackedChangeRejectAllCommand = ECCommandEnvelope<{ note_id: string; base_revision_id: string }>
type NoteLinkRebuildCommand = ECCommandEnvelope<{ note_id: string; body_tiptap_json: TiptapJSONContent }>
type NoteExportCommand = ECCommandEnvelope<{ note_id: string; format: "markdown" | "docx" | "pdf"; save_mode: "download" | "save_to_disk"; target_path?: string | null }>
```
**R1.7 additions to §8.5 — Block commands:**
```ts
type BlockInsertCommand = ECCommandEnvelope<{
note_id: string
base_revision_id: string
position: number // index in block sequence
block_type: NoteBlockType
block_data: TaskListBlockData | ActivityFeedBlockData | InlineThreadBlockData | BarBlockData
}>
type BlockDeleteCommand = ECCommandEnvelope<{
note_id: string
base_revision_id: string
block_id: string
}>
type BlockReorderCommand = ECCommandEnvelope<{
note_id: string
base_revision_id: string
block_id: string
new_position: number
}>
// R2: EC validates that update fields are compatible with the block's block_type.
// If a TaskList update is sent for an ActivityFeed block, EC returns BLOCK_TYPE_MISMATCH.
type BlockUpdateCommand = ECCommandEnvelope<{
note_id: string
base_revision_id: string
block_id: string
updates: Partial<TaskListBlockData | ActivityFeedBlockData | InlineThreadBlockData | BarBlockData>
}>
type InlineThreadCreateCommand = ECCommandEnvelope<{
note_id: string
base_revision_id: string
position: number
context_quote: string | null
initial_message: string
agent_id: string
}>
// Creates an InlineThread block and dispatches the initial message to the agent.
// Agent response is appended as a ThreadMessage via NoteWriteSkill.
type InlineThreadReplyCommand = ECCommandEnvelope<{
note_id: string
block_id: string
body: string
mention_agent_id: string | null // if @mention, routes to agent after saving
}>
type TodayNoteRolloverCommand = ECCommandEnvelope<{
previous_today_note_id: string
new_today_note_id: string
carry_forward_task_ids: string[]
}>
// EC executes on new day: renames yesterday, moves to Daily Notes folder,
// creates new Today note from template, copies unchecked tasks.
```
**R1.7 per-command error table additions:**
| Command | Possible Errors |
|---|---|
| `BlockInsertCommand` | `NOTE_NOT_FOUND`, `NOTE_REVISION_CONFLICT`, `NOTE_WRITE_QUEUE_BUSY` |
| `BlockDeleteCommand` | `NOTE_NOT_FOUND`, `NOTE_REVISION_CONFLICT` |
| `BlockReorderCommand` | `NOTE_NOT_FOUND`, `NOTE_REVISION_CONFLICT` |
| `BlockUpdateCommand` | `NOTE_NOT_FOUND`, `NOTE_REVISION_CONFLICT`, `NOTE_WRITE_QUEUE_BUSY` |
| `InlineThreadCreateCommand` | `NOTE_NOT_FOUND`, `NOTE_REVISION_CONFLICT` |
| `InlineThreadReplyCommand` | `NOTE_NOT_FOUND`, `COMMENT_NOT_FOUND` |
| `TodayNoteRolloverCommand` | `NOTE_NOT_FOUND` |
**R2 additions to §8:**
```ts
// === MODULE PRESET COMMANDS (R2) ===
type ModulePresetCreateCommand = ECCommandEnvelope<{
name: string
icon: string
description: string
category: "custom" // only custom presets can be created by users
source: SystemEventSource | AgentFeedSource
default_max_items: number
default_refresh_interval_minutes: number
default_dormancy_days: number
}>
type ModulePresetUpdateCommand = ECCommandEnvelope<{
preset_id: string
updates: Partial<Pick<ModulePreset,
"name" | "icon" | "description" | "source" |
"default_max_items" | "default_refresh_interval_minutes" | "default_dormancy_days"
>>
}>
// System/agent presets: returns PRESET_READONLY
type ModulePresetDeleteCommand = ECCommandEnvelope<{
preset_id: string // custom presets only; system/agent presets return PRESET_READONLY
}>
type ModulePresetHideCommand = ECCommandEnvelope<{
preset_id: string
hidden: boolean // works on any preset including system presets
}>
interface ModulePresetListReadCall {
op: "list_module_presets"
include_hidden?: boolean
}
interface ModulePresetListResponse {
presets: ModulePreset[]
hidden_preset_ids: string[]
}
// === FEED REFRESH COMMAND (R2) ===
type FeedRefreshCommand = ECCommandEnvelope<{
note_id: string
block_id: string
source: SystemEventSource | AgentFeedSource
max_items: number
reason: "note_open" | "interval_elapsed" | "manual_refresh"
}>
// For system events: EC reads activity_log.jsonl, applies filters, returns last N matching.
// For agent feeds: EC dispatches instruction to agent, parses JSON response into FeedItem[].
// Result cached at feed_cache/{block_id}.json.
interface FeedRefreshResult {
block_id: string
items: FeedItem[]
refreshed_at: string
source_type: "system_events" | "agent_instruction"
}
// === DOCUMENT VIEWER SELECTION AI COMMAND (R2) ===
type DocumentSelectionAICommand = ECCommandEnvelope<{
request_id: string
artifact_id: string
base_version_id: string
action_type: "ask" | "rewrite" | "expand" | "shorten"
selection: {
anchor_type: "text" | "line" | "page"
from?: number
to?: number
line_start?: number
line_end?: number
page_number?: number
quote_text: string
context_before?: string | null
context_after?: string | null
}
instruction: string | null
agent_id: string
}>
// EC dispatches to agent, creates new artifact version with tracked changes. Viewer shows inline diff.
// === NOTE AI PREVIEW / CANCEL COMMANDS (R2) ===
type NoteAIPreviewAcceptCommand = ECCommandEnvelope<{
request_id: string
note_id: string
}>
// Applies the previewed result (creates tracked change or inserts text). State → applied.
type NoteAIPreviewRejectCommand = ECCommandEnvelope<{
request_id: string
note_id: string
}>
// Discards the preview. State → cancelled.
type NoteAICancelCommand = ECCommandEnvelope<{
request_id: string
note_id: string
}>
// Aborts in-flight gateway request. State → cancelled.
// === COMMENT RE-ANCHOR COMMAND (R2) ===
type NoteCommentReanchorCommand = ECCommandEnvelope<{
note_id: string
comment_id: string
new_anchor: {
anchor_type: "text"
from: number
to: number
quote_text: string
context_before: string
context_after: string
block_id?: string
}
}>
// Updates anchor_status → "active", applies new position fields.
// Error: COMMENT_NOT_FOUND
// === TRACKED CHANGE OVERLAP RESOLVE (R2) ===
type TrackedChangeOverlapResolveCommand = ECCommandEnvelope<{
note_id: string
conflict_id: string
resolution: "replace_existing" | "keep_existing" | "apply_both"
existing_change_set_id: string
incoming_change_set_id: string
}>
// Applies chosen resolution, creates new revision with merged result.
// === NOTE TEMPLATE SAVE COMMAND (R2) ===
type NoteTemplateSaveCommand = ECCommandEnvelope<{
source_note_id: string
title?: string // default: "{source_title} Template"
}>
// EC copies note body to new note in Templates folder, stripping comments/tracked changes. Returns { note_id }.
// === PROJECT DUPLICATE COMMAND (R2) ===
type ProjectDuplicateCommand = ECCommandEnvelope<{
source_project_id: string
title?: string
duplicate_mode: "settings_only" | "settings_and_membership"
}>
// Returns { new_project_id }. Deep-copies selected content.
// === PLACE RENAME COMMAND (R2) ===
type PlaceRenameCommand = ECCommandEnvelope<{
place_id: string
new_title: string
}>
// === DOC20 SETTINGS COMMAND (R2) ===
type DOC20SettingsUpdateCommand = ECCommandEnvelope<{
default_note_advisor_agent_id?: string
default_review_agent_id?: string
today_note_auto_generate?: boolean
today_note_rollover_time?: string // HH:MM
}>
// Storage: ELNOR_MEMORY/settings/doc20_settings.json
// === TODAY NOTE RESOLVE READ CALL (R2) ===
// (Schema defined in §6.2B.4A)
// === RECENTLY DELETED COMMANDS (R2) ===
type NoteRestoreCommand = ECCommandEnvelope<{ note_id: string }>
type ProjectRestoreCommand = ECCommandEnvelope<{ project_id: string }>
type ArtifactRestoreCommand = ECCommandEnvelope<{ artifact_id: string }>
// Sets deleted: false, deleted_at: null, status: "active". Item reappears in browser.
type NotePermanentDeleteCommand = ECCommandEnvelope<{ note_id: string }>
type ProjectPermanentDeleteCommand = ECCommandEnvelope<{ project_id: string }>
type ArtifactPermanentDeleteCommand = ECCommandEnvelope<{ artifact_id: string }>
// Irreversible. Removes all data (body, revisions, comments, tracked changes, feed cache).
// Only works on items already in deleted state. Returns error if item is active.
// === BROWSER CATALOG REBUILD (R2) ===
type BrowserCatalogRebuildCommand = ECCommandEnvelope<{
reason: "manual" | "corruption_detected" | "first_launch"
}>
// EC regenerates browser_catalog_current.json from all 13+ sources synchronously.
// Returns { item_count, source_count, duration_ms }.
// === ARTIFACT VERSION HISTORY READ (R2) ===
interface ArtifactVersionHistoryRequest {
artifact_root_id: string
include_minor?: boolean // default: false (show majors only)
limit?: number // default: 20
cursor?: string | null // for pagination
}
interface ArtifactVersionHistoryResponse {
artifact_root_id: string
versions: ArtifactVersionSchema[]
has_more: boolean
}
```
**R2 per-command error table additions:**
| Command | Possible Errors |
|---|---|
| `ModulePresetCreateCommand` | `UNKNOWN_ERROR` |
| `ModulePresetUpdateCommand` | `PRESET_NOT_FOUND`, `PRESET_READONLY` |
| `ModulePresetDeleteCommand` | `PRESET_NOT_FOUND`, `PRESET_READONLY` |
| `FeedRefreshCommand` | `NOTE_NOT_FOUND`, `FEED_REFRESH_FAILED` |
| `DocumentSelectionAICommand` | `ARTIFACT_NOT_FOUND`, `ARTIFACT_VERSION_CONFLICT` |
| `NoteAIPreviewAcceptCommand` | `NOTE_NOT_FOUND` |
| `NoteAIPreviewRejectCommand` | `NOTE_NOT_FOUND` |
| `NoteAICancelCommand` | `NOTE_NOT_FOUND` |
| `NoteCommentReanchorCommand` | `NOTE_NOT_FOUND`, `COMMENT_NOT_FOUND` |
| `TrackedChangeOverlapResolveCommand` | `NOTE_NOT_FOUND`, `TRACKED_CHANGE_NOT_FOUND` |
| `NoteTemplateSaveCommand` | `NOTE_NOT_FOUND`, `TEMPLATE_NOT_FOUND` |
| `ProjectDuplicateCommand` | `PROJECT_NOT_FOUND` |
| `PlaceRenameCommand` | `PLACE_NOT_FOUND` |
| `DOC20SettingsUpdateCommand` | `UNKNOWN_ERROR` |
| `NoteRestoreCommand` | `NOTE_NOT_FOUND` |
| `NotePermanentDeleteCommand` | `NOTE_NOT_FOUND` |
| `BrowserCatalogRebuildCommand` | `UNKNOWN_ERROR` |
**R1.6 additions to §8.5:**
```ts
type NoteDuplicateCommand = ECCommandEnvelope<{
source_note_id: string
title?: string // default: "{source_title} (copy)"
project_id?: string | null // default: same as source
folder_id?: string | null // default: same as source
}>
// EC reads source note body, creates new note with copied body. Comments/tracked changes NOT copied. Version history starts fresh.
type NoteReviewCommand = ECCommandEnvelope<{
note_id: string
base_revision_id: string
agent_id: string
chat_id: string
instruction: string
scope: "full" | "comments_only" | "selected_comments"
selected_comment_ids: string[]
output_mode: "respond_in_chat" | "tracked_changes" | "revised_copy" | "respond_in_comments"
}>
// Drawer-initiated "Send to Agent" for notes. See §6.15.2A for full schema.
```
**R1.6 per-command error table for §8.5:**
| Command | Possible Errors |
|---|---|
| `NoteCreateCommand` | `PROJECT_NOT_FOUND`, `FOLDER_NOT_FOUND`, `UNKNOWN_ERROR` |
| `NoteSaveRevisionCommand` | `NOTE_NOT_FOUND`, `NOTE_REVISION_CONFLICT`, `NOTE_WRITE_QUEUE_BUSY`, `NOTE_LOCKED`, `NOTE_DELETED` |
| `NoteMetadataUpdateCommand` | `NOTE_NOT_FOUND`, `NOTE_DELETED`, `PROJECT_NOT_FOUND`, `FOLDER_NOT_FOUND`, `FOLDER_DEPTH_EXCEEDED` |
| `NoteArchiveCommand` | `NOTE_NOT_FOUND`, `NOTE_DELETED` |
| `NoteDeleteCommand` | `NOTE_NOT_FOUND` |
| `NoteRestoreRevisionCommand` | `NOTE_NOT_FOUND`, `NOTE_DELETED` |
| `NoteImportCommand` | `NOTE_EXPORT_FAILED`, `PROJECT_NOT_FOUND` |
| `NoteCommentAddCommand` | `NOTE_NOT_FOUND`, `COMMENT_ANCHOR_ORPHANED` |
| `NoteCommentReplyCommand` | `NOTE_NOT_FOUND`, `COMMENT_NOT_FOUND`, `COMMENT_THREAD_INVALID` |
| `NoteCommentResolveCommand` | `NOTE_NOT_FOUND`, `COMMENT_NOT_FOUND` |
| `NoteCommentDeleteCommand` | `NOTE_NOT_FOUND`, `COMMENT_NOT_FOUND` |
| `NoteTrackedChangeAcceptCommand` | `NOTE_NOT_FOUND`, `TRACKED_CHANGE_NOT_FOUND`, `NOTE_REVISION_CONFLICT`, `PENDING_CHANGE_OVERLAP` |
| `NoteExportCommand` | `NOTE_NOT_FOUND`, `NOTE_EXPORT_FAILED` |
| `NoteDuplicateCommand` | `NOTE_NOT_FOUND` |
| `NoteReviewCommand` | `NOTE_NOT_FOUND`, `NOTE_REVISION_CONFLICT`, `NOTE_WRITE_QUEUE_BUSY` |
### 8.6 Note AI command
```ts
type NoteAIRequestCommand = ECCommandEnvelope<NoteAIRequest>
```
### 8.6A Document/Artifact commands (R1.4 — renamed and extended)
```ts
type DocumentReviewCommand = ECCommandEnvelope<DocumentReviewRequest>
type ArtifactConvertToNoteCommand = ECCommandEnvelope<{ artifact_id: string; project_id?: string | null }>
type ArtifactCommentAddCommand = ECCommandEnvelope<{ artifact_id: string; anchor: CommentAnchor; body: string }>
type ArtifactCommentReplyCommand = ECCommandEnvelope<{ artifact_id: string; parent_comment_id: string; body: string }>
type ArtifactCommentUpdateCommand = ECCommandEnvelope<{ artifact_id: string; comment_id: string; body: string }>
type ArtifactCommentResolveCommand = ECCommandEnvelope<{ artifact_id: string; comment_id: string }>
type ArtifactCommentReopenCommand = ECCommandEnvelope<{ artifact_id: string; comment_id: string }>
type ArtifactCommentDeleteCommand = ECCommandEnvelope<{ artifact_id: string; comment_id: string }>
type NoteUnarchiveCommand = ECCommandEnvelope<{ note_id: string }>
```
**R1.6 additions — ArtifactDiff commands:**
```ts
type ArtifactDiffAcceptCommand = ECCommandEnvelope<{
artifact_id: string; version_id: string; diff_id: string; base_revision_id: string
}>
type ArtifactDiffRejectCommand = ECCommandEnvelope<{
artifact_id: string; version_id: string; diff_id: string; base_revision_id: string
}>
type ArtifactDiffAcceptAllCommand = ECCommandEnvelope<{
artifact_id: string; version_id: string; base_revision_id: string
}>
type ArtifactDiffRejectAllCommand = ECCommandEnvelope<{
artifact_id: string; version_id: string; base_revision_id: string
}>
```
**R1.6 per-command error table for §8.6A:**
| Command | Possible Errors |
|---|---|
| `DocumentReviewRequestCommand` | `ARTIFACT_NOT_FOUND`, `ARTIFACT_VERSION_CONFLICT` |
| `ArtifactDiffAcceptCommand` | `ARTIFACT_NOT_FOUND`, `ARTIFACT_DIFF_INVALID` |
| `ArtifactDiffRejectCommand` | `ARTIFACT_NOT_FOUND`, `ARTIFACT_DIFF_INVALID` |
| `ArtifactDiffAcceptAllCommand` | `ARTIFACT_NOT_FOUND`, `ARTIFACT_DIFF_INVALID` |
| `ArtifactDiffRejectAllCommand` | `ARTIFACT_NOT_FOUND`, `ARTIFACT_DIFF_INVALID` |
**R1.6 review command naming:** All review commands use `DocumentReview*` naming. Remove any `ArtifactReview*` references.
### 8.6B Artifact creation commands (R1.4)
```ts
type ArtifactCreateFromChatCommand = ECCommandEnvelope<{
conversation_id: string
message_id: string
title?: string
project_id?: string | null
}>
type ArtifactCreateCommand = ECCommandEnvelope<{
title: string
artifact_kind: "document" | "code" | "chart" | "data" | "image" | "mixed"
format: string
content: string
origin_type: "chat" | "task" | "room" | "panel" | "forum" | "chain" | "manual_save"
origin_id: string
agent_id: string
project_id?: string | null
}>
type ArtifactArchiveCommand = ECCommandEnvelope<{ artifact_id: string }>
type ArtifactDeleteCommand = ECCommandEnvelope<{ artifact_id: string }>
```
### 8.6C Attachment commands (R1.4)
```ts
type AttachmentUploadCommand = ECCommandEnvelope<{
filename: string
mime_type: string
parent_type: "chat_message" | "note" | "task" | "room_message" | "forum_post"
parent_id: string
storage_mode: "reference" | "stored"
source_path?: string | null // for reference mode
content_base64?: string | null // for stored mode (small files)
project_id?: string | null
}>
type AttachmentDeleteCommand = ECCommandEnvelope<{ attachment_id: string }>
type AttachmentOrphanCleanupCommand = ECCommandEnvelope<{ max_age_days: number }>
```
### 8.7 Browser query read contract
**R1.6 addition:** Add `show_nested?: boolean` to `BrowserQueryRequest`. When `true` and `primary_scope_family` is `"folder"` or `"place"`, the query returns items from the selected scope AND all descendant scopes. Default: `false`.
```ts
interface BrowserQueryReadCall {
op: "browser_query"
request: BrowserQueryRequest
}
```
### 8.8 Browser Context Menu Command Routing Table (R2 — rewritten)
**Routing table naming rule (R2):** Every non–Q-only cell must contain an exact command type name defined in DOC20 §8 or an exact command type name from the referenced owner spec. Placeholder labels are not acceptable. If a companion spec has not yet named the command, the cell reads `UNRESOLVED:[DOCn]` and is a blocker.
**Severity: CRITICAL — closes the largest phantom feature gap.**
This table maps every `(capability, item_type)` pair from `CAPABILITIES_BY_TYPE` (§3.10) to its command and owning spec. "Q-only" = client action, no EC command. "[DOCn]" = cross-doc obligation.
| Capability | note | chat | task | artifact | project | preset/overlay | agent | panel/forum | place |
|---|---|---|---|---|---|---|---|---|---|
| open | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only |
| pin | NoteMetadataUpdateCommand | [DOC10] | [DOC10] | [DOC10] | ProjectUpdateCommand | [DOC17] | [DOC3] | [DOC12] | — |
| archive | NoteArchiveCommand | [DOC10] | [DOC10] | — | ProjectArchiveCommand | — | — | [DOC12] | — |
| delete | NoteDeleteCommand | [DOC10] | [DOC10] | [DOC10] | ProjectDeleteCommand | [DOC17] | [DOC3] | [DOC12] | PlaceRemoveCommand |
| duplicate | NoteDuplicateCommand | — | — | [DOC10] | — | [DOC17] | [DOC3] | — | — |
| rename | NoteMetadataUpdateCommand | — | — | [DOC10] | ProjectUpdateCommand | [DOC17] | [DOC3] | — | PlaceRenameCommand |
| assign_project | NoteMetadataUpdateCommand | [DOC10] | [DOC10] | [DOC10] | — | — | — | [DOC12] | — |
| move_to_folder | NoteMetadataUpdateCommand {folder_id} | — | — | — | — | — | — | — | — |
| add_to_collection | CollectionEdgeAddCommand | CollectionEdgeAddCommand | CollectionEdgeAddCommand | CollectionEdgeAddCommand | CollectionEdgeAddCommand | CollectionEdgeAddCommand | CollectionEdgeAddCommand | CollectionEdgeAddCommand | — |
| export | NoteExportCommand | — | — | [DOC10] | — | [DOC17] | — | — | — |
| cancel | — | — | [DOC10] | — | — | — | — | — | — |
| copy_link | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only |
| copy_reference | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only | Q-only |
| show_in_finder | — | — | — | Q-only | — | — | — | — | Q-only |
| open_with_default | — | — | — | Q-only | — | — | — | — | — |
| attach_to_bucket | — | — | — | [DOC10] | — | — | — | — | — |
**Cross-doc obligations:** For every cell marked `[DOCn]`, that spec must define the command. DOC20 documents the expectation. If the command does not yet exist in the referenced spec, it becomes a DOC16 punch list item.
### 8.8A Project membership add flow (renumbered from 8.8)
1. User selects rows or drags payload.
2. Q opens move confirmation if needed.
3. Q emits `ProjectMemberAddCommand` or `ProjectMemberMoveCommand`.
4. EC writes owner `project_id` update + `ProjectMembershipEventSchema`.
5. Browser and project read models refresh.
6. Q shows toast.
### 8.9 Note AI apply flow
1. User submits `NoteAIRequestCommand`.
2. EC validates `base_revision_id`.
3. EC dispatches through Gateway/OpenClaw.
4. EC materializes tracked change sets / direct insertion / comments.
5. EC returns `NoteAIResultEvent`.
6. Q refreshes note state and editor decorations.
### 8.10 Export flow
**R1.6 — Pandoc export error handling:**
| Error | Code | User message |
|---|---|---|
| Pandoc not installed | `NOTE_EXPORT_FAILED` | "Export unavailable — Pandoc not found. Install via `brew install pandoc`." |
| Conversion failure | `NOTE_EXPORT_FAILED` | "Export failed — could not convert to {format}. Try a different format." |
| File write failure | `NOTE_EXPORT_FAILED` | "Export failed — could not save file to {path}. Check permissions." |
| Unsupported content | `NOTE_EXPORT_FAILED` | "Export completed with warnings — some content could not be converted." (non-blocking) |
For non-blocking errors (unsupported content), the export still produces a file with the convertible content. The warning toast shows which elements were dropped.
1. Q submits `NoteExportCommand`.
2. EC prepares intermediate markdown/html.
3. OpenClaw runs Pandoc for DOCX/PDF.
4. EC appends to `exports.jsonl`.
5. Q downloads or saves to disk.
### 8.10.1 Note version history read contract (R1.6)
```ts
interface NoteVersionHistoryRequest {
note_id: string
limit?: number // default: 50
before_revision_id?: string // for pagination
}
interface NoteVersionHistoryResponse {
note_id: string
revisions: NoteRevisionEventSchema[]
has_more: boolean
}
```
Q reads note version history by querying `notes/{note_id}/revisions/`. EC returns the revision list sorted by `created_at` descending.
### 8.10.2 Global comments index (R1.6)
EC maintains `system/comments_index.json`:
```ts
interface CommentsIndexEntry {
comment_id: string
entity_type: "note" | "artifact"
entity_id: string
entity_title: string
status: NoteCommentStatus
author: string
body_excerpt: string
created_at: string
updated_at: string | null
}
```
Purpose: Enables cross-surface comment queries ("show me all my open comments across all notes and documents") without reading every entity's comment file.
### 8.10.3 Chat selector population (R1.6)
The Send to Agent drawer chat selector dropdown (§6.15.10) is populated from `browser_catalog_current.json` filtered to `item_type: "chat"`, sorted by `updated_at` descending, limited to 20 most recent. The "origin" badge marks the chat that originally created or last referenced the current note (tracked via correlation_id or note creation context). "New chat" is always the last option.
### 8.10A Artifact review flow
1. User adds comments to artifact in the viewer.
2. User clicks "Send to {Agent}" and selects output mode.
3. Q submits `DocumentReviewCommand`.
4. EC reads artifact content + all open comments.
5. EC dispatches through Gateway/OpenClaw with artifact content, comments, instruction, and output mode.
6. Based on output mode:
- **revise_artifact**: EC creates new artifact version with changes applied. Returns `new_artifact_id`. Viewer refreshes to show new version with diff summary.
- **respond_in_chat**: EC returns response as chat message. Returns `chat_message_id`. Chat pane shows response.
- **convert_to_note_with_changes**: EC creates Note from artifact (§6.16.9), applies Elnor's revisions as tracked changes, migrates user comments to note comment rail. Returns `created_note_id`. Q opens Note editor.
7. EC returns `DocumentReviewResultEvent`.
8. Comments that were addressed are optionally auto-resolved (agent can mark specific `comments_addressed[]`).
### 8.10B Artifact convert-to-note flow
1. Q submits `ArtifactConvertToNoteCommand`.
2. EC reads artifact content.
3. EC converts to Tiptap JSON based on format (markdown direct, DOCX via mammoth, HTML sanitized, PDF text extraction, code as code block).
4. EC creates Note with `project_id` inherited from artifact, revision kind `import`.
5. If artifact has comments, EC migrates them to note comment rail with anchor remapping.
6. Q opens the new Note in the editor.
### 8.11 Deep-link routing rules
Every Browser row and BrowserQuerySkill result MUST use `deep_link`.
```ts
const DEFAULT_ROUTE_BY_TYPE: Record<BrowserItemType, string> = {
document: "document_viewer",
chat: "chat",
preset: "preset",
skill: "skill",
task: "task",
agent: "agent",
panel: "panel",
forum: "forum",
overlay: "overlay",
note: "note",
bucket: "bucket",
project: "project",
automation: "task",
artifact: "artifact",
place: "place",
}
```
---
## 9. React-facing implementation surface
## 9.1 Pages/components to create or extend
| Component | Location | Source spec |
|---|---|---|
| BrowserColumn | New component | §3 |
| BrowserScopeDetail | New component | §3.5 |
| BrowserPlacesBrowser | New component | §3.5.3A |
| BrowserFolderTree | New component | §3.15 |
| BrowserResultRow | New component | §3.9.1 |
| BrowserContextMenu | New component | §3.10 |
| BrowserBulkActionBar | New component | §3.9.4 |
| ProjectPage | New page | §4 |
| ProjectHomeTab | New component (notes workspace) | §4.3.2 |
| ProjectDocumentsTab | New component | §4.4 |
| ProjectActivityTab | New component | §4.5 |
| ProjectContextTab | New component | §4.6 |
| ProjectConfigureTab | New component | §4.7 |
| ManageMembersDrawer | New component | §4.7.3A |
| AddToProjectModal | New component | §4.7.3 |
| NotesPage | New page | §6.4 |
| NoteListPane | New component | §6.4 |
| NoteEditor | New component (Tiptap wrapper) | §6.5 |
| NoteEditorToolbar | New component | §6.5.3 |
| ReviewDropdown | New component | §6.10.4 |
| CommentRail | New component | §6.6 |
| CommentCard | New component | §6.6.4 |
| BubbleMenu | New component (floating) | §6.6.6 |
| TrackedChangeInline | New component (marks) | §6.10.2 |
| TrackedChangeContextMenu | New component | §6.10.8 |
| AgentSelector | New component | §6.15.8 |
| VersionHistoryDrawer | New component | §6.3.6 |
| ArtifactViewer | New page | §6.16 |
| ArtifactToolbar | New component | §6.16.4 |
| ArtifactRendererRegistry | New component | §6.16.5 |
| ArtifactSendToDrawer | New component | §6.16.8 |
## 9.2 Mockup artifacts produced
| Artifact | File | Coverage |
|---|---|---|
| **Q Unified Workspace V7.3** | Q_UNIFIED_WORKSPACE_V7_3.jsx (654 lines, 174KB) | **Current operative mockup.** Full workspace shell: 4-mode browser (Nav/Browser/Notes/Web), 8 tab types, transient utility tabs, right chat column, simplified left rail, Nav tab with conversations/activity/pages/open tabs, full block modules (task lists/threads/feeds with 3-step picker), tracked changes, full comment CRUD, Places/Folders as scopes with resizable splitter, Chrome-like bookmark management with favicon squares, Ask Agent panel with context card/include checkboxes/inline response, Chats management page, session system, IBM Plex Sans font. (R3) |
| Q Unified Workspace V7.2 | Q_UNIFIED_WORKSPACE_V7_2.jsx | Utility tab persistence fix, blue outline, Folders scope fix, [+] overflow fix |
| Q Unified Workspace V7.1 | Q_UNIFIED_WORKSPACE_V7_1.jsx | Tab bar polish, Places single-line, bookmark CRUD, splitter improvements |
| Q Unified Workspace V7 | Q_UNIFIED_WORKSPACE_V7.jsx | V5.1 base + Nav tab, chat/utility tabs, right chat column, simplified rail |
| Q Unified Workspace V5.1 | Q_UNIFIED_WORKSPACE_V5_1.jsx (570 lines, 157KB) | Full-featured base — all block modules, tracked changes, bookmark CRUD, note folder management |
| Q Unified Workspace V4 | Q_UNIFIED_WORKSPACE_V4.jsx | Comprehensive rebuild from all source mockups |
| Font variant: IBM Plex Sans | Q_UNIFIED_WORKSPACE_V7_1_ALT_C.jsx | Selected as working font |
| Font variant: Source Sans 3 | Q_UNIFIED_WORKSPACE_V7_1_ALT_F.jsx | Runner-up font |
| Browser single-line layout | Q_BROWSER_R13.jsx | §3 visual — browser column (source mockup) |
| Notes full | Q_NOTES_FULL.jsx | §6.6 + §6.10 — notes editor with comments/tracked changes (source mockup) |
| Document Viewer | Q_ARTIFACT_VIEWER.jsx | §6.16 — document viewer (source mockup) |
| Web Browser | Q_WEB_BROWSER_VIEW_V2.jsx | §6.19 — web browser with tabs/incognito/reader mode (source mockup) |
| Block Modules V5.2 | Q_WORKSPACE_V5_2.jsx | §6.2A — task lists, threads, feeds, module picker (source mockup) |
| Project page v2 | Q_PROJECT_V2.jsx | §4 visual |
---
## 10. Acceptance criteria
### 10.1 Browser
- Browser can be opened/closed and resized via right-edge drag handle (200px–450px).
- Browser items render as single-line rows at 32px height.
- Type label appears flush right before timestamp in type-specific color.
- Long titles truncate with ellipsis.
- Collections row renders between search and scope chips with 18px colored circles and + button.
- Collection quick row and Collection scope detail list mutate the same `selected_collection_ids[]`.
- Scrollbar is 4px thin and auto-hides when not hovering.
- Vertical splitter between scope detail and type chips is draggable.
- Search supports "This View" and "Everywhere" modes with correct universes.
- Scope detail list works for Project/Bucket/Place/Folders/Saved View.
- Places scope shows folder contents with subdirectory navigation and "Show nested" toggle.
- Folders scope shows expandable folder tree in scope detail.
- No Project appears under Project scope.
- Type chips display in two rows with adaptive visibility.
- Single click selects; double click opens.
- Cmd-click and Shift-click multi-select work.
- Multi-select bulk action bar appears with ≥2 selected rows.
- Arrow key navigation and type-ahead work.
- Right-click context menu shows capability-based actions per type.
- Documents can be dragged into attachment surfaces.
- Browser items can be dragged into note editor to create `[[links]]`.
- Browser items can be dragged onto folders in Folders scope.
- `Recent` built-in saved view resolves correctly. Default user views ("Deadlines this week", "Active matters") are seeded on first launch and are editable/deletable (R4).
- BrowserQuerySkill and Browser UI use the same `BrowserQueryRequest`/`BrowserQueryResponse`.
- All Browser rows include `deep_link`.
### 10.2 Projects
- Project page has five tabs: Home, Documents, Activity, Project Context, Configure.
- Home tab renders full notes workspace scoped to project notes.
- Tabs use connected box style with darker (#E8EAEE) inactive tabs.
- Persistent "Add to Project / Drop here" surface appears in header across all tabs.
- Cost indicator appears below add-to-project surface on right side.
- Project switch dropdown works from header.
- Project creation atomically creates primary project bucket and project record.
- Project status `paused` does not strip project context from existing members.
- Configure > General has editable name, description, color, status.
- Configure > Project Content table shows counts with "View in Browser →" and "Manage Members →".
- Configure > Defaults has agent, model, fallback chain dropdown, think level.
- Configure > Prompt & Overlay supports multiple overlays with add/remove.
- Configure > Output & Cost has full budget configurability (amount/period/on-off/hide).
- Configure > Project Management has Duplicate, Archive, Delete (with detach/delete choice).
- Documents subtab order: Generated · Context Docs · Notes · All.
- Context Docs shows source badges; bucket-inherited docs show "via bucket" without Remove.
- Activity subtabs are all clickable with item lists and Remove buttons.
- Note list in Home tab is collapsible.
- Generated artifacts have their own `project_id` and can be detached without mutating origin.
- Archive preserves everything, is recoverable. Delete prompts for detach or delete.
- Removing bucket-inherited document is not allowed on row; bucket must be detached at project level.
### 10.3 Notes
- Notes page exists in main menu.
- Notes can belong to a project or no project.
- Note title is editable.
- Note list shows date metadata and 1-line excerpts. Title 12.5px, excerpt 11px.
- Note list is collapsible.
- Note autosave uses 1200ms debounce, 15s max unsaved, flushes on blur/navigation.
- Tiptap editor supports core formatting, undo, redo, task lists.
- Highlighting text shows floating bubble menu with Comment, Agent selector, Ask {Agent}, Rewrite, Expand, Shorten.
- Clicking Comment creates anchored comment input in rail with quoted selection.
- Clicking comment in rail highlights corresponding text in editor.
- Clicking different comment moves highlight.
- Comment anchors survive ordinary edits through position remapping and fall back to orphaned state.
- Reply button expands textarea, Enter submits.
- Edit button converts body to textarea.
- Resolve moves to Resolved section. Reopen restores.
- Delete soft-deletes user-authored comments.
- General comments (unanchored) via bottom input.
- Comments show distinct author colors.
- Review dropdown contains Track manual edits toggle, Accept All, Reject All.
- Agent edits to existing text always create tracked change sets, regardless of "Track manual edits."
- Replacement edits are grouped delete+insert ops sharing one `change_set_id`.
- Right-clicking tracked change text shows Accept This/Reject This/Accept All/Reject All.
- Different agent authors show different deletion/insertion colors.
- `note_ai_request` action enums match visible UI actions.
- `base_revision_id` conflict enforced on save, accept/reject, and AI apply.
- Agent selector defaults to project default, overridable per-request, updates label dynamically.
- Cross-note `[[links]]` work via typing and browser drag.
- Backlinks section works.
- DOCX/MD import works.
- Export supports MD/DOCX/PDF download and save-to-disk.
- Version history shows revision log and allows restore.
- Live activity indicator shows when agent is editing.
### 10.4 Wiring completeness
- BrowserQuerySkill and Browser UI share same contracts.
- `open` filter depends on session overlay; absent `q_session_id` yields warning.
- Place refresh rescans availability without importing content.
- Places scope shows browseable folder contents with subdirectory navigation.
- Files from Places can be dragged onto project drop zones, buckets, note editor, and chat composer.
- Dragging from Places creates references, not copies.
- External drives, iCloud paths, and network mounts are supported with appropriate availability badges.
- Unsupported files in Places are visible but dimmed with "Not previewable" badge.
- `sync_to_project_bucket` creates DOC7 local-path refs, not file copies.
- All Browser rows and BrowserQuerySkill results include `deep_link`.
- Folder assignments are independent of project membership.
- Folder deletion detaches items (items survive).
### 10.5 Document Viewer
- Document Viewer opens from browser, chat pane, and project Generated subtab for any viewable file type.
- Toolbar has Download, Convert to Note, Copy Reference, Open External, Send to {Agent}.
- Renderer selects appropriate viewer based on artifact format (markdown, code, PDF, DOCX, HTML, image, JSON).
- DOCX renders via mammoth.js → HTML preview. PDF renders via PDF.js.
- Text-based artifacts support text selection with floating bubble menu (Comment, Ask {Agent}, Rewrite, Expand, Shorten).
- Comment rail works on artifacts with same anchor/thread model as Notes.
- PDF comments are page-level. Image comments are unanchored.
- AI rewrite on artifacts produces a new artifact version with diff preview (accept/reject).
- "Send to {Agent}" bundles artifact + all open comments into structured request with three output modes.
- "Revise artifact" creates new version. "Respond in chat" returns chat message. "Convert to note with changes" creates Note with tracked changes + migrated comments.
- "Convert to Note" creates a fork — original artifact preserved.
- Artifact versioning shows version history dropdown. Comments are per-version.
- Copy Reference copies `@[{title}](artifact:{artifact_id})` format.
---
## 11. Non-goals / deferrals
- global hidden project focus mode;
- generic in-browser rename/delete for arbitrary external files;
- full enterprise collaborative commenting / realtime presence;
- paid editor dependency as baseline;
- automatic bulk import of all files in Places;
- forcing every object type to support drag/drop on day one;
- separate visible Chats vs Rooms categories everywhere;
- OneDrive auto-sync (Phase 2);
- shareable read-only links (Phase 2);
- ~~note templates (deferred)~~ — **addressed in R1.6 §6.15.15B and R2 NoteTemplateSaveCommand.** Advanced template library with variables/placeholders remains deferred.
- filesystem watch mode for linked folders (deferred).
- **R1.6 deferrals (to R1.7 — Workspace Architecture):** **All addressed in R1.7.**
- ~~Block-based modular note architecture~~ → §6.2A
- ~~Today note as workspace home page~~ → §6.2B
- ~~Inline threads as durable comments~~ → §6.2A.4
- ~~@mention agents everywhere~~ → §6.2D
- ~~Notes scope in browser~~ → §3.5.8
- ~~Configurable bar blocks~~ → §6.2A.5
- **R1.6 deferrals (to DOC16 / future):**
- CRDT/Yjs (overkill for single-user-plus-agents)
- SQLite cache layer (violates EC single-writer architecture)
- Mobile/iPad story
- Legal-specific content types in §6.18.2
- Live sections in notes
- Drag-to-chat from browser (all entity types)
- Split compare view (future Document Viewer enhancement within DOC20)
- Case timeline widget
- **R4 deferrals (pending / not yet built):**
- Toolbar collapse behavior (Word-style responsive, deferred until TipTap integration)
- Web browser settings expansion (28 subsections specced in §6.19.24 but not in mockup)
- Native app companion windows (deferred)
- Version dropdown unification
- Right chat column (separate mockup to merge in)
- TipTap integration for notes editor
- Places: right-click "Add to collection", Cmd+click multi-select
- Snooze dropdown custom option (date/time picker)
- Notification delivery tiers wiring (EC → palette)
- Ask button payload serialization (to-do list → chat context, DOC24 integration)
- Chrome extension support UI
- Full notes/bookmark folder management (simplified in mockup)
- Palette: notification auto-show behavior (Electron window management)
- Palette: sound playback (Electron audio API)
- Palette: opacity reduction when unfocused + pinned
---
## 12. Cross-document obligations (DOC16 punch list additions)
| Owner Doc | Obligation | Status |
|---|---|---|
| DOC3 | Register NoteReadSkill, NoteWriteSkill (+ archive/unarchive), BrowserQuerySkill, ArtifactReadSkill with full operation schemas from §6.15.6 and §6.16 | new |
| DOC3 | Register ArtifactCommentSkill with add/reply/resolve/reopen/update/delete operations (§6.16.6) | new (R1.4) |
| DOC3 | Register DocumentViewerSkill extending ArtifactReadSkill to cover all file types (§6.16) | new (R1.4) |
| DOC3 | Register ArtifactWriteSkill: `create_artifact`, `save_chat_as_artifact`, `update_artifact_metadata` (§6.18.8) | new (R1.4) |
| DOC3 | Register AttachmentSkill: `upload_attachment`, `get_attachment`, `list_attachments` (§6.18.8) | new (R1.4) |
| DOC3 | `AgentPinCommand`, `AgentDuplicateCommand`, `AgentDeleteCommand` for browser context menu routing (§8.8) | new (R1.6) |
| EC Core | Add `artifact_ids: string[]` to conversation metadata schema. Implement artifact creation pipeline (§6.18.3). Call `ensureDocIndexEntry()` on artifact creation. | new (R1.4) |
| EC Core | Add `ELNOR_MEMORY/attachments/` to §2.2 directory structure. Implement attachment storage + dedup (§6.18.4). | new (R1.4) |
| EC Core | Implement browser read model materialization with incremental updates (§6.18.5). SLOs: <200ms cached, <250ms delta, <3s rebuild (§3.9.9). | updated (R1.6) |
| DOC10 | Add intent routing for note/browser/document operations. Classify "update my notes", "search browser", "review this document", "save this as an artifact" to appropriate skills | new |
| DOC10 | `ConversationPinCommand`, `ConversationArchiveCommand`, `TaskPinCommand`, `TaskCancelCommand`, and `assign_project`/`move_to_project`/`remove_from_project` for chats, tasks, documents, automations — required by browser context menu routing (§8.8) | new (R1.6) |
| DOC12 | `RoomPinCommand`, `RoomArchiveCommand`, and project assignment commands for panels/forums — required by browser context menu routing (§8.8) | new (R1.6) |
| DOC15 CIL | Add `note_reference` and `artifact_reference` signal types. Score by project scope, title match, edit recency | new |
| DOC7 | Document project primary bucket auto-creation flow. No changes to bucket storage needed — browser resolver reads DOC7 registry as one source. | new |
| DOC17 | Support multiple overlay assignment per project (§4.7.2). prompt_recipe.create command (DOC17 §12.2) for "Save as Prompt" flow (§6.18.9). | new |
| DOC17 | `PresetPinCommand`, `PresetDuplicateCommand`, `PresetDeleteCommand`, `OverlayDuplicateCommand`, `OverlayDeleteCommand`, `PresetExportCommand`, `OverlayExportCommand` — required by browser context menu routing (§8.8) | new (R1.6) |
| DOC21 | Add RendererCapabilities interface (§6.16.5A), version history panel UI spec, "Recently Deleted" saved view UI spec | new (R1.6) |
| DOC22 | Page 15 (Document Viewer): version drawer wireframe, diff accept/reject buttons, "Save as Prompt" modal | new (R1.6) |
| DOC22 | Page 14 (Notes): full-text search toggle, table support, "Save as Prompt" modal | new (R1.6) |
| DOC22 | Surface 1 (Browser): responsive chip collapse states, folder overlay DnD states, empty states, "Recently Deleted" view | new (R1.6) |
| Settings | Add Notes section with Default Note Advisor agent dropdown (§6.15.8). Add Document Viewer section with Default Review Agent dropdown. | new |
| Master UI Spec | R4.4 to register Document Viewer (renamed), tabbed panel, folder overlay, note list search/sort/archive, browser footer | new (R1.4) |
| DOC16 | Track: attachment garbage collection policy, cross-device attachment sync (Phase 2), attachment versioning for overwritten files | new (R1.4) |
| DOC16 | Track: Daily Note lifecycle, Focus mode, @mention agents, split compare view, case timeline widget, drag-to-chat, live note sections. Capture Q42 redesign visions in "v2 Architecture Notes" section. | new (R1.6) |
| DOC3 | Update NoteWriteSkill to support block-aware operations: BlockInsertCommand, BlockDeleteCommand, BlockReorderCommand, BlockUpdateCommand, InlineThreadCreateCommand, InlineThreadReplyCommand. Agent must understand block structure in Tiptap JSON. | new (R1.7) |
| DOC11 | Gateway routing for @mention invocations from inline threads. When InlineThreadCreateCommand or InlineThreadReplyCommand includes `mention_agent_id`, Gateway routes to the specified agent with thread context. | new (R1.7) |
| DOC15 CIL | Add `inline_thread_interaction` signal type for CIL capture. Inline thread creation and replies are high-value signals for context relevance. | new (R1.7) |
| DOC22 | Page 14 (Notes): Add +Module toolbar button wireframe, module preset picker, TaskList block rendering, ActivityFeed block with dormancy states, InlineThread block rendering, ConfigurableBar block rendering, Today note badge, collapsed block states, feed gear config panel. | new (R1.8 — updated from R1.7) |
| DOC22 | Surface 1 (Browser): Add Notes scope chip, note folder tree in scope detail area, note results with task count badge. | new (R1.7) |
| DOC16 | Track: Focus mode, split compare view, case timeline widget, drag-to-chat, live note sections. @mention agents now implemented in R1.7. Daily Note lifecycle now implemented in R1.7. | updated (R1.7) |
| DOC11 | Gateway must accept `source_context: { source_type: "inline_thread" | "note_comment" | "chat", note_id?, thread_id?, block_id? }` on gateway requests to distinguish inline thread messages from chat messages. | new (R2) |
| DOC10 | `ConversationPinCommand`, `TaskPinCommand`, `TaskCancelCommand`, `assign_project`/`move_to_project`/`remove_from_project` — exact command names required by §8.8 routing table. If undefined, mark as UNRESOLVED in routing table. | updated (R2) |
| DOC22 | Settings → Workspace page: auto-generate toggle, rollover time, link to Today Template note. | new (R2) |
| DOC11 | Note content endpoint for Gateway remote access (Phase 2) | deferred |
| DOC18 | Note and artifact content as semantic retrieval corpora (optional) | deferred |
| DOC72 | **Surface intake contracts — full specification needed.** (1) `intake.todo`: `obligation` nodes from tasks, `work_product` nodes from lists, entity resolution on names/text, deterministic extraction for structured fields, `subtask_of`/`belongs_to_list`/`references_document` edges, trigger on every CRUD command. (2) `intake.calendar`: `obligation` nodes from events with `event_type` categorization, entity resolution on title/location/participants/notes, `synced_to_calendar` edges, trigger on CRUD + Outlook sync. (3) `intake.notes`: LLM-assisted extraction — needs significance gate, extraction prompt, output node schema. (4) `intake.browser`: two-tier pipeline per browser intake proposal. See separate DOC72 Proposal: Surface Intake Contracts. | expanded (R4.1, was R4) |
| DOC24 | "Ask" button payload serialization: to-do list → chat context → EC → agent. Follows DOC24 contextual packet assembly. | new (R4) |
| DOC16 | Calendar module Outlook sources: calendar discovery, sync, shared calendar indicators via DOC16 Entry 16.7. | new (R4) |
| DOC3 | "Watch" quick action in palette sidebar triggers DOC3 demonstration/observation mode. | new (R4) |
| DOC21 | Icon system spec (Ic/I/TabIcon/NavIcon). Chrome-style tab bar. Tab group display:contents. Notification card anatomy. Palette sidebar. Calendar module. Split view. Zoom slider. Bookmarks bar. | new (R4) |
| DOC22 | Add: Notices (palette sidebar), Activity (palette sidebar). Remove: "⌘ Command" tab. Update all icon refs emoji→SVG. | new (R4) |
| EC Core | Add `ELNOR_MEMORY/todo_lists/`, `ELNOR_MEMORY/todo_items/`, `ELNOR_MEMORY/cal_events/`, `ELNOR_MEMORY/system/notifications/` to §2.2 directory structure. Implement CRUD commands. | new (R4) |
| EC Core | Notification store: create, read, mark-read, snooze, dismiss. Delivery matrix enforcement per source. | new (R4) |
| Settings | Add Notifications section with delivery matrix (§6.25.5): per-source Badge/Glow/Sound/Popup/Auto-show toggles, sound picker, focus hours, badge position. | new (R4) |
| **DOC25 (proposed)** | **New document: Real-Time Sync and Multi-Client Architecture.** Owns server-side sync transport (WebSocket/IPC push channel, fan-out), per-content-type conflict resolution strategy, chat streaming multiplexing, CRDT/Yjs integration plan for notes (Phase 2), offline queue for mobile, networking layer (multi-node topology, relay/tunnel, auth middleware, per-user identity, per-item permissions, invitation flow, access revocation), content-type sync matrix implementation. DOC20 §2.4–2.6 defines the surface contract; DOC25 defines the server contract. | **new (R4.1)** |
| EC Core | Implement surface registration table and push channel (§2.6.1–2.6.2). EC must track which surfaces have which content items open and push `ECStateUpdate` payloads on mutation. This is the foundation for multi-surface sync. | new (R4.1) |
| EC Core | Implement optimistic update reconciliation (§2.6.3). EC commands must return success/failure fast enough for optimistic UI. On rejection, EC pushes rollback to source surface. | new (R4.1) |
| EC Core | Implement note edit lock (§2.6.4 Phase 1). Lock acquisition, release on close/blur/idle, read-only fallback for locked notes. | new (R4.1) |
| EC Core | Implement agent availability status as subscribable state. Surfaces need to know when an agent is busy and what it's working on. | new (R4.1) |
| DOC11 | Agent contention protocol: define what happens when a second request arrives while the agent is busy. Options: queue with priority, interrupt with confirmation, spawn parallel session, reject. DOC20 surfaces will render whatever DOC11 decides. See §2.6.7. | new (R4.1) |
| DOC7 | Networked bucket replication strategy. Firm-shared vs personal buckets. Sync of bucket contents and file references across network nodes. Stale bucket prevention (critical for context assembly correctness). | new (R4.1) |
| DOC12 | Multi-user sync for panels and forums. When human participants join panel reviews or forum discussions, DOC12 needs the same real-time push contract as chats/rooms. Presence indicators for human participants. | new (R4.1) |
**Implementation phasing guidance (R1.6):** The recommended build order is: (1) Browser with Project scope, (2) Notes with basic editing, (3) Document Viewer with comment system, (4) Content Map, (5) Advanced browser features (Places, Saved Views, folder overlay). This is guidance, not a constraint — teams should start with the surface that unblocks the most downstream work.
---
## 13. Summary of DOC20 R4.3's key stance
DOC20 R4.2 builds on R4.1's centralized data architecture with entity resolution hierarchy clarifications and expanded intake contracts.
**R4.2 additions:**
- **Entity resolution hierarchy (§6.21.2, §6.21.7).** Task `text` is the primary entity resolution signal — each individual task ("Prepare expert report in Paramount") is resolved independently for matter/case, people, document types, and actions. List `name` is contextual — provides temporal framing ("February 8") or matter framing ("Henderson MTD Prep") that boosts confidence on task text matches. Subtask text inherits the parent task's resolved context. `project_id` is organizational grouping, independent of matter/case association via entity resolution.
- **Expanded extraction tables (§6.21.7, §6.22.8).** Reordered by priority with explicit hierarchy column. Context inheritance for subtasks documented.
- **DOC72 Proposal V2.** Updated with matching hierarchy, context inheritance edge rules, resolved schema gaps.
**R4.1 additions (retained):**
DOC20 R4.1 is the Centralized Data and Multi-Client Architecture revision. It builds on R4's surface additions with a foundational architectural principle:
**Surface Independence (§2.4–2.6).** Every Q rendering surface — main tabs, palette tabs, right chat column, note canvas modules, split view panes, future mobile — is a stateless viewport subscribing to EC-owned durable state. No surface owns data. All mutations flow through EC commands. EC pushes state updates to all subscribed surfaces.
The revision defines:
- A **three-tier content sync matrix** classifying all content types by sync requirements (Tier 1: multi-surface + networking; Tier 2: networking; Tier 3: config data).
- A **surface-side sync contract** covering surface registration, EC push channel, optimistic updates, note edit locking, rename propagation, selector population, and agent busy indicators.
- A **networking readiness analysis** showing that the data model does not change for mobile, collaboration, or multi-user — only transport, auth, and conflict resolution layers change, all owned by proposed DOC25.
- **Agent contention indicators** — surfaces show busy status and offer interrupt/queue options, with the contention protocol itself delegated to DOC11.
R4.1 also proposes **DOC25 (Real-Time Sync and Multi-Client Architecture)** as a new spec owning the server-side contract: push transport, WebSocket multiplexing, chat streaming fan-out, CRDT integration for collaborative note editing, offline queue for mobile, networking topology, auth/permissions, invitation flow. DOC20 owns what the surface shows; DOC25 owns how the data gets there.
1. **Floating Palette V2 (§6.20.30).** Redesigned from 3-tab overlay to sidebar-based control surface. Three persistent content tabs (Chat, Note, To Do) with state preservation. ☰ sidebar (170px) slides out left with Command bar, Notices (Linear-style notification inbox), Activity feed, Quick Actions (Watch, Run procedure, New note, Search knowledge, Key Commands). Pin (always-on-top) toggle. Silent mode. Key capture for hotkey customization. Designed as standalone micro-workspace for use while in other apps.
2. **Unified To-Do System (§6.21).** First-class data type with single shared `fpTodoLists` data pool across three view surfaces: palette To Do tab, note canvas to-do modules (embedded), standalone to-do tabs. Changes propagate instantly. Per-task date/reminder popover. List-level "Ask" button (per-task sparks removed). Two-layer data architecture: EC application tables (Layer 1) + DOC72 entity graph extraction (Layer 2).
3. **Calendar Module (§6.22).** Embeddable in notes or standalone. Month/Week/Day/List views. Event editor with reminders. Settings with agent instructions, Outlook sources (DOC16), sync rules, notification preferences. Same two-layer data architecture as To-Do.
4. **Chrome-style Tab Bar (§6.20.13–14).** Active tabs white, seamlessly connected to content below. Proportional shrinking (`flex: "1 1 0"`). Tab groups use `display: contents` for uniform shrinking. Group labels as colored pills. Active grouped tabs get continuous colored border outline. 10-color picker. Auto-persist on creation.
5. **SVG Icon System (§6.23).** All emoji icons replaced. Heroicons (outline) primary, Lucide for selective swaps, Tabler for 2 specialized icons. Component architecture: Ic → I → TabIcon → NavIcon. 60+ icons. Tab icon format migrated from emoji strings to string IDs affecting serialization.
6. **Split View (§6.24).** Two independent panes with draggable divider. Each pane: own tab bar, active tab, content area. Left pane keeps browser column toggle. Close merges tabs.
7. **Notification System (§6.25).** Linear-style inbox via palette sidebar. Source-colored cards (Calendar amber, Agent purple, Email blue, System gray). Snooze options. Silent mode. Configurable delivery matrix (Badge/Glow/Sound/Popup/Auto-show per source).
8. **Cross-cutting updates.** Document title click + right-click menu. Zoom slider in status bar. Per-tab right panel state. Browser column scope toggles (Notes/To Do/Cal). Collection filter dropdown for Places. Saved views overhaul. Naming: "Conversations" → "Chats", "Knowledge Manager" → "Knowledge". Tab colors updated (web → slate, chat → dark blue, todo → teal). Note canvas editable with module islands. Bookmarks bar for web tabs.
---
*The following R2.3/R2 summaries are retained for historical context:*
DOC20 R2.3 added the full Q Browser feature spec (§6.19): tabs and tab groups, privacy controls, credential vault, downloads, reader mode, clippings, PDF interception, DOC72 signal emission, and all Chrome-equivalent behaviors.
DOC20 R2 was the contract-closure revision addressing all 5-reviewer red-team findings. Full enum canonicalization, 20+ missing commands, 5 E2E gaps closed, contract hardening across surfaces.
### 13.1 Implementation completeness note
After R4.1, DOC20 is materially complete for the workspace shell architecture, all content surfaces, the embedded browser, and the surface-side sync contract. The surface independence principle (§2.4) makes mobile, collaboration, and multi-device sync achievable without data model changes. Remaining deferred items: Focus mode, split compare view, case timeline widget, drag-to-chat from browser, live sections in notes (tracked in DOC16). CRDT/Yjs for collaborative note editing (tracked as Phase 2 in §2.6.4).
**Next priorities:**
1. **DOC25** — new spec for server-side sync transport, conflict resolution, networking, auth/permissions. This is the biggest new obligation from R4.1.
2. **DOC11** — agent contention protocol (§2.6.7 obligation).
3. **DOC21 and DOC22** — companion docs must be updated to reflect R4 surfaces (§6.20.29 lists all obligations).
4. Individual utility page content (Tasks, Projects, Knowledge, Forums & Panels, Agents, Skills & Connectors, Overlays & Prompts, Context Buckets, Settings) should be specified as those specs mature.
### 13.2 Mockup inventory
| Mockup | File | Version | Key features |
|---|---|---|---|
| **Q Unified Workspace** | Q_UNIFIED_WORKSPACE_V7_3.jsx | **V7.3 (R3 operative)** | Full workspace shell: 4-mode browser, 8 tab types, Nav tab, transient utility tabs, right chat column, simplified left rail, block modules, tracked changes, comment CRUD, Places/Folders scopes, bookmark CRUD, Ask Agent with inline response, Chats page, session system, IBM Plex Sans. 654 lines, 174KB. |
| Workspace (V5.1 base) | Q_UNIFIED_WORKSPACE_V5_1.jsx | V5.1 | Full-featured base with all block modules, tracked changes, bookmark CRUD, note folder management. 570 lines, 157KB. |
| Browser | Q_BROWSER_R13.jsx | V4 | Source mockup — browser column with scopes, type chips, folder overlay, Places browsing. |
| Document Viewer | Q_ARTIFACT_VIEWER.jsx | V6 | Source mockup — viewer toolbar, comments, Send to Agent, versions, inline diff. |
| Notes | Q_NOTES_FULL.jsx | V5 | Source mockup — notes editor, comments, tracked changes, Send to Agent drawer. |
| Web Browser | Q_WEB_BROWSER_VIEW_V2.jsx | V2 | Source mockup — web browser tabs, incognito, reader mode, Ask panel with Include checkboxes. |
| Block Modules | Q_WORKSPACE_V5_2.jsx | V5.2 | Source mockup — task lists, threads, feeds, 3-step module picker, custom feed config. |
| Project | Q_PROJECT_V2.jsx | V2 | Project page (unchanged from R1.3). |