{
  "version": 1,
  "project": {
    "name": "blink",
    "title": "Canvas",
    "description": "Spatial notes, placement, and named desks."
  },
  "generatedAt": "2026-10-28T16:13:29.025Z",
  "artifacts": {
    "llms": "/canvas/docs/llms.txt",
    "agents": "/canvas/docs/AGENTS.md",
    "fullManifest": "/canvas/docs/agent-docs.json",
    "allMarkdown": "/canvas/docs/llms-full.txt",
    "markdownBase": "/canvas/docs/markdown/"
  },
  "recommendedReadOrder": [
    "blink:overview",
    "blink:concepts",
    "blink:cli",
    "blink:placement",
    "blink:workspaces",
    "blink:config",
    "blink:ios-sync",
    "blink:agents",
    "blink:api"
  ],
  "docs": [
    {
      "id": "blink:overview",
      "slug": "overview",
      "product": "blink",
      "kind": "doc",
      "catalogKind": "guide",
      "current": true,
      "title": "Overview",
      "description": "What Canvas is, how to install it, and how it relates to Lattices.",
      "order": null,
      "sourcePath": "products/blink/docs/overview.md",
      "url": "/canvas/docs/overview",
      "rawUrl": "/canvas/docs/markdown/overview.md",
      "markdownUrl": "/canvas/docs/overview.md",
      "headings": [
        {
          "depth": 1,
          "text": "Canvas overview",
          "anchor": "canvas-overview"
        },
        {
          "depth": 2,
          "text": "Install and launch",
          "anchor": "install-and-launch"
        },
        {
          "depth": 2,
          "text": "First note",
          "anchor": "first-note"
        },
        {
          "depth": 2,
          "text": "Agent surfaces",
          "anchor": "agent-surfaces"
        },
        {
          "depth": 2,
          "text": "Relationship to Lattices",
          "anchor": "relationship-to-lattices"
        },
        {
          "depth": 2,
          "text": "Next steps",
          "anchor": "next-steps"
        }
      ],
      "tokensEstimate": 856,
      "frontmatter": {
        "title": "Overview",
        "description": "What Canvas is, how to install it, and how it relates to Lattices."
      },
      "markdown": "---\ntitle: Overview\ndescription: What Canvas is, how to install it, and how it relates to Lattices.\n---\n\n# Canvas overview\n\nCanvas is a native macOS app for floating Markdown notes. Each note is one\nfloating panel and one Markdown file. The panel remembers its position and size.\nThe app and the `canvas` CLI read and write the same local files, so a person and\nan agent work on the same notes.\n\nCanvas 2.3.0 ships `Canvas.app` and the `canvas` CLI through `@arach/blink`.\n`Blink.app` and `blink` remain compatibility names. Existing note paths,\nconfiguration keys, bundle identity and `blink.sock` remain unchanged.\n\n**The note is the window, and the desktop is the workspace.**\nCanvas has no library window and no Dock icon. It runs from the menu\nbar with floating note panels, a menu bar popover, and a command palette.\n\n## Install and launch\n\nRequirements:\n\n- macOS 14 or later on Apple Silicon.\n- Node.js 18 or later for the npm command launchers.\n\n1. Install the package. It includes the signed native `canvas` CLI.\n\n   ```sh\n   npm install -g @arach/blink    # or: bun add --global @arach/blink\n   ```\n\n2. Install the app. `canvas app install` downloads the signed, notarized\n   release, validates it, and installs `Canvas.app` in `/Applications`.\n\n   ```sh\n   canvas app install\n   ```\n\n3. Launch the app.\n\n   ```sh\n   canvas app open\n   ```\n\n   Result: the app icon appears in the menu bar. If Lattices is running, Canvas\n   hides its own icon by default (see `behavior.alwaysShowMenuBarIcon` in\n   [Configuration](/canvas/docs/config)).\n\nTo update the app, run `canvas app update`. You can also download the DMG from\nthe [download page](https://lattices.dev/canvas/download).\n\n## First note\n\nPress Hyper+N to create a note. Hyper is Control+Option+Shift+Command. Hyper+B\nshows or hides all note panels. You can change both hotkeys in `config.json`.\n\nFrom a terminal or an agent:\n\n```sh\ncanvas present standup \"# Standup\" --slot 6 --writer agent\ncanvas type standup \"- shipped the CLI\" --writer agent\ncanvas cat standup\n```\n\n`present` creates the note and sets its grid slot. `type` appends text that the\nopen panel reveals character by character. See [CLI](/canvas/docs/cli) for the\nfull command set.\n\n## Agent surfaces\n\nUse these agent surfaces in this order:\n\n| Surface | Use it for | Reference |\n|---|---|---|\n| `blink` CLI | Create, read, edit, search, and delete notes; group and brand workspaces; open and arrange live panels. | [CLI](/canvas/docs/cli) |\n| Files | Read or write `<id>.md` notes and `config.json` directly when the CLI does not fit. | [Concepts](/canvas/docs/concepts), [Configuration](/canvas/docs/config) |\n| Agent socket | Query open panels, present notes, save and restore desks, and receive change events from the running app. | [Agent socket API](/canvas/docs/api) |\n\nInstall the agent skill to give a coding agent these instructions:\n\n```sh\nnpx skills add arach/lattices --skill blink --global --yes\n```\n\nThere is no Canvas MCP server today. The broader agent verb set and MCP layer in\n[Agent API proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-api.md) and\n[Agent integration proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-integration.md) are proposed, not\nshipped.\n\n## Relationship to Lattices\n\nCanvas is a separate product in the Lattices family. It shares the Lattices\nrepository (`products/blink/`) and website, but it is not part of the Lattices\nmenu bar app.\n\n- `Blink.app` (bundle ID `dev.arach.blink`) has its own process, lifecycle,\n  release tags (`blink-v*`), and npm package (`@arach/blink`).\n- Quitting Lattices does not quit Canvas. Quitting Canvas does not quit Lattices.\n- The Lattices menu bar app can install and launch it from the current **Blink**\n  entry in its Apps list.\n  Canvas's note panels and NoteStore stay inside Canvas.\n- There is no `lats canvas` command or family `canvas.*` API today. The earlier\n  family-domain proposal is separate from this product rename. Operate Canvas\n  with the `blink` CLI.\n\nFor the Lattices side of the family, see the\n[Lattices overview](/lattices/docs/overview) and the\n[product map](/products).\n\n## Next steps\n\n- [Concepts](/canvas/docs/concepts): the note, panel, and file model.\n- [CLI](/canvas/docs/cli): every `blink` command.\n- [Configuration](/canvas/docs/config): behavior, hotkeys, and appearance.\n- [Workspaces](/canvas/docs/workspaces): named, branded groups of notes.\n- [Placement](/canvas/docs/placement): the grid and slot model.\n- [iOS sync](/canvas/docs/ios-sync): the read-only iPhone and iPad companion.\n- [Release](/canvas/docs/release): how Canvas ships.\n",
      "content": "# Canvas overview\n\nCanvas is a native macOS app for floating Markdown notes. Each note is one\nfloating panel and one Markdown file. The panel remembers its position and size.\nThe app and the `canvas` CLI read and write the same local files, so a person and\nan agent work on the same notes.\n\nCanvas 2.3.0 ships `Canvas.app` and the `canvas` CLI through `@arach/blink`.\n`Blink.app` and `blink` remain compatibility names. Existing note paths,\nconfiguration keys, bundle identity and `blink.sock` remain unchanged.\n\n**The note is the window, and the desktop is the workspace.**\nCanvas has no library window and no Dock icon. It runs from the menu\nbar with floating note panels, a menu bar popover, and a command palette.\n\n## Install and launch\n\nRequirements:\n\n- macOS 14 or later on Apple Silicon.\n- Node.js 18 or later for the npm command launchers.\n\n1. Install the package. It includes the signed native `canvas` CLI.\n\n   ```sh\n   npm install -g @arach/blink    # or: bun add --global @arach/blink\n   ```\n\n2. Install the app. `canvas app install` downloads the signed, notarized\n   release, validates it, and installs `Canvas.app` in `/Applications`.\n\n   ```sh\n   canvas app install\n   ```\n\n3. Launch the app.\n\n   ```sh\n   canvas app open\n   ```\n\n   Result: the app icon appears in the menu bar. If Lattices is running, Canvas\n   hides its own icon by default (see `behavior.alwaysShowMenuBarIcon` in\n   [Configuration](/canvas/docs/config)).\n\nTo update the app, run `canvas app update`. You can also download the DMG from\nthe [download page](https://lattices.dev/canvas/download).\n\n## First note\n\nPress Hyper+N to create a note. Hyper is Control+Option+Shift+Command. Hyper+B\nshows or hides all note panels. You can change both hotkeys in `config.json`.\n\nFrom a terminal or an agent:\n\n```sh\ncanvas present standup \"# Standup\" --slot 6 --writer agent\ncanvas type standup \"- shipped the CLI\" --writer agent\ncanvas cat standup\n```\n\n`present` creates the note and sets its grid slot. `type` appends text that the\nopen panel reveals character by character. See [CLI](/canvas/docs/cli) for the\nfull command set.\n\n## Agent surfaces\n\nUse these agent surfaces in this order:\n\n| Surface | Use it for | Reference |\n|---|---|---|\n| `blink` CLI | Create, read, edit, search, and delete notes; group and brand workspaces; open and arrange live panels. | [CLI](/canvas/docs/cli) |\n| Files | Read or write `<id>.md` notes and `config.json` directly when the CLI does not fit. | [Concepts](/canvas/docs/concepts), [Configuration](/canvas/docs/config) |\n| Agent socket | Query open panels, present notes, save and restore desks, and receive change events from the running app. | [Agent socket API](/canvas/docs/api) |\n\nInstall the agent skill to give a coding agent these instructions:\n\n```sh\nnpx skills add arach/lattices --skill blink --global --yes\n```\n\nThere is no Canvas MCP server today. The broader agent verb set and MCP layer in\n[Agent API proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-api.md) and\n[Agent integration proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-integration.md) are proposed, not\nshipped.\n\n## Relationship to Lattices\n\nCanvas is a separate product in the Lattices family. It shares the Lattices\nrepository (`products/blink/`) and website, but it is not part of the Lattices\nmenu bar app.\n\n- `Blink.app` (bundle ID `dev.arach.blink`) has its own process, lifecycle,\n  release tags (`blink-v*`), and npm package (`@arach/blink`).\n- Quitting Lattices does not quit Canvas. Quitting Canvas does not quit Lattices.\n- The Lattices menu bar app can install and launch it from the current **Blink**\n  entry in its Apps list.\n  Canvas's note panels and NoteStore stay inside Canvas.\n- There is no `lats canvas` command or family `canvas.*` API today. The earlier\n  family-domain proposal is separate from this product rename. Operate Canvas\n  with the `blink` CLI.\n\nFor the Lattices side of the family, see the\n[Lattices overview](/lattices/docs/overview) and the\n[product map](/products).\n\n## Next steps\n\n- [Concepts](/canvas/docs/concepts): the note, panel, and file model.\n- [CLI](/canvas/docs/cli): every `blink` command.\n- [Configuration](/canvas/docs/config): behavior, hotkeys, and appearance.\n- [Workspaces](/canvas/docs/workspaces): named, branded groups of notes.\n- [Placement](/canvas/docs/placement): the grid and slot model.\n- [iOS sync](/canvas/docs/ios-sync): the read-only iPhone and iPad companion.\n- [Release](/canvas/docs/release): how Canvas ships."
    },
    {
      "id": "blink:concepts",
      "slug": "concepts",
      "product": "blink",
      "kind": "doc",
      "catalogKind": "guide",
      "current": true,
      "title": "Concepts",
      "description": "Canvas's model of notes, panels, files, configuration, workspaces, and device sync.",
      "order": null,
      "sourcePath": "products/blink/docs/concepts.md",
      "url": "/canvas/docs/concepts",
      "rawUrl": "/canvas/docs/markdown/concepts.md",
      "markdownUrl": "/canvas/docs/concepts.md",
      "headings": [
        {
          "depth": 1,
          "text": "Canvas concepts",
          "anchor": "canvas-concepts"
        },
        {
          "depth": 2,
          "text": "The note is the window",
          "anchor": "the-note-is-the-window"
        },
        {
          "depth": 2,
          "text": "Capture, place, recall",
          "anchor": "capture-place-recall"
        },
        {
          "depth": 2,
          "text": "Files are the truth",
          "anchor": "files-are-the-truth"
        },
        {
          "depth": 2,
          "text": "How changes converge",
          "anchor": "how-changes-converge"
        },
        {
          "depth": 2,
          "text": "Panels and placement",
          "anchor": "panels-and-placement"
        },
        {
          "depth": 2,
          "text": "Configuration",
          "anchor": "configuration"
        },
        {
          "depth": 2,
          "text": "Workspaces",
          "anchor": "workspaces"
        },
        {
          "depth": 2,
          "text": "Device sync",
          "anchor": "device-sync"
        },
        {
          "depth": 2,
          "text": "Boundary with Lattices",
          "anchor": "boundary-with-lattices"
        }
      ],
      "tokensEstimate": 1305,
      "frontmatter": {
        "title": "Concepts",
        "description": "Canvas's model of notes, panels, files, configuration, workspaces, and device sync."
      },
      "markdown": "---\ntitle: Concepts\ndescription: Canvas's model of notes, panels, files, configuration, workspaces, and device sync.\n---\n\n# Canvas concepts\n\nThis page explains the model that the app, the CLI, and the agent socket share.\nRead it before you script Canvas or change its code.\n\n## The note is the window\n\nA note is one Markdown file and, when it is open, one floating `NSPanel`.\nCanvas keeps one panel for each note. If you open a note that is already open,\nCanvas focuses the existing panel. It does not create a duplicate.\n\nThe menu bar popover and the command palette are launchers. They find and open\nnotes, but they are not alternate document windows. You read and edit a note in\nits own panel.\n\n## Capture, place, recall\n\nThe loop for a person is capture, place, recall:\n\n1. **Capture.** Create or find a note from the menu bar popover, the command\n   palette, or the global new-note hotkey (Hyper+N by default).\n2. **Place.** Move, resize, shade, or focus panels to arrange a working set.\n3. **Recall.** Canvas restores each note to the same place and size. Hyper+B\n   shows or hides all panels.\n\nAn agent follows the same loop with `blink present`, `blink desk`, and the\n[agent socket](/canvas/docs/api).\n\n## Files are the truth\n\nThe Markdown files are the durable truth for notes.\n\n| Item | Location |\n|---|---|\n| Notes | `~/Library/Application Support/Blink/Notes/<id>.md` |\n| Configuration | `~/Library/Application Support/Blink/config.json` |\n| Attachments and brand marks | `~/Library/Application Support/Blink/attachments/` |\n| Saved desks | `~/Library/Application Support/Blink/desks/<name>.json` |\n| Edit ledger | `~/Library/Application Support/Blink/edits.sqlite` |\n| Agent socket | `~/Library/Application Support/Blink/blink.sock` |\n\nIf `BLINK_HOME` is set, Canvas uses that directory as the root for every item in\nthis table. Use `BLINK_HOME` to sandbox notes for tests or agents.\n`BLINK_HOME` does not redirect `UserDefaults`. Canvas disables device sync while\n`BLINK_HOME` is set.\n\nEach note file holds the body and its metadata together. Metadata is YAML\nfrontmatter:\n\n```markdown\n---\nid: q3-planning\ncreated: 2026-07-25T22:39:44.042Z\nupdated: 2026-07-25T22:39:44.042Z\ntags: []\npinned: false\nblink:\n  workspace: acme-docs\n  slot: 6\n  lastWriter: agent\n---\n# Q3 Planning\n```\n\n- The `id` is a unique slug derived from the title. It is also the file name.\n- The `blink:` block holds presentation intent: `workspace`, `style`, `sheet`,\n  `accent`, `font`, `fontSize`, `lineHeight`, `tint`, `tintRead`, `tintEdit`,\n  `radius`, `slot`, and `lastWriter`.\n- Canvas preserves frontmatter keys that it does not own. You can add your own\n  keys, and they survive every save.\n- All writes are atomic: Canvas writes a temporary file, syncs it, and renames it.\n\nThe edit ledger (`edits.sqlite`) records who changed each note. It is not a\nsecond note store. If the ledger is missing or a ledger write fails, the note\nsave still succeeds. Read it with `blink log <id>`.\n\n## How changes converge\n\nEvery change reaches the same store through one path:\n\n```text\npanel edits / popover actions  -> PanelManager / AppModel -> NoteStore -> atomic .md\nCLI / external writes          -> same .md -> directory watcher -> NoteStore.reconcile()\nNoteStore notifications        -> AppModel snapshot -> popover / palette / open panels\n```\n\n- `NoteStore` owns note mutations and the in-memory index.\n- `AppModel` mirrors `NoteStore` for the user interface. It is not a second store.\n- `PanelManager` owns live panels: one panel per note, focus, pending editor\n  text, geometry, and save flushing.\n- External writers are first-class. When a script or the CLI writes a file, the\n  directory watcher calls `NoteStore.reconcile()`. The open panel updates the\n  same way it does for an in-app edit. If the person has unsaved edits in that\n  panel, the person's edits win.\n\nThe CLI works when the app is closed because it writes files. The `blink desk`\ncommands and the agent socket need the running app.\n\n## Panels and placement\n\nA panel has a mode (read or edit), a frame, and a display. It can also be\nshaded, which folds it into its top band.\n\nTwo kinds of placement exist:\n\n- **Placement intent** is portable. The `blink.slot` key (1 to 9) names a cell\n  in a 3×3 grid on the panel's display. `blink present --slot N` sets it.\n- **The exact frame** is per device. Canvas stores each panel's frame, the set of\n  open panels, and each note's mode in `UserDefaults`.\n\nA **desk** is a named snapshot of the open panels with their frames, slots,\nmodes, and displays. `blink desk save` and `blink desk restore` write and apply\ndesks. See [Placement](/canvas/docs/placement) for the grid model and which parts\nare proposed.\n\n## Configuration\n\n`config.json` owns behavior and appearance: hotkeys, panel material, editor\ntypography, motion, physics, the drape backdrop, named styles, and workspace\ndefinitions. The running app watches the file and applies changes without a\nrestart. A missing key uses its default. If the file does not parse, Canvas logs\nthe error and keeps the last valid configuration. Agents should edit this\nfile instead of driving the Settings window. See\n[Configuration](/canvas/docs/config).\n\n## Workspaces\n\nA workspace is a named group of notes with an optional brand. It has two parts:\n\n| Part | Lives in | Holds |\n|---|---|---|\n| Definition | `config.json` → `workspaces.<name>` | Title and brand (mark, palette, typography, corners). |\n| Membership | Each note's `blink.workspace` key | The workspace name only. |\n\nA note never carries colors, fonts, or asset paths for its workspace, so the\nfile stays portable Markdown. See [Workspaces](/canvas/docs/workspaces).\n\n## Device sync\n\nThe iPhone and iPad companion is a read-only, offline view of the Mac's notes.\nThe Mac serves a complete snapshot over an encrypted local-network channel\n(`BlinkPeer`, Multipeer Connectivity with an end-to-end sealed payload). A new\ndevice must be approved on the Mac. You can revoke it from **Mobile Access** in\nCanvas's right-click menu. The local network is the only route today. See\n[iOS sync](/canvas/docs/ios-sync).\n\n## Boundary with Lattices\n\nCanvas is a separate app with its own lifecycle. The Lattices menu bar app does\nnot own Canvas's panels, NoteStore, or files. Lattices can install and launch\nCanvas, and the two apps share the repository and website. See the\n[Canvas overview](/canvas/docs/overview#relationship-to-lattices).\n",
      "content": "# Canvas concepts\n\nThis page explains the model that the app, the CLI, and the agent socket share.\nRead it before you script Canvas or change its code.\n\n## The note is the window\n\nA note is one Markdown file and, when it is open, one floating `NSPanel`.\nCanvas keeps one panel for each note. If you open a note that is already open,\nCanvas focuses the existing panel. It does not create a duplicate.\n\nThe menu bar popover and the command palette are launchers. They find and open\nnotes, but they are not alternate document windows. You read and edit a note in\nits own panel.\n\n## Capture, place, recall\n\nThe loop for a person is capture, place, recall:\n\n1. **Capture.** Create or find a note from the menu bar popover, the command\n   palette, or the global new-note hotkey (Hyper+N by default).\n2. **Place.** Move, resize, shade, or focus panels to arrange a working set.\n3. **Recall.** Canvas restores each note to the same place and size. Hyper+B\n   shows or hides all panels.\n\nAn agent follows the same loop with `blink present`, `blink desk`, and the\n[agent socket](/canvas/docs/api).\n\n## Files are the truth\n\nThe Markdown files are the durable truth for notes.\n\n| Item | Location |\n|---|---|\n| Notes | `~/Library/Application Support/Blink/Notes/<id>.md` |\n| Configuration | `~/Library/Application Support/Blink/config.json` |\n| Attachments and brand marks | `~/Library/Application Support/Blink/attachments/` |\n| Saved desks | `~/Library/Application Support/Blink/desks/<name>.json` |\n| Edit ledger | `~/Library/Application Support/Blink/edits.sqlite` |\n| Agent socket | `~/Library/Application Support/Blink/blink.sock` |\n\nIf `BLINK_HOME` is set, Canvas uses that directory as the root for every item in\nthis table. Use `BLINK_HOME` to sandbox notes for tests or agents.\n`BLINK_HOME` does not redirect `UserDefaults`. Canvas disables device sync while\n`BLINK_HOME` is set.\n\nEach note file holds the body and its metadata together. Metadata is YAML\nfrontmatter:\n\n```markdown\n---\nid: q3-planning\ncreated: 2026-07-25T22:39:44.042Z\nupdated: 2026-07-25T22:39:44.042Z\ntags: []\npinned: false\nblink:\n  workspace: acme-docs\n  slot: 6\n  lastWriter: agent\n---\n# Q3 Planning\n```\n\n- The `id` is a unique slug derived from the title. It is also the file name.\n- The `blink:` block holds presentation intent: `workspace`, `style`, `sheet`,\n  `accent`, `font`, `fontSize`, `lineHeight`, `tint`, `tintRead`, `tintEdit`,\n  `radius`, `slot`, and `lastWriter`.\n- Canvas preserves frontmatter keys that it does not own. You can add your own\n  keys, and they survive every save.\n- All writes are atomic: Canvas writes a temporary file, syncs it, and renames it.\n\nThe edit ledger (`edits.sqlite`) records who changed each note. It is not a\nsecond note store. If the ledger is missing or a ledger write fails, the note\nsave still succeeds. Read it with `blink log <id>`.\n\n## How changes converge\n\nEvery change reaches the same store through one path:\n\n```text\npanel edits / popover actions  -> PanelManager / AppModel -> NoteStore -> atomic .md\nCLI / external writes          -> same .md -> directory watcher -> NoteStore.reconcile()\nNoteStore notifications        -> AppModel snapshot -> popover / palette / open panels\n```\n\n- `NoteStore` owns note mutations and the in-memory index.\n- `AppModel` mirrors `NoteStore` for the user interface. It is not a second store.\n- `PanelManager` owns live panels: one panel per note, focus, pending editor\n  text, geometry, and save flushing.\n- External writers are first-class. When a script or the CLI writes a file, the\n  directory watcher calls `NoteStore.reconcile()`. The open panel updates the\n  same way it does for an in-app edit. If the person has unsaved edits in that\n  panel, the person's edits win.\n\nThe CLI works when the app is closed because it writes files. The `blink desk`\ncommands and the agent socket need the running app.\n\n## Panels and placement\n\nA panel has a mode (read or edit), a frame, and a display. It can also be\nshaded, which folds it into its top band.\n\nTwo kinds of placement exist:\n\n- **Placement intent** is portable. The `blink.slot` key (1 to 9) names a cell\n  in a 3×3 grid on the panel's display. `blink present --slot N` sets it.\n- **The exact frame** is per device. Canvas stores each panel's frame, the set of\n  open panels, and each note's mode in `UserDefaults`.\n\nA **desk** is a named snapshot of the open panels with their frames, slots,\nmodes, and displays. `blink desk save` and `blink desk restore` write and apply\ndesks. See [Placement](/canvas/docs/placement) for the grid model and which parts\nare proposed.\n\n## Configuration\n\n`config.json` owns behavior and appearance: hotkeys, panel material, editor\ntypography, motion, physics, the drape backdrop, named styles, and workspace\ndefinitions. The running app watches the file and applies changes without a\nrestart. A missing key uses its default. If the file does not parse, Canvas logs\nthe error and keeps the last valid configuration. Agents should edit this\nfile instead of driving the Settings window. See\n[Configuration](/canvas/docs/config).\n\n## Workspaces\n\nA workspace is a named group of notes with an optional brand. It has two parts:\n\n| Part | Lives in | Holds |\n|---|---|---|\n| Definition | `config.json` → `workspaces.<name>` | Title and brand (mark, palette, typography, corners). |\n| Membership | Each note's `blink.workspace` key | The workspace name only. |\n\nA note never carries colors, fonts, or asset paths for its workspace, so the\nfile stays portable Markdown. See [Workspaces](/canvas/docs/workspaces).\n\n## Device sync\n\nThe iPhone and iPad companion is a read-only, offline view of the Mac's notes.\nThe Mac serves a complete snapshot over an encrypted local-network channel\n(`BlinkPeer`, Multipeer Connectivity with an end-to-end sealed payload). A new\ndevice must be approved on the Mac. You can revoke it from **Mobile Access** in\nCanvas's right-click menu. The local network is the only route today. See\n[iOS sync](/canvas/docs/ios-sync).\n\n## Boundary with Lattices\n\nCanvas is a separate app with its own lifecycle. The Lattices menu bar app does\nnot own Canvas's panels, NoteStore, or files. Lattices can install and launch\nCanvas, and the two apps share the repository and website. See the\n[Canvas overview](/canvas/docs/overview#relationship-to-lattices)."
    },
    {
      "id": "blink:cli",
      "slug": "cli",
      "product": "blink",
      "kind": "doc",
      "catalogKind": "guide",
      "current": true,
      "title": "Canvas CLI",
      "description": "The 2.3.0 release contract uses canvas; blink remains a compatible\nexecutable alias, and the npm package remains @arach/blink. The examples below\nuse canvas; substitute blink on an older installation. Update the app and\n",
      "order": null,
      "sourcePath": "products/blink/docs/cli.md",
      "url": "/canvas/docs/cli",
      "rawUrl": "/canvas/docs/markdown/cli.md",
      "markdownUrl": "/canvas/docs/cli.md",
      "headings": [
        {
          "depth": 1,
          "text": "Canvas CLI",
          "anchor": "canvas-cli"
        },
        {
          "depth": 2,
          "text": "Where it operates",
          "anchor": "where-it-operates"
        },
        {
          "depth": 2,
          "text": "Build / install",
          "anchor": "build-install"
        },
        {
          "depth": 2,
          "text": "Commands",
          "anchor": "commands"
        },
        {
          "depth": 2,
          "text": "JSON shape",
          "anchor": "json-shape"
        },
        {
          "depth": 2,
          "text": "Or skip the CLI entirely",
          "anchor": "or-skip-the-cli-entirely"
        }
      ],
      "tokensEstimate": 1602,
      "frontmatter": {},
      "markdown": "# Canvas CLI\n\nThe 2.3.0 release contract uses `canvas`; `blink` remains a compatible\nexecutable alias, and the npm package remains `@arach/blink`. The examples below\nuse `canvas`; substitute `blink` on an older installation. Update the app and\nCLI together for 2.3.0's stricter private IPC permissions. See the\n[release contract](release.md) for canonical and legacy downloads.\n\nThe CLI reads and writes the same files as the app. Writes are atomic and\nslug-safe via BlinkCore. The running app picks every change up\n**live**: it watches the Notes directory, reconciles disk against memory, and\nroutes the diff through\nthe same notifications in-app edits use. Create a note from a script and it's\nin the popover a second later; edit an open note's file and the panel updates\nin place (unless the user has unsaved edits in flight — then the user wins).\n\n## Where it operates\n\n```\n$CANVAS_HOME/Notes                                 # first nonempty override\n$BLINK_HOME/Notes                                  # compatible fallback\n~/Library/Application Support/Blink/Notes           # default, unchanged\n```\n\nCurrent source builds of the app and both CLI names use the first nonempty\n`CANVAS_HOME`, then `BLINK_HOME`; empty values are ignored. Use the same value\nfor both processes to isolate file-backed notes, config, attachments, ledger,\nand socket. The override does not redirect `UserDefaults`. The Mac app disables\nmobile sharing with either override. Older releases recognize `BLINK_HOME` only.\n\nThe rename preserves the default data directory, `blink.sock`, `blink.*`\nfrontmatter, and `blink://attachments/` URLs. Do not rename these on disk.\n\n## Build / install\n\n```sh\nswift build -c release --product canvas\ncp .build/release/canvas ~/bin/   # or anywhere on PATH\n```\n\n`swift build --product blink` remains supported. The current source family\nentry points `lats canvas` and `lats blink` forward to the product. App installer\nverbs belong to the npm wrapper or family entry point, not the Swift notes CLI.\n\n## Commands\n\nCommands that expose structured output take `--json` where shown below. Errors\ngo to stderr with a nonzero exit code.\n\n```sh\ncanvas ls [--limit N] [--json]     # list notes, most recently updated first\ncanvas cat <id> [--json]           # print content (exact bytes); --json adds all metadata\ncanvas new [text ...] [--writer W] [--json]\ncanvas present <id> [text ...] [--style … --slot … --writer W] [--json]\ncanvas append <id> [text ...] [--writer W] [--json]\ncanvas type <id> [text ...] [--writer W] [--json]\ncanvas write <id> [text ...] [--writer W] [--json]\ncanvas search <query> [--json]\ncanvas rm <id> [--writer W] [--json]\ncanvas log <id> [--limit N] [--json]   # append-only edit ledger\ncanvas path [<id>]\ncanvas workspace <subcommand>\ncanvas desk <subcommand>\n```\n\n`workspace` is the grouping/branding verb set — see `docs/workspaces.md` for the\nfull guide:\n\n```sh\ncanvas workspace init <name> [--title T] [brand flags…]   # create (idempotent)\ncanvas workspace ls                                       # list + note counts\ncanvas workspace show <name>                              # definition + effective brand\ncanvas workspace brand <name> [brand flags…]              # mark, palette, type, corners\ncanvas workspace add <name> <id>…                         # put notes in it\ncanvas workspace remove <id>…                             # take notes out\ncanvas workspace notes <name>                             # list member notes\ncanvas workspace rm <name> [--detach]                     # forget the definition\n```\n\n`new` and `present` also take `--workspace <name>` so a note can be filed on\ncreation. Membership is one `blink.workspace:` key in the note's frontmatter;\nthe brand itself lives in `config.json`, so note markdown stays portable and\nfree of presentation-only branding. In the running app, the capture popover's\nworkspace selector filters the log/canvas and recalls only that workspace's\nopen panels; notes created while a named workspace is active inherit it.\n\n`present` is the compound arrival verb: it sets a note's markdown **and** its\n`blink:` presentation metadata in one write, get-or-create\nby id. Only the fields you pass change; the rest are preserved. Its options mirror\nthe `blink:` keys: `--style --sheet --accent --font --font-size --line-height\n--tint --tint-read --tint-edit --radius --slot`. `--slot 1–9` is placement intent\nfor the grid. Omit the text to change presentation alone.\n\n`type` and `write` are the two ways to change a note's body, and they differ only\nin *how the open panel reacts*: `type` appends an anchored suffix, so the running\napp reveals it character by character (the visible hand); `write` replaces the\nwhole body, so the panel updates in place with no animation. `append` is the\nestablished sibling of `type` (identical behavior). All three preserve presentation\nand foreign frontmatter.\n\n`--writer` stamps `blink.lastWriter` on the note and appends a row to\n`<home>/edits.sqlite` under the selected data directory.\nThe markdown file stays truth: a ledger failure never fails the save.\nOmit `--writer` and the CLI records `cli`. In-app edits record `user`.\n`canvas log` reads that sidecar even after `rm`; it is not a second note store.\n\n\n`desk` is the intentionally small live counterpart to the file API. It asks the\nrunning Canvas app to realize an existing note as a panel while `PanelManager`\ncontinues to own one-panel-per-note identity, focus, save flushing, and geometry\npersistence. Coordinates are display-local AppKit points measured from the\nvisible top-left corner; omitted frame values preserve the current value.\n`--display N` selects a one-based display (`1` is the main display); otherwise\nthe panel stays on its current display.\n\n```sh\ncanvas desk open standup --display 1 --x 980 --y 90 --width 460 --height 260\ncanvas desk move standup --x 1130 --y 70     # visible lock-and-settle\ncanvas desk focus standup\ncanvas desk close standup                    # closes the panel, keeps the note\ncanvas desk screens --json                   # responsive-script display geometry\n```\n\nExamples:\n\n```sh\ncanvas new \"grocery run\"                    # → grocery-run\nprintf '# Standup\\n\\n- ship CLI\\n' | canvas new --json\ncanvas present q3-planning \"# Q3 Planning\" --style focus --slot 6   # content + look + place\ncanvas present q3-planning --accent \"#9ece6a\"   # presentation-only; body untouched\ncanvas append grocery-run \"- oat milk\"      # types on live if the panel is open\ncanvas type standup \"shipped the CLI verbs\"     # same visible-hand reveal, phrasebook name\nprintf '%s\\n' '- ship docs' | canvas append standup --json\ncanvas write standup < revised-standup.md       # replace the body, no animation\ncanvas cat grocery-run\ncanvas search standup --json | jq '.[0].id'\nopen \"$(canvas path grocery-run)\"           # hand the file to anything\n\ncanvas workspace init \"Acme Docs\"                     # a named, brandable group\ncanvas new --workspace acme-docs \"# Q3 Planning\"      # file a note into it\ncanvas workspace brand acme-docs --accent \"#7aa2f7\" --install-mark ~/acme.svg\ncanvas workspace notes acme-docs --json | jq -r '.[].id'\n```\n\n## JSON shape\n\n```jsonc\n{\n  \"id\": \"standup\",\n  \"title\": \"Standup\",\n  \"tags\": [],\n  \"pinned\": false,\n  \"created\": \"2026-07-14T19:51:41.934Z\",\n  \"updated\": \"2026-07-14T19:51:41.934Z\",\n  \"path\": \"/Users/you/Library/Application Support/Blink/Notes/standup.md\",\n  \"content\": \"…\",             // cat/append --json only\n  \"extraFrontmatter\": []        // cat/append --json only — foreign keys, preserved verbatim\n}\n```\n\n## Or skip the CLI entirely\n\nThe filesystem is the API (layer 1): notes are frontmattered markdown, one\nfile per note. You may edit them with anything — Canvas preserves frontmatter\nkeys it doesn't own and merges on-disk metadata before every save, so foreign\nkeys survive. The CLI just adds atomic writes, correct slug assignment, and\nstructured output for free. Prefer it for *creating* notes (slug uniqueness),\n`append` when an agent should add a visible update without replacing the note,\nand any scripted workflow. `append` always writes exactly one separating\nnewline before the supplied text; stdin bytes after that separator are kept as\nprovided.\n\n`canvas link` and a product MCP server are not implemented. The agent API and\nMCP documents describe proposals; the existing local socket serves live panel\ncommands.\n",
      "content": "# Canvas CLI\n\nThe 2.3.0 release contract uses `canvas`; `blink` remains a compatible\nexecutable alias, and the npm package remains `@arach/blink`. The examples below\nuse `canvas`; substitute `blink` on an older installation. Update the app and\nCLI together for 2.3.0's stricter private IPC permissions. See the\n[release contract](release.md) for canonical and legacy downloads.\n\nThe CLI reads and writes the same files as the app. Writes are atomic and\nslug-safe via BlinkCore. The running app picks every change up\n**live**: it watches the Notes directory, reconciles disk against memory, and\nroutes the diff through\nthe same notifications in-app edits use. Create a note from a script and it's\nin the popover a second later; edit an open note's file and the panel updates\nin place (unless the user has unsaved edits in flight — then the user wins).\n\n## Where it operates\n\n```\n$CANVAS_HOME/Notes                                 # first nonempty override\n$BLINK_HOME/Notes                                  # compatible fallback\n~/Library/Application Support/Blink/Notes           # default, unchanged\n```\n\nCurrent source builds of the app and both CLI names use the first nonempty\n`CANVAS_HOME`, then `BLINK_HOME`; empty values are ignored. Use the same value\nfor both processes to isolate file-backed notes, config, attachments, ledger,\nand socket. The override does not redirect `UserDefaults`. The Mac app disables\nmobile sharing with either override. Older releases recognize `BLINK_HOME` only.\n\nThe rename preserves the default data directory, `blink.sock`, `blink.*`\nfrontmatter, and `blink://attachments/` URLs. Do not rename these on disk.\n\n## Build / install\n\n```sh\nswift build -c release --product canvas\ncp .build/release/canvas ~/bin/   # or anywhere on PATH\n```\n\n`swift build --product blink` remains supported. The current source family\nentry points `lats canvas` and `lats blink` forward to the product. App installer\nverbs belong to the npm wrapper or family entry point, not the Swift notes CLI.\n\n## Commands\n\nCommands that expose structured output take `--json` where shown below. Errors\ngo to stderr with a nonzero exit code.\n\n```sh\ncanvas ls [--limit N] [--json]     # list notes, most recently updated first\ncanvas cat <id> [--json]           # print content (exact bytes); --json adds all metadata\ncanvas new [text ...] [--writer W] [--json]\ncanvas present <id> [text ...] [--style … --slot … --writer W] [--json]\ncanvas append <id> [text ...] [--writer W] [--json]\ncanvas type <id> [text ...] [--writer W] [--json]\ncanvas write <id> [text ...] [--writer W] [--json]\ncanvas search <query> [--json]\ncanvas rm <id> [--writer W] [--json]\ncanvas log <id> [--limit N] [--json]   # append-only edit ledger\ncanvas path [<id>]\ncanvas workspace <subcommand>\ncanvas desk <subcommand>\n```\n\n`workspace` is the grouping/branding verb set — see `docs/workspaces.md` for the\nfull guide:\n\n```sh\ncanvas workspace init <name> [--title T] [brand flags…]   # create (idempotent)\ncanvas workspace ls                                       # list + note counts\ncanvas workspace show <name>                              # definition + effective brand\ncanvas workspace brand <name> [brand flags…]              # mark, palette, type, corners\ncanvas workspace add <name> <id>…                         # put notes in it\ncanvas workspace remove <id>…                             # take notes out\ncanvas workspace notes <name>                             # list member notes\ncanvas workspace rm <name> [--detach]                     # forget the definition\n```\n\n`new` and `present` also take `--workspace <name>` so a note can be filed on\ncreation. Membership is one `blink.workspace:` key in the note's frontmatter;\nthe brand itself lives in `config.json`, so note markdown stays portable and\nfree of presentation-only branding. In the running app, the capture popover's\nworkspace selector filters the log/canvas and recalls only that workspace's\nopen panels; notes created while a named workspace is active inherit it.\n\n`present` is the compound arrival verb: it sets a note's markdown **and** its\n`blink:` presentation metadata in one write, get-or-create\nby id. Only the fields you pass change; the rest are preserved. Its options mirror\nthe `blink:` keys: `--style --sheet --accent --font --font-size --line-height\n--tint --tint-read --tint-edit --radius --slot`. `--slot 1–9` is placement intent\nfor the grid. Omit the text to change presentation alone.\n\n`type` and `write` are the two ways to change a note's body, and they differ only\nin *how the open panel reacts*: `type` appends an anchored suffix, so the running\napp reveals it character by character (the visible hand); `write` replaces the\nwhole body, so the panel updates in place with no animation. `append` is the\nestablished sibling of `type` (identical behavior). All three preserve presentation\nand foreign frontmatter.\n\n`--writer` stamps `blink.lastWriter` on the note and appends a row to\n`<home>/edits.sqlite` under the selected data directory.\nThe markdown file stays truth: a ledger failure never fails the save.\nOmit `--writer` and the CLI records `cli`. In-app edits record `user`.\n`canvas log` reads that sidecar even after `rm`; it is not a second note store.\n\n\n`desk` is the intentionally small live counterpart to the file API. It asks the\nrunning Canvas app to realize an existing note as a panel while `PanelManager`\ncontinues to own one-panel-per-note identity, focus, save flushing, and geometry\npersistence. Coordinates are display-local AppKit points measured from the\nvisible top-left corner; omitted frame values preserve the current value.\n`--display N` selects a one-based display (`1` is the main display); otherwise\nthe panel stays on its current display.\n\n```sh\ncanvas desk open standup --display 1 --x 980 --y 90 --width 460 --height 260\ncanvas desk move standup --x 1130 --y 70     # visible lock-and-settle\ncanvas desk focus standup\ncanvas desk close standup                    # closes the panel, keeps the note\ncanvas desk screens --json                   # responsive-script display geometry\n```\n\nExamples:\n\n```sh\ncanvas new \"grocery run\"                    # → grocery-run\nprintf '# Standup\\n\\n- ship CLI\\n' | canvas new --json\ncanvas present q3-planning \"# Q3 Planning\" --style focus --slot 6   # content + look + place\ncanvas present q3-planning --accent \"#9ece6a\"   # presentation-only; body untouched\ncanvas append grocery-run \"- oat milk\"      # types on live if the panel is open\ncanvas type standup \"shipped the CLI verbs\"     # same visible-hand reveal, phrasebook name\nprintf '%s\\n' '- ship docs' | canvas append standup --json\ncanvas write standup < revised-standup.md       # replace the body, no animation\ncanvas cat grocery-run\ncanvas search standup --json | jq '.[0].id'\nopen \"$(canvas path grocery-run)\"           # hand the file to anything\n\ncanvas workspace init \"Acme Docs\"                     # a named, brandable group\ncanvas new --workspace acme-docs \"# Q3 Planning\"      # file a note into it\ncanvas workspace brand acme-docs --accent \"#7aa2f7\" --install-mark ~/acme.svg\ncanvas workspace notes acme-docs --json | jq -r '.[].id'\n```\n\n## JSON shape\n\n```jsonc\n{\n  \"id\": \"standup\",\n  \"title\": \"Standup\",\n  \"tags\": [],\n  \"pinned\": false,\n  \"created\": \"2026-07-14T19:51:41.934Z\",\n  \"updated\": \"2026-07-14T19:51:41.934Z\",\n  \"path\": \"/Users/you/Library/Application Support/Blink/Notes/standup.md\",\n  \"content\": \"…\",             // cat/append --json only\n  \"extraFrontmatter\": []        // cat/append --json only — foreign keys, preserved verbatim\n}\n```\n\n## Or skip the CLI entirely\n\nThe filesystem is the API (layer 1): notes are frontmattered markdown, one\nfile per note. You may edit them with anything — Canvas preserves frontmatter\nkeys it doesn't own and merges on-disk metadata before every save, so foreign\nkeys survive. The CLI just adds atomic writes, correct slug assignment, and\nstructured output for free. Prefer it for *creating* notes (slug uniqueness),\n`append` when an agent should add a visible update without replacing the note,\nand any scripted workflow. `append` always writes exactly one separating\nnewline before the supplied text; stdin bytes after that separator are kept as\nprovided.\n\n`canvas link` and a product MCP server are not implemented. The agent API and\nMCP documents describe proposals; the existing local socket serves live panel\ncommands."
    },
    {
      "id": "blink:placement",
      "slug": "placement",
      "product": "blink",
      "kind": "doc",
      "catalogKind": "guide",
      "current": true,
      "title": "Placement",
      "description": "The grid placement model for note panels: what ships today and what is proposed.",
      "order": null,
      "sourcePath": "products/blink/docs/placement.md",
      "url": "/canvas/docs/placement",
      "rawUrl": "/canvas/docs/markdown/placement.md",
      "markdownUrl": "/canvas/docs/placement.md",
      "headings": [
        {
          "depth": 1,
          "text": "Canvas — Desk placement model",
          "anchor": "canvas-desk-placement-model"
        },
        {
          "depth": 2,
          "text": "Thesis: one primitive, everything else is sugar",
          "anchor": "thesis-one-primitive-everything-else-is-sugar"
        },
        {
          "depth": 2,
          "text": "Agent-facing targets",
          "anchor": "agent-facing-targets"
        },
        {
          "depth": 2,
          "text": "Declarative home, not imperative moves",
          "anchor": "declarative-home-not-imperative-moves"
        },
        {
          "depth": 2,
          "text": "Determinism is non-negotiable",
          "anchor": "determinism-is-non-negotiable"
        },
        {
          "depth": 2,
          "text": "Provenance-gated collisions (live plane)",
          "anchor": "provenance-gated-collisions-live-plane"
        },
        {
          "depth": 2,
          "text": "Reflow: sticky by default, explicit tidy",
          "anchor": "reflow-sticky-by-default-explicit-tidy"
        },
        {
          "depth": 2,
          "text": "Scenes: declarative sets",
          "anchor": "scenes-declarative-sets"
        },
        {
          "depth": 2,
          "text": "Multi-screen",
          "anchor": "multi-screen"
        },
        {
          "depth": 2,
          "text": "Deliberately skipped",
          "anchor": "deliberately-skipped"
        },
        {
          "depth": 2,
          "text": "Where each piece lives",
          "anchor": "where-each-piece-lives"
        }
      ],
      "tokensEstimate": 1301,
      "frontmatter": {
        "title": "Placement",
        "description": "The grid placement model for note panels: what ships today and what is proposed."
      },
      "markdown": "---\ntitle: Placement\ndescription: The grid placement model for note panels: what ships today and what is proposed.\n---\n\n# Canvas — Desk placement model\n\n> Draft 2026-07-15. How agents (and the user) put notes *where they go* on screen.\n> Canvas's own design; the good ideas are adapted from **Lattices'** placement\n> engine (one primitive + aliases, declarative, deterministic, provenance-gated)\n> — the ideas, not a dependency. Companion to `agent-integration.md` (the verbs)\n> and `cli.md`.\n\n> **Status: mixed.** Shipped: the `BlinkGrid` primitive and the `slot 1…9`\n> home (`blink present --slot N`, `notes.present` on the\n> [agent socket](/canvas/docs/api)), plus named desks (`blink desk save` /\n> `restore`). Proposed and not built: named-zone and span targets in the CLI or\n> frontmatter, `auto`, provenance-gated collisions, `tidy`, scenes, and display\n> addressing by stable id. The agent socket exists, but these parts did not ship\n> with it.\n\n## Thesis: one primitive, everything else is sugar\n\nThere is exactly one placement primitive; every target an agent can name compiles\ndown to it:\n\n```\ngrid:C×R : col,row (+colSpan×rowSpan)\nframe = visibleFrame.origin + visibleFrame.size × fraction    // per target screen\n```\n\n`slot 1…9` is sugar for a single cell in a 3×3 grid (row-major, matching the\n`Q W E / A S D / Z X C` keys). Named zones, halves, thirds, quarters, and `full`\nare just other `C×R` shapes. Spans are two inclusive corners. All of it is one\nfunction — `BlinkGrid.frame(for:in:)` — so halves and 2×2 blocks fall out for\nfree and never drift from the visual grid overlay (which shares the same code).\n\n**Why a primitive and not N hard-coded cells:** reproducibility. A placement is\nfully described by a tiny string, so the same request always yields the same\nframe — the property that lets an agent *and* the user build spatial memory.\n\n## Agent-facing targets\n\n| Form | Example | Status |\n|---|---|---|\n| slot number | `--slot 6` | **shipped** (frontmatter `blink.slot`) |\n| named zone | `top-left`, `center`, `right` | geometry ready; CLI/frontmatter next |\n| half / third / quarter | `left-half`, `middle-third`, `top-left-quarter` | geometry ready |\n| full area | `full` / `fill` | geometry ready |\n| raw spec | `3x3:2,1`, `2x2:1,0+1x2` | geometry ready |\n| **auto** | `--slot auto` | with the socket (needs live occupancy) |\n\nNamed targets are the point — agents reason in \"top-right / next to the outline /\nsomewhere sensible,\" not pixels. Pixels stay an escape hatch, never the interface.\n\n## Declarative home, not imperative moves\n\n`blink.slot` (and later `blink.place`) is the **source of truth**. `present`\n*converges* the panel to it: **idempotent** (already there → no-op) and it should\n**echo the resolved placement** back in its output. This is why placement survives\nrestarts and stays reproducible — the note carries its home.\n\n## Determinism is non-negotiable\n\n`auto` = the first free slot in a **fixed priority order** (\"edges first, keep the\ncenter clear\"), never random or time-based. Deterministic auto is what makes an\nagent's \"put this somewhere sensible\" predictable enough to trust.\n\n## Provenance-gated collisions *(live plane)*\n\nEvery placement is tagged `user` or `agent`. Precedence:\n\n```\nuser-pinned  >  declarative-home  >  agent-auto\n```\n\n- **Master rule: never auto-move a panel the user placed by hand.** A user drag\n  pins the note; agents route around it. This single rule preserves spatial memory.\n- **Occupied cell → route around, don't displace:** spiral to the nearest free\n  cell in a *fixed* order (deterministic; not a pixel cascade, which drifts\n  off-grid and accumulates mess).\n- **Displace only** when a declarative-home placement lands on a *homeless agent*\n  panel — and move that occupant to nearest-free, never onto a pinned one.\n- **One writer:** slot allocation serializes through the menubar process, so two\n  agents placing at once can't race.\n\n## Reflow: sticky by default, explicit `tidy`\n\nAdding or removing a note **frees a slot but moves nothing**. Motion the user\ndidn't ask for is the fastest way to lose their trust. A dedicated `tidy` /\n`relayout` verb is the *sanctioned* moment things rearrange — never a side effect.\n\n## Scenes: declarative sets\n\nA **scene** is a named `{ note → placement }` mapping applied in one atomic pass\n(conflicts resolved by the scene's own order). Plus **capture-current-as-scene**:\nthe user arranges panels by hand, then snapshots that arrangement into a named\nscene. Cheap, high value.\n\n## Multi-screen\n\n- Default to the note's **current screen** (the display containing its center).\n- The grid is **per-screen**; a display is an optional *axis* of the same\n  primitive (`slot 6 on display 2`).\n- Address displays by **stable id** (UUID / friendly name), never array index —\n  index is fragile across hotplug and rearrangement.\n- Resolver order: explicit display → screen-containing-center → main → first.\n\n## Deliberately skipped\n\nLattices tiles arbitrary *foreign* windows over Accessibility APIs; Canvas owns its\npanels and gives each note a declarative home, so we skip its scar tissue:\n\n- **masonry / bin-packing / auto-tiling** — non-reproducible in a fixed-home model\n- **implicit reflow on add/remove** — the single biggest trust trap\n- **pixel cascade** as a collision primary — nearest-free *cell* stays crisp\n- **nondeterministic `auto`**\n- **heavy ghost/screenshot preview + fly-in** — highlighting the target cell\n  outline is enough; the rest is polish, not core\n- **capture-frontmost dance** — we address notes by id, so we never \"act on\n  whatever's focused\"\n- **drag-notification logic** — if we add snap-on-drag, act on gesture end\n  (mouseUp + distance threshold), not the ~60fps move stream\n\n## Where each piece lives\n\n- **Shipped:** `BlinkGrid` primitive + spans + named/spec resolver; `slot 1…9`\n  place-on-open (declarative home) and live slot-move via `blink present --slot N`;\n  shared geometry with the visual `GridOverlay`.\n- **Shipped, related:** named desks. `blink desk save` captures the current\n  arrangement, which covers the \"capture-current-as-scene\" idea above without\n  the declarative scene format.\n- **Proposed, not built:** `blink.place` (named + span) via `--place`;\n  deterministic `auto`; provenance + collision routing; `tidy`; declarative\n  scenes; multi-screen by stable id. Today `blink desk` addresses displays by\n  one-based index.\n",
      "content": "# Canvas — Desk placement model\n\n> Draft 2026-07-15. How agents (and the user) put notes *where they go* on screen.\n> Canvas's own design; the good ideas are adapted from **Lattices'** placement\n> engine (one primitive + aliases, declarative, deterministic, provenance-gated)\n> — the ideas, not a dependency. Companion to `agent-integration.md` (the verbs)\n> and `cli.md`.\n\n> **Status: mixed.** Shipped: the `BlinkGrid` primitive and the `slot 1…9`\n> home (`blink present --slot N`, `notes.present` on the\n> [agent socket](/canvas/docs/api)), plus named desks (`blink desk save` /\n> `restore`). Proposed and not built: named-zone and span targets in the CLI or\n> frontmatter, `auto`, provenance-gated collisions, `tidy`, scenes, and display\n> addressing by stable id. The agent socket exists, but these parts did not ship\n> with it.\n\n## Thesis: one primitive, everything else is sugar\n\nThere is exactly one placement primitive; every target an agent can name compiles\ndown to it:\n\n```\ngrid:C×R : col,row (+colSpan×rowSpan)\nframe = visibleFrame.origin + visibleFrame.size × fraction    // per target screen\n```\n\n`slot 1…9` is sugar for a single cell in a 3×3 grid (row-major, matching the\n`Q W E / A S D / Z X C` keys). Named zones, halves, thirds, quarters, and `full`\nare just other `C×R` shapes. Spans are two inclusive corners. All of it is one\nfunction — `BlinkGrid.frame(for:in:)` — so halves and 2×2 blocks fall out for\nfree and never drift from the visual grid overlay (which shares the same code).\n\n**Why a primitive and not N hard-coded cells:** reproducibility. A placement is\nfully described by a tiny string, so the same request always yields the same\nframe — the property that lets an agent *and* the user build spatial memory.\n\n## Agent-facing targets\n\n| Form | Example | Status |\n|---|---|---|\n| slot number | `--slot 6` | **shipped** (frontmatter `blink.slot`) |\n| named zone | `top-left`, `center`, `right` | geometry ready; CLI/frontmatter next |\n| half / third / quarter | `left-half`, `middle-third`, `top-left-quarter` | geometry ready |\n| full area | `full` / `fill` | geometry ready |\n| raw spec | `3x3:2,1`, `2x2:1,0+1x2` | geometry ready |\n| **auto** | `--slot auto` | with the socket (needs live occupancy) |\n\nNamed targets are the point — agents reason in \"top-right / next to the outline /\nsomewhere sensible,\" not pixels. Pixels stay an escape hatch, never the interface.\n\n## Declarative home, not imperative moves\n\n`blink.slot` (and later `blink.place`) is the **source of truth**. `present`\n*converges* the panel to it: **idempotent** (already there → no-op) and it should\n**echo the resolved placement** back in its output. This is why placement survives\nrestarts and stays reproducible — the note carries its home.\n\n## Determinism is non-negotiable\n\n`auto` = the first free slot in a **fixed priority order** (\"edges first, keep the\ncenter clear\"), never random or time-based. Deterministic auto is what makes an\nagent's \"put this somewhere sensible\" predictable enough to trust.\n\n## Provenance-gated collisions *(live plane)*\n\nEvery placement is tagged `user` or `agent`. Precedence:\n\n```\nuser-pinned  >  declarative-home  >  agent-auto\n```\n\n- **Master rule: never auto-move a panel the user placed by hand.** A user drag\n  pins the note; agents route around it. This single rule preserves spatial memory.\n- **Occupied cell → route around, don't displace:** spiral to the nearest free\n  cell in a *fixed* order (deterministic; not a pixel cascade, which drifts\n  off-grid and accumulates mess).\n- **Displace only** when a declarative-home placement lands on a *homeless agent*\n  panel — and move that occupant to nearest-free, never onto a pinned one.\n- **One writer:** slot allocation serializes through the menubar process, so two\n  agents placing at once can't race.\n\n## Reflow: sticky by default, explicit `tidy`\n\nAdding or removing a note **frees a slot but moves nothing**. Motion the user\ndidn't ask for is the fastest way to lose their trust. A dedicated `tidy` /\n`relayout` verb is the *sanctioned* moment things rearrange — never a side effect.\n\n## Scenes: declarative sets\n\nA **scene** is a named `{ note → placement }` mapping applied in one atomic pass\n(conflicts resolved by the scene's own order). Plus **capture-current-as-scene**:\nthe user arranges panels by hand, then snapshots that arrangement into a named\nscene. Cheap, high value.\n\n## Multi-screen\n\n- Default to the note's **current screen** (the display containing its center).\n- The grid is **per-screen**; a display is an optional *axis* of the same\n  primitive (`slot 6 on display 2`).\n- Address displays by **stable id** (UUID / friendly name), never array index —\n  index is fragile across hotplug and rearrangement.\n- Resolver order: explicit display → screen-containing-center → main → first.\n\n## Deliberately skipped\n\nLattices tiles arbitrary *foreign* windows over Accessibility APIs; Canvas owns its\npanels and gives each note a declarative home, so we skip its scar tissue:\n\n- **masonry / bin-packing / auto-tiling** — non-reproducible in a fixed-home model\n- **implicit reflow on add/remove** — the single biggest trust trap\n- **pixel cascade** as a collision primary — nearest-free *cell* stays crisp\n- **nondeterministic `auto`**\n- **heavy ghost/screenshot preview + fly-in** — highlighting the target cell\n  outline is enough; the rest is polish, not core\n- **capture-frontmost dance** — we address notes by id, so we never \"act on\n  whatever's focused\"\n- **drag-notification logic** — if we add snap-on-drag, act on gesture end\n  (mouseUp + distance threshold), not the ~60fps move stream\n\n## Where each piece lives\n\n- **Shipped:** `BlinkGrid` primitive + spans + named/spec resolver; `slot 1…9`\n  place-on-open (declarative home) and live slot-move via `blink present --slot N`;\n  shared geometry with the visual `GridOverlay`.\n- **Shipped, related:** named desks. `blink desk save` captures the current\n  arrangement, which covers the \"capture-current-as-scene\" idea above without\n  the declarative scene format.\n- **Proposed, not built:** `blink.place` (named + span) via `--place`;\n  deterministic `auto`; provenance + collision routing; `tidy`; declarative\n  scenes; multi-screen by stable id. Today `blink desk` addresses displays by\n  one-based index."
    },
    {
      "id": "blink:workspaces",
      "slug": "workspaces",
      "product": "blink",
      "kind": "doc",
      "catalogKind": "guide",
      "current": true,
      "title": "Canvas workspaces",
      "description": "A workspace is a named group of notes with shared presentation settings.\nThe notes remain plain Markdown. See the CLI reference and\nconfiguration guide for the full command and config surfaces.",
      "order": null,
      "sourcePath": "products/blink/docs/workspaces.md",
      "url": "/canvas/docs/workspaces",
      "rawUrl": "/canvas/docs/markdown/workspaces.md",
      "markdownUrl": "/canvas/docs/workspaces.md",
      "headings": [
        {
          "depth": 1,
          "text": "Canvas workspaces",
          "anchor": "canvas-workspaces"
        },
        {
          "depth": 2,
          "text": "Definition and membership",
          "anchor": "definition-and-membership"
        },
        {
          "depth": 2,
          "text": "Quick start",
          "anchor": "quick-start"
        },
        {
          "depth": 2,
          "text": "Commands",
          "anchor": "commands"
        },
        {
          "depth": 2,
          "text": "Defining a brand",
          "anchor": "defining-a-brand"
        },
        {
          "depth": 3,
          "text": "Sharing a house style",
          "anchor": "sharing-a-house-style"
        },
        {
          "depth": 2,
          "text": "Brand assets, safely",
          "anchor": "brand-assets-safely"
        },
        {
          "depth": 2,
          "text": "How a note's look is decided",
          "anchor": "how-a-note-s-look-is-decided"
        },
        {
          "depth": 2,
          "text": "Defaults and compatibility",
          "anchor": "defaults-and-compatibility"
        },
        {
          "depth": 2,
          "text": "Durable group, live desktop",
          "anchor": "durable-group-live-desktop"
        }
      ],
      "tokensEstimate": 1751,
      "frontmatter": {},
      "markdown": "# Canvas workspaces\n\nA **workspace** is a named group of notes with shared presentation settings.\nThe notes remain plain Markdown. See the [CLI reference](cli.md) and\n[configuration guide](config.md) for the full command and config surfaces.\n\nExamples use the current source command `canvas`; `blink` accepts the same\nworkspace verbs on existing installations. The published package remains\n`@arach/blink`. The rename preserves `blink.workspace`, `blink.style`, and the\nother stored frontmatter and config keys.\n\nCurrent source builds use the first nonempty `CANVAS_HOME`, then `BLINK_HOME`,\nfor the file-backed root; empty values are ignored. The default remains\n`~/Library/Application Support/Blink`. Use the same root for app and CLI.\nOlder releases recognize `BLINK_HOME` only.\n\n## Definition and membership\n\nA workspace is two halves, kept deliberately apart:\n\n| Half | Lives in | Holds |\n|---|---|---|\n| **Definition** | `config.json` → `workspaces.<name>` | title + brand (mark, palette, typography, corners) |\n| **Membership** | each note's frontmatter → `blink.workspace` | one name. Nothing else. |\n\nThat split is the whole design. The brand is edited in exactly one place, and a\nnote carries a workspace *name* — never a color, a font, or an asset path. So a\nbranded note is still a boring `.md` file that works in Obsidian, git, or\n`grep`, five years after Canvas is uninstalled.\n\n```markdown\n---\nid: q3-planning\ncreated: 2026-07-25T22:39:44.042Z\nupdated: 2026-07-25T22:39:44.042Z\ntags: []\npinned: false\nblink:\n  workspace: acme-docs\n---\n# Q3 Planning\n```\n\nThat is the entire on-disk cost of belonging to a fully branded workspace.\n\n## Quick start\n\n```sh\ncanvas workspace init \"Acme Docs\" --title \"Acme Documentation\"\ncanvas new --workspace acme-docs \"# Q3 Planning\"\ncanvas new --workspace acme-docs \"# Onboarding\"\n\ncanvas workspace brand acme-docs \\\n  --background \"#0b0d0c\" --text \"#f2f4ef\" --accent \"#7aa2f7\" \\\n  --title-font \"Inter, system-ui\" --radius 6 \\\n  --install-mark ~/brand/acme.svg\n\ncanvas workspace notes acme-docs      # what's in here\n```\n\nThe running app picks all of it up live — it watches both the notes directory\nand `config.json`, so a brand change re-themes open panels without a restart.\n\n## Commands\n\n```sh\ncanvas workspace init <name> [--title T] [brand flags…]   # create (idempotent)\ncanvas workspace ls                                       # list + note counts\ncanvas workspace show <name>                              # definition + effective brand\ncanvas workspace brand <name> [brand flags…]              # set/adjust the brand\ncanvas workspace add <name> <id>…                         # put notes in it\ncanvas workspace remove <id>…                             # take notes out\ncanvas workspace notes <name>                             # list member notes\ncanvas workspace rm <name> [--detach]                     # forget the definition\n```\n\nPlus membership on the note verbs you already use:\n\n```sh\ncanvas new --workspace <name> \"# Title\"\ncanvas present <id> --workspace <name> [--style … --slot …]\n```\n\nEvery command takes `--json`. Errors go to stderr with a non-zero exit code.\n\nNames are normalized to slugs, so `\"Acme Docs\"` and `acme-docs` address the same\nworkspace and you can pass whichever you have.\n\n## Defining a brand\n\nA brand is a **treatment** — the same generic, reusable preset `styles` uses, so\nthere is one presentation vocabulary in Canvas, not two. Every field is optional;\nwhat you don't set is inherited.\n\n| Group | Flags |\n|---|---|\n| Surface | `--sheet --background --radius --tint --tint-read --tint-edit` |\n| Typography | `--font --mono --title-font --font-size --line-height` |\n| Palette | `--text --text-strong --text-muted --dim --border --accent --accent-dim --code-background --code-text --caret --selection` |\n| Identity | `--mark` (reference) · `--install-mark` (copy in) |\n| Bulk | `--brand-from <file.json>` · `--style <name>` · `--clear <field>` |\n\n`brand` is a partial overlay, so adjusting one color never restates the rest:\n\n```sh\ncanvas workspace brand acme-docs --accent \"#9ece6a\"     # only the accent moves\ncanvas workspace brand acme-docs --clear mark           # back to inheriting\n```\n\nTo define a whole brand in one call, hand it a JSON file:\n\n```sh\ncat > acme-brand.json <<'JSON'\n{\n  \"background\": \"#0b0d0c\",\n  \"text\": \"#f2f4ef\",\n  \"accent\": \"#7aa2f7\",\n  \"border\": \"rgba(239,244,237,.09)\",\n  \"titleFont\": \"Inter, system-ui\",\n  \"radius\": 6\n}\nJSON\ncanvas workspace brand acme-docs --brand-from acme-brand.json\n```\n\n### Sharing a house style\n\n`--style <name>` names an entry in `config.json` → `styles` to use as the brand's\nbase, with the workspace's own brand overlaid on top. Several workspaces can\nthen share one house style and differ only where they mean to:\n\n```sh\ncanvas workspace brand acme-docs   --style terminal --accent \"#7aa2f7\"\ncanvas workspace brand acme-alerts --style terminal --accent \"#f7768e\"\n```\n\n`canvas workspace show <name> --json` reports both the stored `brand` and the\n`effectiveBrand` (base + overlay, already composed) so an agent never has to\nrecompute the merge itself.\n\n## Brand assets, safely\n\nCanvas renders marks **only** from inside `<home>/attachments`, using the root\ndescribed above. This is a containment boundary: absolute paths, `../` traversal, and\nsymlinks pointing out of the directory are all refused, so a bad brand value\nbecomes an absent mark rather than an arbitrary file read.\n\nTwo ways to set one, and the first is the one to reach for:\n\n```sh\n# Install: copies the asset into attachments/marks/<workspace>.<ext> and\n# records the relative path for you. Validates type and size (≤2 MB).\ncanvas workspace brand acme-docs --install-mark ~/brand/acme.svg\n\n# Reference: for an asset already inside the attachments directory.\ncanvas workspace brand acme-docs --mark marks/acme.svg\n```\n\nSupported: `svg png jpg jpeg gif webp pdf tiff`. The mark renders at 20px in the\npanel's top-left chrome and yields that spot to the close control on hover;\nmarked panels get a slightly wider content gutter so the identity never collides\nwith the first heading.\n\n`show` tells you whether a mark actually resolves — a mark recorded but missing\nfrom disk is flagged rather than silently ignored:\n\n```sh\ncanvas workspace show acme-docs --json | jq '.mark'\n# { \"path\": \"marks/acme.svg\", \"resolved\": \"/…/attachments/marks/acme.svg\", \"installed\": true }\n```\n\n## How a note's look is decided\n\nLeast specific first, so **later wins**:\n\n```\nconfig defaults ← workspace brand ← blink.style ← loose blink: keys\n```\n\nA note inherits its workspace's identity for free; a per-note `style` still\noverrides the brand; a loose `blink:` key beats both. The brand is a default,\nnever a cage.\n\n```sh\ncanvas present exception --workspace acme-docs --accent \"#f7768e\"\n# ↑ Acme's brand, except this one note runs a red accent.\n```\n\n## Defaults and compatibility\n\n- **Unbranded is the default.** Canvas with no workspaces behaves exactly as it\n  did before this feature existed. A workspace with no brand is valid and\n  renders with Canvas's defaults.\n- **Unknown names never fail.** A note pointing at a workspace or style that\n  isn't defined renders unbranded — it always opens.\n- **Existing notes and configs are untouched.** `blink.workspace` is a new,\n  optional key in the `blink:` block; `workspaces` is a new, optional config key.\n  Notes without them are unaffected, and `blink.style` keeps working exactly as\n  before.\n- **Your config is not rewritten.** `canvas workspace` parses `config.json` as\n  generic JSON, replaces only the `workspaces` key, and writes everything else\n  back verbatim — including keys this build has never heard of. A config it\n  cannot parse is refused, never overwritten.\n- **Forgetting a workspace costs a look, never a note.** `canvas workspace rm`\n  removes the definition only; member notes keep their content and simply render\n  unbranded. Add `--detach` to also clear membership from each member note.\n\n## Durable group, live desktop\n\n`canvas workspace` manages *durable* state — the files and the config. The\nrunning app owns the live plane: choose a workspace from the capture popover to\nfilter its log and canvas and recall only that workspace's already-open panels.\nPanels in other workspaces are hidden, never closed, so their pending text,\nexact frames, and session membership survive the switch. New notes inherit the\nactive named workspace; **All notes** and **Unfiled** create unfiled notes.\n\nThe file-backed `workspace` verbs do not mutate live panels. The `desk`\ncompanion sends acknowledged open, move, focus, and close requests to the\nrunning Canvas instance with the same selected data root. An agent can inspect\na group and explicitly present the notes it wants on that live desktop:\n\n```sh\ncanvas workspace notes acme-docs --json \\\n  | jq -r '.[].id' \\\n  | while read -r id; do canvas desk open \"$id\"; done\n```\n",
      "content": "# Canvas workspaces\n\nA **workspace** is a named group of notes with shared presentation settings.\nThe notes remain plain Markdown. See the [CLI reference](cli.md) and\n[configuration guide](config.md) for the full command and config surfaces.\n\nExamples use the current source command `canvas`; `blink` accepts the same\nworkspace verbs on existing installations. The published package remains\n`@arach/blink`. The rename preserves `blink.workspace`, `blink.style`, and the\nother stored frontmatter and config keys.\n\nCurrent source builds use the first nonempty `CANVAS_HOME`, then `BLINK_HOME`,\nfor the file-backed root; empty values are ignored. The default remains\n`~/Library/Application Support/Blink`. Use the same root for app and CLI.\nOlder releases recognize `BLINK_HOME` only.\n\n## Definition and membership\n\nA workspace is two halves, kept deliberately apart:\n\n| Half | Lives in | Holds |\n|---|---|---|\n| **Definition** | `config.json` → `workspaces.<name>` | title + brand (mark, palette, typography, corners) |\n| **Membership** | each note's frontmatter → `blink.workspace` | one name. Nothing else. |\n\nThat split is the whole design. The brand is edited in exactly one place, and a\nnote carries a workspace *name* — never a color, a font, or an asset path. So a\nbranded note is still a boring `.md` file that works in Obsidian, git, or\n`grep`, five years after Canvas is uninstalled.\n\n```markdown\n---\nid: q3-planning\ncreated: 2026-07-25T22:39:44.042Z\nupdated: 2026-07-25T22:39:44.042Z\ntags: []\npinned: false\nblink:\n  workspace: acme-docs\n---\n# Q3 Planning\n```\n\nThat is the entire on-disk cost of belonging to a fully branded workspace.\n\n## Quick start\n\n```sh\ncanvas workspace init \"Acme Docs\" --title \"Acme Documentation\"\ncanvas new --workspace acme-docs \"# Q3 Planning\"\ncanvas new --workspace acme-docs \"# Onboarding\"\n\ncanvas workspace brand acme-docs \\\n  --background \"#0b0d0c\" --text \"#f2f4ef\" --accent \"#7aa2f7\" \\\n  --title-font \"Inter, system-ui\" --radius 6 \\\n  --install-mark ~/brand/acme.svg\n\ncanvas workspace notes acme-docs      # what's in here\n```\n\nThe running app picks all of it up live — it watches both the notes directory\nand `config.json`, so a brand change re-themes open panels without a restart.\n\n## Commands\n\n```sh\ncanvas workspace init <name> [--title T] [brand flags…]   # create (idempotent)\ncanvas workspace ls                                       # list + note counts\ncanvas workspace show <name>                              # definition + effective brand\ncanvas workspace brand <name> [brand flags…]              # set/adjust the brand\ncanvas workspace add <name> <id>…                         # put notes in it\ncanvas workspace remove <id>…                             # take notes out\ncanvas workspace notes <name>                             # list member notes\ncanvas workspace rm <name> [--detach]                     # forget the definition\n```\n\nPlus membership on the note verbs you already use:\n\n```sh\ncanvas new --workspace <name> \"# Title\"\ncanvas present <id> --workspace <name> [--style … --slot …]\n```\n\nEvery command takes `--json`. Errors go to stderr with a non-zero exit code.\n\nNames are normalized to slugs, so `\"Acme Docs\"` and `acme-docs` address the same\nworkspace and you can pass whichever you have.\n\n## Defining a brand\n\nA brand is a **treatment** — the same generic, reusable preset `styles` uses, so\nthere is one presentation vocabulary in Canvas, not two. Every field is optional;\nwhat you don't set is inherited.\n\n| Group | Flags |\n|---|---|\n| Surface | `--sheet --background --radius --tint --tint-read --tint-edit` |\n| Typography | `--font --mono --title-font --font-size --line-height` |\n| Palette | `--text --text-strong --text-muted --dim --border --accent --accent-dim --code-background --code-text --caret --selection` |\n| Identity | `--mark` (reference) · `--install-mark` (copy in) |\n| Bulk | `--brand-from <file.json>` · `--style <name>` · `--clear <field>` |\n\n`brand` is a partial overlay, so adjusting one color never restates the rest:\n\n```sh\ncanvas workspace brand acme-docs --accent \"#9ece6a\"     # only the accent moves\ncanvas workspace brand acme-docs --clear mark           # back to inheriting\n```\n\nTo define a whole brand in one call, hand it a JSON file:\n\n```sh\ncat > acme-brand.json <<'JSON'\n{\n  \"background\": \"#0b0d0c\",\n  \"text\": \"#f2f4ef\",\n  \"accent\": \"#7aa2f7\",\n  \"border\": \"rgba(239,244,237,.09)\",\n  \"titleFont\": \"Inter, system-ui\",\n  \"radius\": 6\n}\nJSON\ncanvas workspace brand acme-docs --brand-from acme-brand.json\n```\n\n### Sharing a house style\n\n`--style <name>` names an entry in `config.json` → `styles` to use as the brand's\nbase, with the workspace's own brand overlaid on top. Several workspaces can\nthen share one house style and differ only where they mean to:\n\n```sh\ncanvas workspace brand acme-docs   --style terminal --accent \"#7aa2f7\"\ncanvas workspace brand acme-alerts --style terminal --accent \"#f7768e\"\n```\n\n`canvas workspace show <name> --json` reports both the stored `brand` and the\n`effectiveBrand` (base + overlay, already composed) so an agent never has to\nrecompute the merge itself.\n\n## Brand assets, safely\n\nCanvas renders marks **only** from inside `<home>/attachments`, using the root\ndescribed above. This is a containment boundary: absolute paths, `../` traversal, and\nsymlinks pointing out of the directory are all refused, so a bad brand value\nbecomes an absent mark rather than an arbitrary file read.\n\nTwo ways to set one, and the first is the one to reach for:\n\n```sh\n# Install: copies the asset into attachments/marks/<workspace>.<ext> and\n# records the relative path for you. Validates type and size (≤2 MB).\ncanvas workspace brand acme-docs --install-mark ~/brand/acme.svg\n\n# Reference: for an asset already inside the attachments directory.\ncanvas workspace brand acme-docs --mark marks/acme.svg\n```\n\nSupported: `svg png jpg jpeg gif webp pdf tiff`. The mark renders at 20px in the\npanel's top-left chrome and yields that spot to the close control on hover;\nmarked panels get a slightly wider content gutter so the identity never collides\nwith the first heading.\n\n`show` tells you whether a mark actually resolves — a mark recorded but missing\nfrom disk is flagged rather than silently ignored:\n\n```sh\ncanvas workspace show acme-docs --json | jq '.mark'\n# { \"path\": \"marks/acme.svg\", \"resolved\": \"/…/attachments/marks/acme.svg\", \"installed\": true }\n```\n\n## How a note's look is decided\n\nLeast specific first, so **later wins**:\n\n```\nconfig defaults ← workspace brand ← blink.style ← loose blink: keys\n```\n\nA note inherits its workspace's identity for free; a per-note `style` still\noverrides the brand; a loose `blink:` key beats both. The brand is a default,\nnever a cage.\n\n```sh\ncanvas present exception --workspace acme-docs --accent \"#f7768e\"\n# ↑ Acme's brand, except this one note runs a red accent.\n```\n\n## Defaults and compatibility\n\n- **Unbranded is the default.** Canvas with no workspaces behaves exactly as it\n  did before this feature existed. A workspace with no brand is valid and\n  renders with Canvas's defaults.\n- **Unknown names never fail.** A note pointing at a workspace or style that\n  isn't defined renders unbranded — it always opens.\n- **Existing notes and configs are untouched.** `blink.workspace` is a new,\n  optional key in the `blink:` block; `workspaces` is a new, optional config key.\n  Notes without them are unaffected, and `blink.style` keeps working exactly as\n  before.\n- **Your config is not rewritten.** `canvas workspace` parses `config.json` as\n  generic JSON, replaces only the `workspaces` key, and writes everything else\n  back verbatim — including keys this build has never heard of. A config it\n  cannot parse is refused, never overwritten.\n- **Forgetting a workspace costs a look, never a note.** `canvas workspace rm`\n  removes the definition only; member notes keep their content and simply render\n  unbranded. Add `--detach` to also clear membership from each member note.\n\n## Durable group, live desktop\n\n`canvas workspace` manages *durable* state — the files and the config. The\nrunning app owns the live plane: choose a workspace from the capture popover to\nfilter its log and canvas and recall only that workspace's already-open panels.\nPanels in other workspaces are hidden, never closed, so their pending text,\nexact frames, and session membership survive the switch. New notes inherit the\nactive named workspace; **All notes** and **Unfiled** create unfiled notes.\n\nThe file-backed `workspace` verbs do not mutate live panels. The `desk`\ncompanion sends acknowledged open, move, focus, and close requests to the\nrunning Canvas instance with the same selected data root. An agent can inspect\na group and explicitly present the notes it wants on that live desktop:\n\n```sh\ncanvas workspace notes acme-docs --json \\\n  | jq -r '.[].id' \\\n  | while read -r id; do canvas desk open \"$id\"; done\n```"
    },
    {
      "id": "blink:config",
      "slug": "config",
      "product": "blink",
      "kind": "doc",
      "catalogKind": "guide",
      "current": true,
      "title": "Canvas configuration",
      "description": "Canvas's behavior and theme live in one JSON file:",
      "order": null,
      "sourcePath": "products/blink/docs/config.md",
      "url": "/canvas/docs/config",
      "rawUrl": "/canvas/docs/markdown/config.md",
      "markdownUrl": "/canvas/docs/config.md",
      "headings": [
        {
          "depth": 1,
          "text": "Canvas configuration",
          "anchor": "canvas-configuration"
        },
        {
          "depth": 2,
          "text": "Schema",
          "anchor": "schema"
        },
        {
          "depth": 3,
          "text": "Mobile access",
          "anchor": "mobile-access"
        },
        {
          "depth": 2,
          "text": "How it applies",
          "anchor": "how-it-applies"
        },
        {
          "depth": 2,
          "text": "Reader layer",
          "anchor": "reader-layer"
        },
        {
          "depth": 2,
          "text": "Examples",
          "anchor": "examples"
        },
        {
          "depth": 2,
          "text": "Workspaces",
          "anchor": "workspaces"
        },
        {
          "depth": 2,
          "text": "Motion (Arrival)",
          "anchor": "motion-arrival"
        },
        {
          "depth": 2,
          "text": "Panel physics",
          "anchor": "panel-physics"
        },
        {
          "depth": 2,
          "text": "What does NOT live here",
          "anchor": "what-does-not-live-here"
        },
        {
          "depth": 3,
          "text": "Companion menu icon",
          "anchor": "companion-menu-icon"
        }
      ],
      "tokensEstimate": 3585,
      "frontmatter": {},
      "markdown": "# Canvas configuration\n\nCanvas's behavior and theme live in one JSON file:\n\n```\n~/Library/Application Support/Blink/config.json\n```\n\nCanvas watches this file and applies valid changes to open panels without a\nrestart. The default path is unchanged from Blink.\n\nCurrent source builds use the first nonempty `CANVAS_HOME`, then `BLINK_HOME`,\nto override the file-backed root. Empty values are ignored. The selected root\ncontains `config.json`, `Notes`, and `attachments`; use the same root for app and\nCLI. These overrides do not redirect `UserDefaults`, and either disables mobile\nsharing. Existing releases recognize `BLINK_HOME` only.\n\nExamples use the current `canvas` command. The compatible `blink` command and\npublished package name `@arach/blink` remain supported. The rename does not\nchange config keys, `blink.*` frontmatter, attachment URLs, or settings keys;\n`hotkeys.blink` still controls showing and hiding all notes.\n\nRules:\n- Every field is optional. Missing fields use the defaults below. `{}` is a valid config.\n- Invalid JSON never breaks the app: Canvas keeps the last good config and logs\n  `[BLINK] config invalid — keeping last good`. An invalid file found at startup\n  stays untouched; the app uses defaults with mobile access off.\n- Write atomically (write temp + rename, or your editor's normal save) — Canvas\n  watches the directory, so atomic replaces are picked up fine.\n- The settings window is a *view* over this file; edits there round-trip through it.\n\n## Schema\n\n```jsonc\n{\n  \"appearance\": \"auto\",         // app-wide light/dark: \"auto\" (follow macOS) | \"light\" | \"dark\"\n                                // \"auto\" tracks the system live; menubar → Appearance overrides it\n  \"behavior\": {\n    \"mobileAccess\": false,     // new installs default off; see upgrade policy below\n    \"restoreSession\": true,     // reopen last session's panels at launch\n    \"defaultMode\": \"read\",      // \"read\" | \"edit\" — mode for notes with no remembered mode\n    \"launchAtLogin\": false      // register Canvas as a login item (SMAppService)\n  },\n  \"hotkeys\": {\n    // Chord strings: modifiers joined by \"+\", ending in one key.\n    // Modifiers: hyper (⌃⌥⇧⌘), cmd, ctrl, alt, shift.\n    // Keys: a–z, 0–9, punctuation (. , / ; ' [ ] \\ - = `), space, return,\n    // tab, escape, delete. An invalid chord is logged and the previous\n    // binding kept — a bad edit never leaves the app unreachable.\n    \"newNote\": \"hyper+n\",       // global — create a note from anywhere\n    \"blink\": \"hyper+b\",         // global — show all notes / hide all\n    \"grid\": \"hyper+c\",          // global — grid/constellation overlay (\"c\" — hyper+g collides with Lattices)\n    \"reader\": \"hyper+e\",        // global — read the frontmost app's selection in the reader layer\n    \"toggleMode\": \"cmd+shift+p\",// per-panel — flip read/edit\n    \"focus\": \"cmd+.\",           // per-panel — quiet everything else\n    \"toggleChrome\": \"cmd+shift+t\" // per-panel — pin the chrome rail after hover leaves\n  },\n  \"panel\": {\n    \"sheet\": \"glass\",           // sheet template: \"glass\" | \"card\" | \"dotted\" | \"bracket\" | \"marginalia\"\n                                // per-note override: a `sheet:` frontmatter key in the note file\n    \"material\": \"hud\",          // glass material: \"hud\" | \"underWindow\" | \"popover\" | \"sidebar\" | \"menu\"\n    \"cornerRadius\": 12,\n    \"tintRead\": 0.28,           // 0–1 black tint over the glass in read mode (contrast floor)\n    \"tintEdit\": 0.38,           // 0–1 tint in edit mode (focused writing surface)\n    \"shadow\": true,             // window drop shadow\n    \"defaultWidth\": 420,        // size for panels opening for the first time\n    \"defaultHeight\": 340,\n    \"background\": null,         // optional CSS color; null keeps the glass sheet transparent\n    \"mark\": null,               // optional identity mark from <home>/attachments\n    \"chrome\": \"rail\"            // where the ✕ and mode toggle live:\n                                // \"rail\"   — a detached strip above the note, off the page.\n                                //            Shown on hover (or pinned with hotkeys.toggleChrome);\n                                //            the strip stays reachable across the seam. Idle notes\n                                //            stay bare. Edit mode does not force it.\n                                // \"inside\" — the original hover-earned chrome floating over\n                                //            the note's own top corners.\n  },\n  \"focus\": {\n    \"dim\": 0.30                 // 0–1 strength of the focus-mode veil over everything else\n  },\n  \"reader\": {                   // the reader layer (hotkeys.reader)\n    \"width\": 760,               // reading column width in points, capped at 80% of the screen\n    \"fontSize\": 17,             // starting size; ⌘+ / ⌘− adjust, ⌘0 resets\n    \"dim\": 0.5                  // 0–1 veil over the screen behind the sheet\n  },\n  \"drape\": {                    // a backdrop parked BEHIND every note — a calm stage under the set\n    \"enabled\": false,           // off by default; true parks a full-screen blur+dim behind the notes\n    \"dim\": 0.45,                // 0–1 black tint over the blurred backdrop\n    \"opacity\": 1.0,             // 0–1 overall presence; lower = a lighter veil the desktop shows through\n    \"material\": \"hud\"           // blur material: \"hud\" | \"underWindow\" | \"popover\" | \"sidebar\" | \"menu\"\n  },\n  \"motion\": {                   // Arrival: every show/hide is choreographed\n    \"entrance\": \"shimmer\",      // \"shimmer\" | \"drop\" | \"draw\" | \"none\" — how a note lands\n    \"durationMs\": 260,          // base duration for one panel's entrance\n    \"staggerMs\": 40,            // per-panel delay in group reveals (session restore, show all notes)\n    \"enabled\": true             // master switch — false = instant show/hide (today's behavior)\n  },\n  \"physics\": {                  // panel physics: how a note behaves under the hand\n    \"flingEnabled\": true,       // throw a panel by its top band — it glides on and bounces off screen edges\n    \"flingFriction\": 3.2,       // exponential glide friction (1/s); higher = heavier, stops sooner\n                                // (total glide distance ≈ release speed / flingFriction)\n    \"flingMinVelocity\": 900,    // release speed (pt/s) that starts a glide; below it the panel just stays\n    \"bounceDamping\": 0.6,       // 0–1 velocity kept after an edge bounce\n    \"shakeEnabled\": true        // shake side-to-side during a drag to fold the panel into its band (shade);\n                                // shake again — or double-click the band — to restore\n  },\n  \"editor\": {                   // typography & colors; the reader takes the typography, keeps its own colors\n    \"fontFamily\": null,         // null → system font stack; any CSS font-family string\n    \"monoFamily\": null,         // null → ui-monospace stack\n    \"titleFamily\": null,        // null → body stack; headings/titles only\n    \"fontSize\": 13,             // px\n    \"lineHeight\": 1.75,\n    \"paddingX\": 20,             // px\n    \"paddingY\": 16,\n    \"textColor\": null,          // any CSS color; null → rgba(255,255,255,0.85)\n    \"textStrongColor\": null,    // headings/bold; default rgba(255,255,255,0.96)\n    \"textMutedColor\": null,     // markers/list bullets; default rgba(255,255,255,0.45)\n    \"dimColor\": null,           // Markdown syntax markers\n    \"borderColor\": null,        // rules, quote borders, sheet frame\n    \"accentColor\": null,        // links; default rgba(158,203,255,0.9)\n    \"accentDimColor\": null,      // source-mode link targets\n    \"codeBackground\": null,     // default rgba(255,255,255,0.07)\n    \"codeTextColor\": null,       // inline + block code ink\n    \"caretColor\": null,         // default white\n    \"selectionColor\": null,     // default rgba(255,255,255,0.18)\n    \"h1Size\": null,             // px, reader scale; default 20 (editor derives slightly smaller)\n    \"h2Size\": null,             // default 17\n    \"h3Size\": null              // default 15\n  }\n}\n```\n\n### Mobile access\n\n`behavior.mobileAccess` controls the encrypted LAN peer host. Explicit `true`\nand `false` are preserved. For a config that predates this key, Canvas retains\naccess only when it can read a valid, nonempty Keychain approval list. Missing\nor malformed trust data and Keychain read failures leave access off. A custom\nhome never reads the production trust list and never starts the peer host.\n\nThis compatibility check does not rewrite an existing config or preferences.\nA later settings save writes the effective Boolean explicitly. An explicit\n`null` or non-Boolean value is invalid; it cannot inherit permission from\nstored approvals. Pairing approval and per-request authorization still apply.\n\n## How it applies\n\n- `panel.*` and `focus.*` are native (NSVisualEffectView material, tint layers,\n  window shadow, overlay dim) — applied immediately to all open panels.\n- `hotkeys.*` hot-apply too: global chords re-register with Carbon on change;\n  panel chords are read live on each keypress.\n- `behavior.launchAtLogin` syncs the macOS login item on change.\n- `motion.*` choreographs every show/hide (see **Motion (Arrival)** below);\n  applied live, so the next note you open — or the next Hyper+B — uses the new\n  feel. `enabled: false` restores the instant behavior exactly.\n- `physics.*` is read at each gesture, so edits apply to the very next drag —\n  see **Panel physics** below. macOS **Reduce Motion** disables the fling and\n  shake gestures regardless of these values.\n- `editor.*` maps to the web bundle's CSS custom properties\n  (`--blink-font-size`, `--blink-text`, …) and is pushed over the bridge via\n  `window.blink.setTheme`. The full variable table lives in\n  `web/editor/README.md`.\n\n## Reader layer\n\n`hotkeys.reader` lifts the text selected in the frontmost app — usually a long\nanswer in a terminal — into a centered reading column over a dimmed screen (`reader.dim`).\nEsc, ⌘W, a click outside the column, the hotkey again, or switching to another\napp closes it; ⌘S keeps the text as a new note (opened in read mode). ⌘+ / ⌘−\nchange the reading size and ⌘0 resets it to `reader.fontSize`.\n\n- Arrival is sequenced: a breathing cue lands beside the cursor the moment\n  the hotkey fires, then the screen dims and the sheet rises while the cue\n  grows at its centre, and the text streams in block by block once both the\n  sheet and editor are ready. Closing is a short fade. Reduced Motion skips\n  the rise and the stream. The sheet uses a fixed paper/ink palette (light or\n  dark with the app's appearance), not the note glass or `editor` colors.\n- Canvas becomes the active app while the reader is up, and the column is an\n  ordinary resizable window, so window-manager placement (Lattices tiling\n  shortcuts, Rectangle, and the like) moves and sizes it. The placed frame is\n  remembered and reused while it sits on the screen the reader opens on.\n  Closing returns focus to the app the text came from.\n- The selection comes from Accessibility (`AXSelectedText`) when the app\n  exposes it. Terminals mostly don't, so Canvas presses the app's own ⌘C menu\n  item through Accessibility (posting ⌘C once the hotkey's modifiers lift\n  only for apps without one), reads the pasteboard, and restores its previous\n  contents. If Copy leaves the pasteboard untouched — Ghostty with\n  `copy-on-select` already put the selection there — the current clipboard is\n  used. All of this needs Accessibility permission; until it is granted,\n  Canvas prompts and shows the current clipboard instead.\n- Terminal text is cleaned before rendering: ANSI escapes, TUI box frames, and\n  Claude Code's `⏺`/`⎿` markers are stripped, the common indent removed, and\n  lines the program hard-wrapped are rejoined. Box-drawn tables are kept as\n  monospace. Short lines, lists, and headings keep their breaks.\n- The text is not a note, so it renders under the editor's untrusted policy:\n  raw HTML shows as text, only web and mail links are clickable, and images\n  show their alt text.\n\n## Examples\n\nCozy serif reading, warmer accent, softer glass:\n\n```json\n{\n  \"editor\": {\n    \"fontFamily\": \"Charter, Georgia, serif\",\n    \"fontSize\": 14,\n    \"lineHeight\": 1.8,\n    \"accentColor\": \"rgba(255,196,150,0.9)\"\n  },\n  \"panel\": { \"tintRead\": 0.12, \"cornerRadius\": 16 }\n}\n```\n\nMaximum-contrast writing mode:\n\n```json\n{\n  \"panel\": { \"tintEdit\": 0.6 },\n  \"focus\": { \"dim\": 0.45 }\n}\n```\n\nReusable treatment with a restrained identity mark (a neutral house style —\nsubstitute your own colors, faces, and mark):\n\n```json\n{\n  \"styles\": {\n    \"terminal\": {\n      \"background\": \"#070908\",\n      \"text\": \"#f2f4ef\",\n      \"textStrong\": \"#f2f4ef\",\n      \"textMuted\": \"#aab1a7\",\n      \"dim\": \"#777e75\",\n      \"border\": \"rgba(239,244,237,.09)\",\n      \"accent\": \"#a6ef87\",\n      \"accentDim\": \"rgba(166,239,135,.58)\",\n      \"font\": \"\\\"JetBrains Mono\\\", ui-monospace, Menlo, monospace\",\n      \"mono\": \"\\\"JetBrains Mono\\\", ui-monospace, Menlo, monospace\",\n      \"titleFont\": \"\\\"JetBrains Mono\\\", ui-monospace, Menlo, monospace\",\n      \"codeBackground\": \"#111411\",\n      \"codeText\": \"#f2f4ef\",\n      \"caret\": \"#a6ef87\",\n      \"selection\": \"rgba(166,239,135,.1)\",\n      \"radius\": 6,\n      \"mark\": \"marks/terminal.svg\"\n    }\n  }\n}\n```\n\n`panel.mark`, `styles.<name>.mark`, and a workspace brand's `mark` are relative\npaths under `<home>/attachments`, using the root described above (default:\n`~/Library/Application Support/Blink/attachments`). The mark appears at 20px in a 24pt top-left chrome cell and yields\nthat position to the close control on hover. Marked themes receive a compact\ncontent gutter so the identity never overlaps the first heading or editor\nsource. Keeping it in the treatment instead of the markdown body preserves the\nnote as portable, presentation-free Markdown. Absolute paths and paths escaping\nthe attachments directory are ignored — `canvas workspace brand … --install-mark`\ncopies an asset into bounds for you.\n\nTreatments are partial overlays. In addition to the original\n`sheet`/`accent`/`font`/`fontSize`/`lineHeight`/`tint*`/`radius` fields, they\naccept `background`, `text`, `textStrong`, `textMuted`, `accentDim`, `mono`,\n`titleFont`, `dim`, `border`, `codeBackground`, `codeText`, `caret`, and\n`selection`. These map to the same editor and sheet variables as their global\nconfig counterparts, but apply only to notes that reference that named style.\n\n## Workspaces\n\nA **workspace** is a named group of notes plus the brand they render under. The\ndefinition lives here; membership lives in each note's `blink.workspace`\nfrontmatter key, so note markdown never carries a color, face, or asset path.\n\n```json\n{\n  \"workspaces\": {\n    \"acme-docs\": {\n      \"title\": \"Acme Documentation\",\n      \"style\": \"terminal\",\n      \"brand\": {\n        \"accent\": \"#7aa2f7\",\n        \"background\": \"#0b0d0c\",\n        \"radius\": 6,\n        \"mark\": \"marks/acme-docs.svg\"\n      }\n    }\n  }\n}\n```\n\n`style` names an entry in `styles` to use as a base; `brand` is a treatment\noverlaid on top of it, so several workspaces can share one house style and tint\nit differently. Both are optional — a workspace with neither is unbranded, and\nits notes render with Canvas's defaults.\n\nA note's look resolves least-specific first, so **later wins**:\n\n```\nconfig defaults ← workspaces[blink.workspace].brand ← styles[blink.style] ← loose blink: keys\n```\n\nUnknown workspace and style names contribute nothing rather than failing, so a\nnote whose workspace was forgotten still opens — just unbranded. Agents should\ndrive this through `canvas workspace` (see `docs/workspaces.md`) rather than\nhand-editing, because the CLI writes only the `workspaces` key and leaves every\nother config key byte-for-byte intact.\n\n## Motion (Arrival)\n\nNotes don't appear — they land. Every show/hide is choreographed, with the\ncharacter set by `motion.*` so a theme ships a matching feel. All of it no-ops\ncleanly when `motion.enabled` is `false`, and macOS **Reduce Motion**\n(System Settings → Accessibility → Display) is always honored as `\"none\"`.\n\nEntrances (`motion.entrance`):\n\n- **shimmer** — content fades up from nothing while a soft highlight sweep\n  crosses the sheet left→right.\n- **drop** — the panel drifts down ~8pt into place with a slight overshoot\n  settle as the content fades in.\n- **draw** — on flat sheets (`dotted`/`bracket`/`marginalia`) the frame draws\n  itself on, then the text fades in behind it. On `glass`/`card` (no frame to\n  draw) it falls back to **shimmer**.\n- **none** — instant (today's behavior).\n\nWhere the choreography shows up:\n\n- **Opening a note** (new note, popover, focusing) plays one entrance.\n- **Session restore** staggers the reopened panels `staggerMs` apart,\n  left-to-right by on-screen position, so the desk assembles.\n- **Show or hide all notes** (Hyper+B): the reveal staggers panels in from their screen-edge\n  direction; the hide is one synchronized exhale (all panels fade + drift\n  outward together, then vanish). The state flips instantly regardless — the\n  motion is garnish, and rapid toggles never leave a panel half-faded. Pending\n  saves and the open-notes list are untouched.\n- **Focus mode** recedes the non-key panels a hair (a subtle depth cue), so the\n  note you're writing stands proud. Transform-only — window positions never\n  move.\n\n## Panel physics\n\nNotes aren't just placed — they can be thrown. `physics.*` governs the two\ngestures on a panel's top band (the invisible 24pt drag strip along its top\nedge):\n\n- **Fling.** Drag a panel and release it with speed: it glides on with\n  momentum, decays exponentially (`flingFriction`), and bounces off the edges\n  of the screen it's mostly on, keeping `bounceDamping` of its speed per hit.\n  Below `flingMinVelocity` a release is just a drop. Any new grab, close, or\n  programmatic placement kills a glide mid-flight.\n- **Shake-to-shade.** Shake the panel side-to-side mid-drag (~3 reversals in\n  0.6s, ≥40pt each, little vertical travel) and it folds up into its top band,\n  windowshade-style. Shake again — or double-click the band — and it unfolds\n  back to its full frame. Resizing is pinned while shaded so nothing fights\n  the fold.\n\nGeometry is persisted only when a panel is at rest — never mid-glide — and\nalways as the UNSHADED frame, so a note that was shaded when Canvas quit still\nrelaunches full-size. The shaded state itself is session-only.\n\n## What does NOT live here\n\n- Notes themselves: `~/Library/Application Support/Blink/Notes/*.md` (frontmattered\n  markdown). Agents may write their own frontmatter keys into a note — Canvas preserves\n  unknown keys verbatim through every save, and merges on-disk metadata (tags, pinned,\n  foreign keys) before each content save, so editing a note's frontmatter while it's\n  open in a panel is safe.\n- Per-machine workspace state (open panels, per-note modes, window frames):\n  UserDefaults today, with a planned migration to `.blink/workspace.json`.\n\n### Companion menu icon\n\n`behavior.alwaysShowMenuBarIcon` defaults to `false`: Canvas hides its menu icon while Lattices is running and restores it when Lattices exits. Set it to `true` to keep a separate Canvas icon. This setting hot-applies and is also available in Settings. Reopening Canvas from Finder or Lattices Apps opens Settings even when the icon is hidden. It does not change notes or launch-at-login behavior.\n",
      "content": "# Canvas configuration\n\nCanvas's behavior and theme live in one JSON file:\n\n```\n~/Library/Application Support/Blink/config.json\n```\n\nCanvas watches this file and applies valid changes to open panels without a\nrestart. The default path is unchanged from Blink.\n\nCurrent source builds use the first nonempty `CANVAS_HOME`, then `BLINK_HOME`,\nto override the file-backed root. Empty values are ignored. The selected root\ncontains `config.json`, `Notes`, and `attachments`; use the same root for app and\nCLI. These overrides do not redirect `UserDefaults`, and either disables mobile\nsharing. Existing releases recognize `BLINK_HOME` only.\n\nExamples use the current `canvas` command. The compatible `blink` command and\npublished package name `@arach/blink` remain supported. The rename does not\nchange config keys, `blink.*` frontmatter, attachment URLs, or settings keys;\n`hotkeys.blink` still controls showing and hiding all notes.\n\nRules:\n- Every field is optional. Missing fields use the defaults below. `{}` is a valid config.\n- Invalid JSON never breaks the app: Canvas keeps the last good config and logs\n  `[BLINK] config invalid — keeping last good`. An invalid file found at startup\n  stays untouched; the app uses defaults with mobile access off.\n- Write atomically (write temp + rename, or your editor's normal save) — Canvas\n  watches the directory, so atomic replaces are picked up fine.\n- The settings window is a *view* over this file; edits there round-trip through it.\n\n## Schema\n\n```jsonc\n{\n  \"appearance\": \"auto\",         // app-wide light/dark: \"auto\" (follow macOS) | \"light\" | \"dark\"\n                                // \"auto\" tracks the system live; menubar → Appearance overrides it\n  \"behavior\": {\n    \"mobileAccess\": false,     // new installs default off; see upgrade policy below\n    \"restoreSession\": true,     // reopen last session's panels at launch\n    \"defaultMode\": \"read\",      // \"read\" | \"edit\" — mode for notes with no remembered mode\n    \"launchAtLogin\": false      // register Canvas as a login item (SMAppService)\n  },\n  \"hotkeys\": {\n    // Chord strings: modifiers joined by \"+\", ending in one key.\n    // Modifiers: hyper (⌃⌥⇧⌘), cmd, ctrl, alt, shift.\n    // Keys: a–z, 0–9, punctuation (. , / ; ' [ ] \\ - = `), space, return,\n    // tab, escape, delete. An invalid chord is logged and the previous\n    // binding kept — a bad edit never leaves the app unreachable.\n    \"newNote\": \"hyper+n\",       // global — create a note from anywhere\n    \"blink\": \"hyper+b\",         // global — show all notes / hide all\n    \"grid\": \"hyper+c\",          // global — grid/constellation overlay (\"c\" — hyper+g collides with Lattices)\n    \"reader\": \"hyper+e\",        // global — read the frontmost app's selection in the reader layer\n    \"toggleMode\": \"cmd+shift+p\",// per-panel — flip read/edit\n    \"focus\": \"cmd+.\",           // per-panel — quiet everything else\n    \"toggleChrome\": \"cmd+shift+t\" // per-panel — pin the chrome rail after hover leaves\n  },\n  \"panel\": {\n    \"sheet\": \"glass\",           // sheet template: \"glass\" | \"card\" | \"dotted\" | \"bracket\" | \"marginalia\"\n                                // per-note override: a `sheet:` frontmatter key in the note file\n    \"material\": \"hud\",          // glass material: \"hud\" | \"underWindow\" | \"popover\" | \"sidebar\" | \"menu\"\n    \"cornerRadius\": 12,\n    \"tintRead\": 0.28,           // 0–1 black tint over the glass in read mode (contrast floor)\n    \"tintEdit\": 0.38,           // 0–1 tint in edit mode (focused writing surface)\n    \"shadow\": true,             // window drop shadow\n    \"defaultWidth\": 420,        // size for panels opening for the first time\n    \"defaultHeight\": 340,\n    \"background\": null,         // optional CSS color; null keeps the glass sheet transparent\n    \"mark\": null,               // optional identity mark from <home>/attachments\n    \"chrome\": \"rail\"            // where the ✕ and mode toggle live:\n                                // \"rail\"   — a detached strip above the note, off the page.\n                                //            Shown on hover (or pinned with hotkeys.toggleChrome);\n                                //            the strip stays reachable across the seam. Idle notes\n                                //            stay bare. Edit mode does not force it.\n                                // \"inside\" — the original hover-earned chrome floating over\n                                //            the note's own top corners.\n  },\n  \"focus\": {\n    \"dim\": 0.30                 // 0–1 strength of the focus-mode veil over everything else\n  },\n  \"reader\": {                   // the reader layer (hotkeys.reader)\n    \"width\": 760,               // reading column width in points, capped at 80% of the screen\n    \"fontSize\": 17,             // starting size; ⌘+ / ⌘− adjust, ⌘0 resets\n    \"dim\": 0.5                  // 0–1 veil over the screen behind the sheet\n  },\n  \"drape\": {                    // a backdrop parked BEHIND every note — a calm stage under the set\n    \"enabled\": false,           // off by default; true parks a full-screen blur+dim behind the notes\n    \"dim\": 0.45,                // 0–1 black tint over the blurred backdrop\n    \"opacity\": 1.0,             // 0–1 overall presence; lower = a lighter veil the desktop shows through\n    \"material\": \"hud\"           // blur material: \"hud\" | \"underWindow\" | \"popover\" | \"sidebar\" | \"menu\"\n  },\n  \"motion\": {                   // Arrival: every show/hide is choreographed\n    \"entrance\": \"shimmer\",      // \"shimmer\" | \"drop\" | \"draw\" | \"none\" — how a note lands\n    \"durationMs\": 260,          // base duration for one panel's entrance\n    \"staggerMs\": 40,            // per-panel delay in group reveals (session restore, show all notes)\n    \"enabled\": true             // master switch — false = instant show/hide (today's behavior)\n  },\n  \"physics\": {                  // panel physics: how a note behaves under the hand\n    \"flingEnabled\": true,       // throw a panel by its top band — it glides on and bounces off screen edges\n    \"flingFriction\": 3.2,       // exponential glide friction (1/s); higher = heavier, stops sooner\n                                // (total glide distance ≈ release speed / flingFriction)\n    \"flingMinVelocity\": 900,    // release speed (pt/s) that starts a glide; below it the panel just stays\n    \"bounceDamping\": 0.6,       // 0–1 velocity kept after an edge bounce\n    \"shakeEnabled\": true        // shake side-to-side during a drag to fold the panel into its band (shade);\n                                // shake again — or double-click the band — to restore\n  },\n  \"editor\": {                   // typography & colors; the reader takes the typography, keeps its own colors\n    \"fontFamily\": null,         // null → system font stack; any CSS font-family string\n    \"monoFamily\": null,         // null → ui-monospace stack\n    \"titleFamily\": null,        // null → body stack; headings/titles only\n    \"fontSize\": 13,             // px\n    \"lineHeight\": 1.75,\n    \"paddingX\": 20,             // px\n    \"paddingY\": 16,\n    \"textColor\": null,          // any CSS color; null → rgba(255,255,255,0.85)\n    \"textStrongColor\": null,    // headings/bold; default rgba(255,255,255,0.96)\n    \"textMutedColor\": null,     // markers/list bullets; default rgba(255,255,255,0.45)\n    \"dimColor\": null,           // Markdown syntax markers\n    \"borderColor\": null,        // rules, quote borders, sheet frame\n    \"accentColor\": null,        // links; default rgba(158,203,255,0.9)\n    \"accentDimColor\": null,      // source-mode link targets\n    \"codeBackground\": null,     // default rgba(255,255,255,0.07)\n    \"codeTextColor\": null,       // inline + block code ink\n    \"caretColor\": null,         // default white\n    \"selectionColor\": null,     // default rgba(255,255,255,0.18)\n    \"h1Size\": null,             // px, reader scale; default 20 (editor derives slightly smaller)\n    \"h2Size\": null,             // default 17\n    \"h3Size\": null              // default 15\n  }\n}\n```\n\n### Mobile access\n\n`behavior.mobileAccess` controls the encrypted LAN peer host. Explicit `true`\nand `false` are preserved. For a config that predates this key, Canvas retains\naccess only when it can read a valid, nonempty Keychain approval list. Missing\nor malformed trust data and Keychain read failures leave access off. A custom\nhome never reads the production trust list and never starts the peer host.\n\nThis compatibility check does not rewrite an existing config or preferences.\nA later settings save writes the effective Boolean explicitly. An explicit\n`null` or non-Boolean value is invalid; it cannot inherit permission from\nstored approvals. Pairing approval and per-request authorization still apply.\n\n## How it applies\n\n- `panel.*` and `focus.*` are native (NSVisualEffectView material, tint layers,\n  window shadow, overlay dim) — applied immediately to all open panels.\n- `hotkeys.*` hot-apply too: global chords re-register with Carbon on change;\n  panel chords are read live on each keypress.\n- `behavior.launchAtLogin` syncs the macOS login item on change.\n- `motion.*` choreographs every show/hide (see **Motion (Arrival)** below);\n  applied live, so the next note you open — or the next Hyper+B — uses the new\n  feel. `enabled: false` restores the instant behavior exactly.\n- `physics.*` is read at each gesture, so edits apply to the very next drag —\n  see **Panel physics** below. macOS **Reduce Motion** disables the fling and\n  shake gestures regardless of these values.\n- `editor.*` maps to the web bundle's CSS custom properties\n  (`--blink-font-size`, `--blink-text`, …) and is pushed over the bridge via\n  `window.blink.setTheme`. The full variable table lives in\n  `web/editor/README.md`.\n\n## Reader layer\n\n`hotkeys.reader` lifts the text selected in the frontmost app — usually a long\nanswer in a terminal — into a centered reading column over a dimmed screen (`reader.dim`).\nEsc, ⌘W, a click outside the column, the hotkey again, or switching to another\napp closes it; ⌘S keeps the text as a new note (opened in read mode). ⌘+ / ⌘−\nchange the reading size and ⌘0 resets it to `reader.fontSize`.\n\n- Arrival is sequenced: a breathing cue lands beside the cursor the moment\n  the hotkey fires, then the screen dims and the sheet rises while the cue\n  grows at its centre, and the text streams in block by block once both the\n  sheet and editor are ready. Closing is a short fade. Reduced Motion skips\n  the rise and the stream. The sheet uses a fixed paper/ink palette (light or\n  dark with the app's appearance), not the note glass or `editor` colors.\n- Canvas becomes the active app while the reader is up, and the column is an\n  ordinary resizable window, so window-manager placement (Lattices tiling\n  shortcuts, Rectangle, and the like) moves and sizes it. The placed frame is\n  remembered and reused while it sits on the screen the reader opens on.\n  Closing returns focus to the app the text came from.\n- The selection comes from Accessibility (`AXSelectedText`) when the app\n  exposes it. Terminals mostly don't, so Canvas presses the app's own ⌘C menu\n  item through Accessibility (posting ⌘C once the hotkey's modifiers lift\n  only for apps without one), reads the pasteboard, and restores its previous\n  contents. If Copy leaves the pasteboard untouched — Ghostty with\n  `copy-on-select` already put the selection there — the current clipboard is\n  used. All of this needs Accessibility permission; until it is granted,\n  Canvas prompts and shows the current clipboard instead.\n- Terminal text is cleaned before rendering: ANSI escapes, TUI box frames, and\n  Claude Code's `⏺`/`⎿` markers are stripped, the common indent removed, and\n  lines the program hard-wrapped are rejoined. Box-drawn tables are kept as\n  monospace. Short lines, lists, and headings keep their breaks.\n- The text is not a note, so it renders under the editor's untrusted policy:\n  raw HTML shows as text, only web and mail links are clickable, and images\n  show their alt text.\n\n## Examples\n\nCozy serif reading, warmer accent, softer glass:\n\n```json\n{\n  \"editor\": {\n    \"fontFamily\": \"Charter, Georgia, serif\",\n    \"fontSize\": 14,\n    \"lineHeight\": 1.8,\n    \"accentColor\": \"rgba(255,196,150,0.9)\"\n  },\n  \"panel\": { \"tintRead\": 0.12, \"cornerRadius\": 16 }\n}\n```\n\nMaximum-contrast writing mode:\n\n```json\n{\n  \"panel\": { \"tintEdit\": 0.6 },\n  \"focus\": { \"dim\": 0.45 }\n}\n```\n\nReusable treatment with a restrained identity mark (a neutral house style —\nsubstitute your own colors, faces, and mark):\n\n```json\n{\n  \"styles\": {\n    \"terminal\": {\n      \"background\": \"#070908\",\n      \"text\": \"#f2f4ef\",\n      \"textStrong\": \"#f2f4ef\",\n      \"textMuted\": \"#aab1a7\",\n      \"dim\": \"#777e75\",\n      \"border\": \"rgba(239,244,237,.09)\",\n      \"accent\": \"#a6ef87\",\n      \"accentDim\": \"rgba(166,239,135,.58)\",\n      \"font\": \"\\\"JetBrains Mono\\\", ui-monospace, Menlo, monospace\",\n      \"mono\": \"\\\"JetBrains Mono\\\", ui-monospace, Menlo, monospace\",\n      \"titleFont\": \"\\\"JetBrains Mono\\\", ui-monospace, Menlo, monospace\",\n      \"codeBackground\": \"#111411\",\n      \"codeText\": \"#f2f4ef\",\n      \"caret\": \"#a6ef87\",\n      \"selection\": \"rgba(166,239,135,.1)\",\n      \"radius\": 6,\n      \"mark\": \"marks/terminal.svg\"\n    }\n  }\n}\n```\n\n`panel.mark`, `styles.<name>.mark`, and a workspace brand's `mark` are relative\npaths under `<home>/attachments`, using the root described above (default:\n`~/Library/Application Support/Blink/attachments`). The mark appears at 20px in a 24pt top-left chrome cell and yields\nthat position to the close control on hover. Marked themes receive a compact\ncontent gutter so the identity never overlaps the first heading or editor\nsource. Keeping it in the treatment instead of the markdown body preserves the\nnote as portable, presentation-free Markdown. Absolute paths and paths escaping\nthe attachments directory are ignored — `canvas workspace brand … --install-mark`\ncopies an asset into bounds for you.\n\nTreatments are partial overlays. In addition to the original\n`sheet`/`accent`/`font`/`fontSize`/`lineHeight`/`tint*`/`radius` fields, they\naccept `background`, `text`, `textStrong`, `textMuted`, `accentDim`, `mono`,\n`titleFont`, `dim`, `border`, `codeBackground`, `codeText`, `caret`, and\n`selection`. These map to the same editor and sheet variables as their global\nconfig counterparts, but apply only to notes that reference that named style.\n\n## Workspaces\n\nA **workspace** is a named group of notes plus the brand they render under. The\ndefinition lives here; membership lives in each note's `blink.workspace`\nfrontmatter key, so note markdown never carries a color, face, or asset path.\n\n```json\n{\n  \"workspaces\": {\n    \"acme-docs\": {\n      \"title\": \"Acme Documentation\",\n      \"style\": \"terminal\",\n      \"brand\": {\n        \"accent\": \"#7aa2f7\",\n        \"background\": \"#0b0d0c\",\n        \"radius\": 6,\n        \"mark\": \"marks/acme-docs.svg\"\n      }\n    }\n  }\n}\n```\n\n`style` names an entry in `styles` to use as a base; `brand` is a treatment\noverlaid on top of it, so several workspaces can share one house style and tint\nit differently. Both are optional — a workspace with neither is unbranded, and\nits notes render with Canvas's defaults.\n\nA note's look resolves least-specific first, so **later wins**:\n\n```\nconfig defaults ← workspaces[blink.workspace].brand ← styles[blink.style] ← loose blink: keys\n```\n\nUnknown workspace and style names contribute nothing rather than failing, so a\nnote whose workspace was forgotten still opens — just unbranded. Agents should\ndrive this through `canvas workspace` (see `docs/workspaces.md`) rather than\nhand-editing, because the CLI writes only the `workspaces` key and leaves every\nother config key byte-for-byte intact.\n\n## Motion (Arrival)\n\nNotes don't appear — they land. Every show/hide is choreographed, with the\ncharacter set by `motion.*` so a theme ships a matching feel. All of it no-ops\ncleanly when `motion.enabled` is `false`, and macOS **Reduce Motion**\n(System Settings → Accessibility → Display) is always honored as `\"none\"`.\n\nEntrances (`motion.entrance`):\n\n- **shimmer** — content fades up from nothing while a soft highlight sweep\n  crosses the sheet left→right.\n- **drop** — the panel drifts down ~8pt into place with a slight overshoot\n  settle as the content fades in.\n- **draw** — on flat sheets (`dotted`/`bracket`/`marginalia`) the frame draws\n  itself on, then the text fades in behind it. On `glass`/`card` (no frame to\n  draw) it falls back to **shimmer**.\n- **none** — instant (today's behavior).\n\nWhere the choreography shows up:\n\n- **Opening a note** (new note, popover, focusing) plays one entrance.\n- **Session restore** staggers the reopened panels `staggerMs` apart,\n  left-to-right by on-screen position, so the desk assembles.\n- **Show or hide all notes** (Hyper+B): the reveal staggers panels in from their screen-edge\n  direction; the hide is one synchronized exhale (all panels fade + drift\n  outward together, then vanish). The state flips instantly regardless — the\n  motion is garnish, and rapid toggles never leave a panel half-faded. Pending\n  saves and the open-notes list are untouched.\n- **Focus mode** recedes the non-key panels a hair (a subtle depth cue), so the\n  note you're writing stands proud. Transform-only — window positions never\n  move.\n\n## Panel physics\n\nNotes aren't just placed — they can be thrown. `physics.*` governs the two\ngestures on a panel's top band (the invisible 24pt drag strip along its top\nedge):\n\n- **Fling.** Drag a panel and release it with speed: it glides on with\n  momentum, decays exponentially (`flingFriction`), and bounces off the edges\n  of the screen it's mostly on, keeping `bounceDamping` of its speed per hit.\n  Below `flingMinVelocity` a release is just a drop. Any new grab, close, or\n  programmatic placement kills a glide mid-flight.\n- **Shake-to-shade.** Shake the panel side-to-side mid-drag (~3 reversals in\n  0.6s, ≥40pt each, little vertical travel) and it folds up into its top band,\n  windowshade-style. Shake again — or double-click the band — and it unfolds\n  back to its full frame. Resizing is pinned while shaded so nothing fights\n  the fold.\n\nGeometry is persisted only when a panel is at rest — never mid-glide — and\nalways as the UNSHADED frame, so a note that was shaded when Canvas quit still\nrelaunches full-size. The shaded state itself is session-only.\n\n## What does NOT live here\n\n- Notes themselves: `~/Library/Application Support/Blink/Notes/*.md` (frontmattered\n  markdown). Agents may write their own frontmatter keys into a note — Canvas preserves\n  unknown keys verbatim through every save, and merges on-disk metadata (tags, pinned,\n  foreign keys) before each content save, so editing a note's frontmatter while it's\n  open in a panel is safe.\n- Per-machine workspace state (open panels, per-note modes, window frames):\n  UserDefaults today, with a planned migration to `.blink/workspace.json`.\n\n### Companion menu icon\n\n`behavior.alwaysShowMenuBarIcon` defaults to `false`: Canvas hides its menu icon while Lattices is running and restores it when Lattices exits. Set it to `true` to keep a separate Canvas icon. This setting hot-applies and is also available in Settings. Reopening Canvas from Finder or Lattices Apps opens Settings even when the icon is hidden. It does not change notes or launch-at-login behavior."
    },
    {
      "id": "blink:ios-sync",
      "slug": "ios-sync",
      "product": "blink",
      "kind": "doc",
      "catalogKind": "guide",
      "current": true,
      "title": "Canvas iOS snapshot contract",
      "description": "The first iOS surface is a read-only, offline-capable view of the same note files\nthe Mac app and canvas CLI use (blink remains compatible). It starts with\nLAN as its only enabled route,\nbut its transport boundary can la",
      "order": null,
      "sourcePath": "products/blink/docs/ios-sync.md",
      "url": "/canvas/docs/ios-sync",
      "rawUrl": "/canvas/docs/markdown/ios-sync.md",
      "markdownUrl": "/canvas/docs/ios-sync.md",
      "headings": [
        {
          "depth": 1,
          "text": "Canvas iOS snapshot contract",
          "anchor": "canvas-ios-snapshot-contract"
        },
        {
          "depth": 2,
          "text": "Scope",
          "anchor": "scope"
        },
        {
          "depth": 2,
          "text": "Ownership",
          "anchor": "ownership"
        },
        {
          "depth": 2,
          "text": "V1 snapshot",
          "anchor": "v1-snapshot"
        },
        {
          "depth": 2,
          "text": "Consistency",
          "anchor": "consistency"
        },
        {
          "depth": 2,
          "text": "Encrypted LAN transport",
          "anchor": "encrypted-lan-transport"
        },
        {
          "depth": 2,
          "text": "Building the companion",
          "anchor": "building-the-companion"
        }
      ],
      "tokensEstimate": 1703,
      "frontmatter": {},
      "markdown": "# Canvas iOS snapshot contract\n\n## Scope\n\nThe first iOS surface is a read-only, offline-capable view of the same note files\nthe Mac app and `canvas` CLI use (`blink` remains compatible). It starts with\nLAN as its only enabled route,\nbut its transport boundary can later use Hudson's Tailscale or managed-relay\nproviders without changing Canvas's replication model.\n\nThis describes the current source companion, whose display and product names\nare Canvas. The rename is not evidence of a published iOS release. The Xcode\nproject and target remain `BlinkMobile`, and the bundle ID stays\n`dev.arach.blink.mobile`. The Mac keeps `dev.arach.blink`. Existing Keychain\nservices, cache and preference keys, Bonjour service `blink-notes`\n(`_blink-notes._tcp`), and the `BlinkPeer` wire format are unchanged, so the\nrename alone does not invalidate paired devices or cached notes.\n\n## Ownership\n\n- Hudson owns generic peer identity/trust models, route providers, route\n  settings, cascade, and diagnostics.\n- Canvas owns snapshots, note revisions, tombstones, quarantine, workspace/style\n  projection, attachment policy, offline cache, iOS UI, and the first proven LAN\n  peer channel in `BlinkPeer`.\n- OpenScout donates generic route and connection behavior to Hudson while OSN\n  remains an OpenScout provider.\n\n## V1 snapshot\n\nThe Mac returns one complete `BlinkSnapshot`:\n\n- exact UTF-8 bytes of each frontmattered Markdown file;\n- a SHA-256 revision for each note;\n- a strong whole-snapshot ETag;\n- parsed summary fields for native list rendering;\n- durable tombstones for absent notes;\n- quarantine issues for unreadable, malformed, or identity-divergent files.\n\nEach quarantine issue names the expected filename identity. An iOS cache keeps\nits last known value for that identity until the file becomes readable again or\nan explicit tombstone arrives; quarantine is never interpreted as deletion.\n\nThe mobile app atomically replaces its cached snapshot on a successful changed\nresponse and retains it on an authenticated not-modified response. The cache is\natomically bound to the authenticated Mac agreement public key that produced\nit: connecting to a different key forces a full snapshot and never merges\nquarantined notes across hosts. The same record stores the latest successful\nsync time, including authenticated not-modified responses. Because the cache is\nreplaceable and contains note bodies, every atomic replacement is marked\nexcluded from device backup. **Remove Notes** disconnects any live or pending\npeer operation before deleting it, so an automatic refresh cannot recreate the\ncache after confirmation.\n\n`generatedAt` is diagnostic and is excluded from the ETag. A corpus that has not\nchanged therefore remains cacheable across requests and app launches.\n\n## Consistency\n\nV1 deliberately avoids a manifest-then-live-file race: note bodies travel in the\nsame snapshot response as their revisions. `BlinkSnapshotBuilder` requires two\nidentical consecutive directory reads before publishing. Atomic note writes\nprevent torn individual files; the repeated capture prevents a directory change\nfrom being presented as a stable snapshot.\n\nIf the corpus later becomes too large for full snapshots, the replacement must\nuse immutable snapshot leases keyed by generation. Fetching a manifest and then\nreading current live files by id is not an acceptable incremental design.\n\n## Encrypted LAN transport\n\nOn a new Mac install, enable **Mobile Access** in Canvas's right-click menu to\nstart pairing. The 2.3.0 upgrade preserves explicit `behavior.mobileAccess`\nchoices. If an older config has no such key, valid existing Keychain approvals\nkeep access enabled without rewriting the config. Missing or malformed trust\ndata and Keychain failures leave it off. This preserves already approved\ndevices without enabling sharing for a new installation.\n\nThe snapshot replication and cache layer depends only on `BlinkPeerTransport`.\nThe current mobile model selects its concrete LAN discovery client directly; a\nfuture route cascade requires a separate discovery/connection orchestrator but\ndoes not require a new snapshot format. The first implementation uses Bonjour\ndiscovery through Multipeer Connectivity with\n`MCEncryptionPreference.required`; there is no plaintext HTTP listener. Bonjour\nadvertises a stable Mac host identity and Curve25519 agreement public key. Every\napplication payload is then sealed end to end with a per-connection key using\nChaChaPoly, independently of the Multipeer transport encryption.\n\nThe Mac accepts a peer session but serves nothing until the mobile app requests\naccess inside that sealed channel. Each app installation keeps a private random\nseed in a non-migrating, ThisDeviceOnly Keychain item and derives a different\ncredential for each Mac public key; the reusable seed never leaves the device\nor travels in a backup. A short, non-secret seed fingerprint distinguishes\notherwise generic iPhone/iPad names in approval and revocation UI. Allowing the\nrequest stores only that host-scoped credential, so later connections from the\nsame installation do not repeat the prompt. A relay, lookalike host, or\nsame-network observer cannot decrypt the payload or reuse the credential.\nSame-network presence is discovery, never authorization.\n\nThe credential — not the Multipeer peer identity — is the authorization\nboundary. `MCPeerID` carries a client-chosen display name and an undocumented\nhash, so it is only bookkeeping for revoke-time disconnects. Every data-bearing\nrequest re-presents the host-scoped credential inside the sealed payload, and\nthe Mac re-checks it against the trust list per request.\n\nThe Mac keeps the approved-device list and its own agreement private key in the\nKeychain, matching what the companion does with its seed. Both are bearer\nsecrets, and the Mac app is unsandboxed, so a preferences plist would be readable\nby any process running as the user. The pre-Keychain `UserDefaults` values are\ndeleted rather than migrated, for the same reason the companion refuses to\nmigrate its own: a plist travels in backups and can clone an approved identity.\nDevices still using pre-Keychain credentials must be approved again; the\nproduct rename does not reset Keychain-backed approvals. If the Keychain\nrefuses a trusted-list write, pairing still works for that launch and **Mobile Access**\nsays approvals will not persist. If either platform cannot read or create its\nstable agreement/credential key, it does not advertise or discover with an\nephemeral replacement identity; the UI surfaces the Keychain failure instead.\n\nApproval prompts are rate limited. One prompt is outstanding process-wide, so\ntwo peers cannot stack nested modals, and a denial silences new prompts for a\nminute — an unapproved device that loops connect → request cannot drive\nrepeated focus-stealing alerts.\n\nApproved devices are listed under **Mobile Access** in Canvas's right-click menu.\nRevoking a device deletes its stored credential and disconnects its live session;\nthe device must be explicitly approved again before it can fetch another\nsnapshot. The offline snapshot already on the device remains available because it\nis a replaceable local cache, not continuing access to the Mac.\n\nThe wire format is versioned (`BlinkPeerWire.version`), and discovery filters on\nthe same value, so a Mac and a companion from different protocol generations\nnever pair.\n\n`BlinkPeer` is kept separate from the app UI so the encrypted peer channel can be\nproven here and later donated to Hudson without moving Canvas's snapshot model.\nOrdinary LAN HTTP with only a bearer token or request MAC remains prohibited\nbecause it can expose note contents to passive network observers.\n\nLAN is the only implemented route. Tailscale and hosted relays remain future\nproviders for a route orchestrator, not implemented fallback paths today.\n\nThe Mac peer host does not start with a nonempty `CANVAS_HOME` or `BLINK_HOME`.\nThe first nonempty `CANVAS_HOME`, then `BLINK_HOME`, selects the file-backed\nroot; empty values are ignored. Agent and test sandboxes therefore cannot\nadvertise their notes under the production Mac's persistent identity or\napproved-device trust list. The default Mac store remains\n`~/Library/Application Support/Blink`; older releases recognize `BLINK_HOME`\nonly.\n\n## Building the companion\n\nThe checked-in XcodeGen source lives at `apps/ios/project.yml`:\n\n```sh\ncd apps/ios\nxcodegen generate\nxcodebuild -project BlinkMobile.xcodeproj -scheme BlinkMobile \\\n  -destination 'generic/platform=iOS Simulator' build\n```\n\nThe generated project consumes the repository's local `BlinkCore` and\n`BlinkPeer` Swift package products. The fixture in `apps/ios/Fixtures` documents\nrepresentative offline list and detail states without pretending to be\nproduction data. The iOS target bundles `PrivacyInfo.xcprivacy`, declaring the\napp-only `UserDefaults` use for appearance preferences with Apple's `CA92.1`\nrequired reason.\n",
      "content": "# Canvas iOS snapshot contract\n\n## Scope\n\nThe first iOS surface is a read-only, offline-capable view of the same note files\nthe Mac app and `canvas` CLI use (`blink` remains compatible). It starts with\nLAN as its only enabled route,\nbut its transport boundary can later use Hudson's Tailscale or managed-relay\nproviders without changing Canvas's replication model.\n\nThis describes the current source companion, whose display and product names\nare Canvas. The rename is not evidence of a published iOS release. The Xcode\nproject and target remain `BlinkMobile`, and the bundle ID stays\n`dev.arach.blink.mobile`. The Mac keeps `dev.arach.blink`. Existing Keychain\nservices, cache and preference keys, Bonjour service `blink-notes`\n(`_blink-notes._tcp`), and the `BlinkPeer` wire format are unchanged, so the\nrename alone does not invalidate paired devices or cached notes.\n\n## Ownership\n\n- Hudson owns generic peer identity/trust models, route providers, route\n  settings, cascade, and diagnostics.\n- Canvas owns snapshots, note revisions, tombstones, quarantine, workspace/style\n  projection, attachment policy, offline cache, iOS UI, and the first proven LAN\n  peer channel in `BlinkPeer`.\n- OpenScout donates generic route and connection behavior to Hudson while OSN\n  remains an OpenScout provider.\n\n## V1 snapshot\n\nThe Mac returns one complete `BlinkSnapshot`:\n\n- exact UTF-8 bytes of each frontmattered Markdown file;\n- a SHA-256 revision for each note;\n- a strong whole-snapshot ETag;\n- parsed summary fields for native list rendering;\n- durable tombstones for absent notes;\n- quarantine issues for unreadable, malformed, or identity-divergent files.\n\nEach quarantine issue names the expected filename identity. An iOS cache keeps\nits last known value for that identity until the file becomes readable again or\nan explicit tombstone arrives; quarantine is never interpreted as deletion.\n\nThe mobile app atomically replaces its cached snapshot on a successful changed\nresponse and retains it on an authenticated not-modified response. The cache is\natomically bound to the authenticated Mac agreement public key that produced\nit: connecting to a different key forces a full snapshot and never merges\nquarantined notes across hosts. The same record stores the latest successful\nsync time, including authenticated not-modified responses. Because the cache is\nreplaceable and contains note bodies, every atomic replacement is marked\nexcluded from device backup. **Remove Notes** disconnects any live or pending\npeer operation before deleting it, so an automatic refresh cannot recreate the\ncache after confirmation.\n\n`generatedAt` is diagnostic and is excluded from the ETag. A corpus that has not\nchanged therefore remains cacheable across requests and app launches.\n\n## Consistency\n\nV1 deliberately avoids a manifest-then-live-file race: note bodies travel in the\nsame snapshot response as their revisions. `BlinkSnapshotBuilder` requires two\nidentical consecutive directory reads before publishing. Atomic note writes\nprevent torn individual files; the repeated capture prevents a directory change\nfrom being presented as a stable snapshot.\n\nIf the corpus later becomes too large for full snapshots, the replacement must\nuse immutable snapshot leases keyed by generation. Fetching a manifest and then\nreading current live files by id is not an acceptable incremental design.\n\n## Encrypted LAN transport\n\nOn a new Mac install, enable **Mobile Access** in Canvas's right-click menu to\nstart pairing. The 2.3.0 upgrade preserves explicit `behavior.mobileAccess`\nchoices. If an older config has no such key, valid existing Keychain approvals\nkeep access enabled without rewriting the config. Missing or malformed trust\ndata and Keychain failures leave it off. This preserves already approved\ndevices without enabling sharing for a new installation.\n\nThe snapshot replication and cache layer depends only on `BlinkPeerTransport`.\nThe current mobile model selects its concrete LAN discovery client directly; a\nfuture route cascade requires a separate discovery/connection orchestrator but\ndoes not require a new snapshot format. The first implementation uses Bonjour\ndiscovery through Multipeer Connectivity with\n`MCEncryptionPreference.required`; there is no plaintext HTTP listener. Bonjour\nadvertises a stable Mac host identity and Curve25519 agreement public key. Every\napplication payload is then sealed end to end with a per-connection key using\nChaChaPoly, independently of the Multipeer transport encryption.\n\nThe Mac accepts a peer session but serves nothing until the mobile app requests\naccess inside that sealed channel. Each app installation keeps a private random\nseed in a non-migrating, ThisDeviceOnly Keychain item and derives a different\ncredential for each Mac public key; the reusable seed never leaves the device\nor travels in a backup. A short, non-secret seed fingerprint distinguishes\notherwise generic iPhone/iPad names in approval and revocation UI. Allowing the\nrequest stores only that host-scoped credential, so later connections from the\nsame installation do not repeat the prompt. A relay, lookalike host, or\nsame-network observer cannot decrypt the payload or reuse the credential.\nSame-network presence is discovery, never authorization.\n\nThe credential — not the Multipeer peer identity — is the authorization\nboundary. `MCPeerID` carries a client-chosen display name and an undocumented\nhash, so it is only bookkeeping for revoke-time disconnects. Every data-bearing\nrequest re-presents the host-scoped credential inside the sealed payload, and\nthe Mac re-checks it against the trust list per request.\n\nThe Mac keeps the approved-device list and its own agreement private key in the\nKeychain, matching what the companion does with its seed. Both are bearer\nsecrets, and the Mac app is unsandboxed, so a preferences plist would be readable\nby any process running as the user. The pre-Keychain `UserDefaults` values are\ndeleted rather than migrated, for the same reason the companion refuses to\nmigrate its own: a plist travels in backups and can clone an approved identity.\nDevices still using pre-Keychain credentials must be approved again; the\nproduct rename does not reset Keychain-backed approvals. If the Keychain\nrefuses a trusted-list write, pairing still works for that launch and **Mobile Access**\nsays approvals will not persist. If either platform cannot read or create its\nstable agreement/credential key, it does not advertise or discover with an\nephemeral replacement identity; the UI surfaces the Keychain failure instead.\n\nApproval prompts are rate limited. One prompt is outstanding process-wide, so\ntwo peers cannot stack nested modals, and a denial silences new prompts for a\nminute — an unapproved device that loops connect → request cannot drive\nrepeated focus-stealing alerts.\n\nApproved devices are listed under **Mobile Access** in Canvas's right-click menu.\nRevoking a device deletes its stored credential and disconnects its live session;\nthe device must be explicitly approved again before it can fetch another\nsnapshot. The offline snapshot already on the device remains available because it\nis a replaceable local cache, not continuing access to the Mac.\n\nThe wire format is versioned (`BlinkPeerWire.version`), and discovery filters on\nthe same value, so a Mac and a companion from different protocol generations\nnever pair.\n\n`BlinkPeer` is kept separate from the app UI so the encrypted peer channel can be\nproven here and later donated to Hudson without moving Canvas's snapshot model.\nOrdinary LAN HTTP with only a bearer token or request MAC remains prohibited\nbecause it can expose note contents to passive network observers.\n\nLAN is the only implemented route. Tailscale and hosted relays remain future\nproviders for a route orchestrator, not implemented fallback paths today.\n\nThe Mac peer host does not start with a nonempty `CANVAS_HOME` or `BLINK_HOME`.\nThe first nonempty `CANVAS_HOME`, then `BLINK_HOME`, selects the file-backed\nroot; empty values are ignored. Agent and test sandboxes therefore cannot\nadvertise their notes under the production Mac's persistent identity or\napproved-device trust list. The default Mac store remains\n`~/Library/Application Support/Blink`; older releases recognize `BLINK_HOME`\nonly.\n\n## Building the companion\n\nThe checked-in XcodeGen source lives at `apps/ios/project.yml`:\n\n```sh\ncd apps/ios\nxcodegen generate\nxcodebuild -project BlinkMobile.xcodeproj -scheme BlinkMobile \\\n  -destination 'generic/platform=iOS Simulator' build\n```\n\nThe generated project consumes the repository's local `BlinkCore` and\n`BlinkPeer` Swift package products. The fixture in `apps/ios/Fixtures` documents\nrepresentative offline list and detail states without pretending to be\nproduction data. The iOS target bundles `PrivacyInfo.xcprivacy`, declaring the\napp-only `UserDefaults` use for appearance preferences with Apple's `CA92.1`\nrequired reason."
    },
    {
      "id": "blink:agents",
      "slug": "agents",
      "product": "blink",
      "kind": "doc",
      "catalogKind": "guide",
      "current": true,
      "title": "Canvas — instructions for agents",
      "description": "Canvas is a native macOS spatial note-taking app. It is designed to be operated by humans and agents through the same local files.",
      "order": null,
      "sourcePath": "products/blink/landing/public/agents.md",
      "url": "/canvas/docs/agents",
      "rawUrl": "/canvas/docs/markdown/agents.md",
      "markdownUrl": "/canvas/docs/agents/index.md",
      "headings": [
        {
          "depth": 1,
          "text": "Canvas — instructions for agents",
          "anchor": "canvas-instructions-for-agents"
        },
        {
          "depth": 2,
          "text": "Choose the right surface",
          "anchor": "choose-the-right-surface"
        },
        {
          "depth": 2,
          "text": "First check",
          "anchor": "first-check"
        },
        {
          "depth": 2,
          "text": "Requirements and install",
          "anchor": "requirements-and-install"
        },
        {
          "depth": 2,
          "text": "File layout",
          "anchor": "file-layout"
        },
        {
          "depth": 2,
          "text": "Note model",
          "anchor": "note-model"
        },
        {
          "depth": 2,
          "text": "Core commands",
          "anchor": "core-commands"
        },
        {
          "depth": 3,
          "text": "Choose the correct write verb",
          "anchor": "choose-the-correct-write-verb"
        },
        {
          "depth": 2,
          "text": "Common workflows",
          "anchor": "common-workflows"
        },
        {
          "depth": 2,
          "text": "Configuration",
          "anchor": "configuration"
        },
        {
          "depth": 2,
          "text": "Safety rules",
          "anchor": "safety-rules"
        },
        {
          "depth": 2,
          "text": "Runtime behavior",
          "anchor": "runtime-behavior"
        },
        {
          "depth": 2,
          "text": "Canonical references",
          "anchor": "canonical-references"
        }
      ],
      "tokensEstimate": 1640,
      "frontmatter": {},
      "markdown": "# Canvas — instructions for agents\n\nCanvas is a native macOS spatial note-taking app. It is designed to be operated by humans and agents through the same local files.\n\nCanvas is the new name for Blink. The 2.3.0 canonical release installs\n`Canvas.app` and provides the `canvas` CLI. The package remains `@arach/blink`;\n`blink` is a compatible command alias. Legacy download channels still use\n`Blink.app`. Check the [release contract](https://lattices.dev/canvas/docs/release)\nfor the selected channel. Update the app and CLI together.\n\nKeep existing file paths, `blink.*` frontmatter, and protocol identifiers\nunchanged. The gateway identifies this product as `blink`, and `system.hello`\nreports `\"app\": \"Blink\"` with `\"displayName\": \"Canvas\"`.\n\nCanvas remains an independent app. The family commands `lats canvas` and\n`lats blink` forward to Canvas without sharing the Lattices app lifecycle.\n\n**The note is the window, and the desktop is the workspace**. A note is one floating panel in the running app and one Markdown file on disk. Opening an already-open note focuses its existing panel rather than creating a duplicate.\n\n## Choose the right surface\n\n- Operating Canvas: use the `canvas` CLI for notes and `config.json` for behavior and appearance. Use `blink` on an older installation.\n- Reading or writing content: prefer CLI commands; plain file access is the lower-level escape hatch.\n- Verifying live appearance or placement: use the GUI only when the task requires the rendered visual surface.\n- Developing Canvas itself: read the repository's `AGENTS.md` before changing code.\n\nThe running app also serves a local Unix socket for a limited set of live panel, note, and desk operations, plus newline-delimited events. See the [implemented socket API](https://lattices.dev/canvas/docs/api) for the supported methods. The broader agent API proposal and an MCP server are not implemented. Use the CLI and filesystem for ordinary note edits.\n\n## First check\n\n```sh\ncanvas ls --json\n```\n\nExpected result: a JSON list of notes. The command reads files only. It does\nnot open panels. For automation that must not touch the user's notes, set\n`CANVAS_HOME` to a scratch directory first:\n\n```sh\nexport CANVAS_HOME=\"$(mktemp -d)\"\ncanvas present hello $'# Hello\\n\\nFirst note from an agent.' --json\ncanvas cat hello\n```\n\n`present` creates the note `hello` and returns it as JSON. `cat` prints the\nMarkdown. The note lives only in the scratch directory. A Canvas app that runs\nwith the default home does not show it. Without a home override, the same\ncommands write to the user's notes, and a running app opens the note as a\npanel.\n\n## Requirements and install\n\n- macOS 14 or newer\n- Apple Silicon\n- Node.js 18 or newer for the npm shims\n\n```sh\nnpm install -g @arach/blink\ncanvas app install\ncanvas app open\n```\n\nThe npm package includes the signed native CLI. `canvas app install` fetches\nand validates the signed, notarized release before installing the app in\n`/Applications`. The installer prefers the canonical Canvas channel when\nversions tie; legacy releases retain `Blink.app`.\n\nCheck the installed CLI when version-sensitive behavior matters:\n\n```sh\ncanvas --version\nnpm view @arach/blink version\n```\n\n## File layout\n\nDefault file-backed root:\n\n```text\n~/Library/Application Support/Blink/\n├── Notes/\n│   └── <id>.md\n├── config.json\n└── attachments/\n```\n\nThe first nonempty `CANVAS_HOME`, then `BLINK_HOME`, overrides the file-backed\nroot for notes, config, attachments, ledger, and socket. Empty values are\nignored. Use the same override for the app and CLI for isolated tests and\nautomation. Older releases recognize `BLINK_HOME` only.\n\nThese overrides do not redirect device-local `UserDefaults`. Exact panel\nframes, the open-panel set, and per-note read/edit mode remain Mac-local state.\nThe Mac app disables mobile sharing when either home override is active.\n\n## Note model\n\n- One Markdown file is one note.\n- The filename stem and frontmatter `id` are a unique title-derived slug.\n- The title is derived from the first meaningful content line.\n- Note metadata and portable presentation live in YAML frontmatter.\n- Unknown or foreign frontmatter must survive round trips.\n- Portable placement intent such as `blink.slot` lives in frontmatter.\n- Exact pixel geometry is device-local and must never be substituted for portable placement intent.\n\nExample:\n\n```md\n---\nid: q3-plan\nblink:\n  style: focus\n  accent: \"#7dd3fc\"\n  slot: 6\n---\n# Q3 plan\n\nShip something people love.\n```\n\n## Core commands\n\n```sh\nblink ls [--limit N] [--json]\nblink cat <id> [--json]\nblink new [text ...] [--json]\nblink present <id> [text ...] [presentation options] [--json]\nblink append <id> [text ...] [--json]\nblink type <id> [text ...] [--json]\nblink write <id> [text ...] [--json]\nblink search <query> [--json]\nblink rm <id> [--json]\nblink path [<id>]\nblink workspace <subcommand>\n```\n\nCommands accept stdin where content is optional. Prefer stdin when exact whitespace or multi-line Markdown matters.\n\n### Choose the correct write verb\n\n- `blink new`: create a note and let Canvas assign a unique slug.\n- `blink present`: get or create a known id while changing content, presentation, and placement together. This is the default composition verb.\n- `blink type`: append a visible update. If the panel is open, Canvas reveals the appended suffix as typed text.\n- `blink append`: the established sibling of `type`; it also appends a visible update.\n- `blink write`: quietly replace an existing note's body. Use only when full replacement is intended.\n- `blink rm`: delete a note. Use only when deletion is explicitly intended.\n\n## Common workflows\n\nCreate and place a complete note:\n\n```sh\nblink present q3-plan $'# Q3 plan\\n\\nShip something people love.' \\\n  --style focus --slot 6 --json\n```\n\nRead, search, and inspect paths:\n\n```sh\nblink ls --json\nblink cat q3-plan\nblink search \"launch\" --json\nblink path q3-plan\n```\n\nAdd a visible update without replacing the note:\n\n```sh\nprintf '%s\\n' '- rollout is green' | blink type q3-plan --json\n```\n\nQuietly replace the body from a file:\n\n```sh\nblink write q3-plan < revised-q3-plan.md\n```\n\nCreate and use a named workspace:\n\n```sh\nblink workspace init \"Acme Docs\"\nblink new --workspace acme-docs \"# Q3 planning\"\nblink workspace notes acme-docs --json\n```\n\nA workspace is a generic group plus treatment. Its definition lives in `config.json`; note membership is one `blink.workspace` frontmatter key.\n\n## Configuration\n\nEdit:\n\n```text\n~/Library/Application Support/Blink/config.json\n```\n\nCanvas watches the file and hot-applies valid changes to behavior, hotkeys, panels, appearance, motion, editor styling, named styles, and workspaces. Prefer this file to driving the Settings UI.\n\nFull reference: [Configuration](https://lattices.dev/canvas/docs/config)\n\n## Safety rules\n\n1. Prefer CLI mutations. Canvas's write path is atomic: temporary file, `fsync`, rename.\n2. Preserve note content and frontmatter you do not own.\n3. Do not replace a full note when an append or presentation-only change is enough.\n4. If a user has unsaved text in an open panel, the user's text wins over an external edit.\n5. Never move or rewrite device-local panel geometry casually. Spatial memory is part of the product contract.\n6. Opening an open note should focus it, never create a duplicate.\n7. Use `CANVAS_HOME` for isolated automation that must not touch the user's real note files. Set `BLINK_HOME` instead on older releases.\n8. Treat workspaces as generic treatments; do not bake a third-party product identity into Canvas.\n\n## Runtime behavior\n\nExternal writes are first-class. The running app watches the Notes directory, calls `NoteStore.reconcile()`, and emits the same created/updated/deleted notifications as in-app changes. The menubar, command palette, and open panels then update from the same runtime path.\n\nIf Canvas is closed, CLI writes still succeed. The note is available when the app next opens.\n\n## Canonical references\n\n- Website: https://lattices.dev/canvas\n- LLM overview: https://lattices.dev/canvas/docs/llms.txt\n- Repository: https://github.com/arach/lattices/tree/main/products/blink\n- CLI: https://github.com/arach/lattices/blob/main/products/blink/docs/cli.md\n- Config: https://github.com/arach/lattices/blob/main/products/blink/docs/config.md\n- Workspaces: https://github.com/arach/lattices/blob/main/products/blink/docs/workspaces.md\n- Downloads: https://lattices.dev/canvas/download\n- Development instructions: https://github.com/arach/lattices/blob/main/products/blink/AGENTS.md\n",
      "content": "# Canvas — instructions for agents\n\nCanvas is a native macOS spatial note-taking app. It is designed to be operated by humans and agents through the same local files.\n\nCanvas is the new name for Blink. The 2.3.0 canonical release installs\n`Canvas.app` and provides the `canvas` CLI. The package remains `@arach/blink`;\n`blink` is a compatible command alias. Legacy download channels still use\n`Blink.app`. Check the [release contract](https://lattices.dev/canvas/docs/release)\nfor the selected channel. Update the app and CLI together.\n\nKeep existing file paths, `blink.*` frontmatter, and protocol identifiers\nunchanged. The gateway identifies this product as `blink`, and `system.hello`\nreports `\"app\": \"Blink\"` with `\"displayName\": \"Canvas\"`.\n\nCanvas remains an independent app. The family commands `lats canvas` and\n`lats blink` forward to Canvas without sharing the Lattices app lifecycle.\n\n**The note is the window, and the desktop is the workspace**. A note is one floating panel in the running app and one Markdown file on disk. Opening an already-open note focuses its existing panel rather than creating a duplicate.\n\n## Choose the right surface\n\n- Operating Canvas: use the `canvas` CLI for notes and `config.json` for behavior and appearance. Use `blink` on an older installation.\n- Reading or writing content: prefer CLI commands; plain file access is the lower-level escape hatch.\n- Verifying live appearance or placement: use the GUI only when the task requires the rendered visual surface.\n- Developing Canvas itself: read the repository's `AGENTS.md` before changing code.\n\nThe running app also serves a local Unix socket for a limited set of live panel, note, and desk operations, plus newline-delimited events. See the [implemented socket API](https://lattices.dev/canvas/docs/api) for the supported methods. The broader agent API proposal and an MCP server are not implemented. Use the CLI and filesystem for ordinary note edits.\n\n## First check\n\n```sh\ncanvas ls --json\n```\n\nExpected result: a JSON list of notes. The command reads files only. It does\nnot open panels. For automation that must not touch the user's notes, set\n`CANVAS_HOME` to a scratch directory first:\n\n```sh\nexport CANVAS_HOME=\"$(mktemp -d)\"\ncanvas present hello $'# Hello\\n\\nFirst note from an agent.' --json\ncanvas cat hello\n```\n\n`present` creates the note `hello` and returns it as JSON. `cat` prints the\nMarkdown. The note lives only in the scratch directory. A Canvas app that runs\nwith the default home does not show it. Without a home override, the same\ncommands write to the user's notes, and a running app opens the note as a\npanel.\n\n## Requirements and install\n\n- macOS 14 or newer\n- Apple Silicon\n- Node.js 18 or newer for the npm shims\n\n```sh\nnpm install -g @arach/blink\ncanvas app install\ncanvas app open\n```\n\nThe npm package includes the signed native CLI. `canvas app install` fetches\nand validates the signed, notarized release before installing the app in\n`/Applications`. The installer prefers the canonical Canvas channel when\nversions tie; legacy releases retain `Blink.app`.\n\nCheck the installed CLI when version-sensitive behavior matters:\n\n```sh\ncanvas --version\nnpm view @arach/blink version\n```\n\n## File layout\n\nDefault file-backed root:\n\n```text\n~/Library/Application Support/Blink/\n├── Notes/\n│   └── <id>.md\n├── config.json\n└── attachments/\n```\n\nThe first nonempty `CANVAS_HOME`, then `BLINK_HOME`, overrides the file-backed\nroot for notes, config, attachments, ledger, and socket. Empty values are\nignored. Use the same override for the app and CLI for isolated tests and\nautomation. Older releases recognize `BLINK_HOME` only.\n\nThese overrides do not redirect device-local `UserDefaults`. Exact panel\nframes, the open-panel set, and per-note read/edit mode remain Mac-local state.\nThe Mac app disables mobile sharing when either home override is active.\n\n## Note model\n\n- One Markdown file is one note.\n- The filename stem and frontmatter `id` are a unique title-derived slug.\n- The title is derived from the first meaningful content line.\n- Note metadata and portable presentation live in YAML frontmatter.\n- Unknown or foreign frontmatter must survive round trips.\n- Portable placement intent such as `blink.slot` lives in frontmatter.\n- Exact pixel geometry is device-local and must never be substituted for portable placement intent.\n\nExample:\n\n```md\n---\nid: q3-plan\nblink:\n  style: focus\n  accent: \"#7dd3fc\"\n  slot: 6\n---\n# Q3 plan\n\nShip something people love.\n```\n\n## Core commands\n\n```sh\nblink ls [--limit N] [--json]\nblink cat <id> [--json]\nblink new [text ...] [--json]\nblink present <id> [text ...] [presentation options] [--json]\nblink append <id> [text ...] [--json]\nblink type <id> [text ...] [--json]\nblink write <id> [text ...] [--json]\nblink search <query> [--json]\nblink rm <id> [--json]\nblink path [<id>]\nblink workspace <subcommand>\n```\n\nCommands accept stdin where content is optional. Prefer stdin when exact whitespace or multi-line Markdown matters.\n\n### Choose the correct write verb\n\n- `blink new`: create a note and let Canvas assign a unique slug.\n- `blink present`: get or create a known id while changing content, presentation, and placement together. This is the default composition verb.\n- `blink type`: append a visible update. If the panel is open, Canvas reveals the appended suffix as typed text.\n- `blink append`: the established sibling of `type`; it also appends a visible update.\n- `blink write`: quietly replace an existing note's body. Use only when full replacement is intended.\n- `blink rm`: delete a note. Use only when deletion is explicitly intended.\n\n## Common workflows\n\nCreate and place a complete note:\n\n```sh\nblink present q3-plan $'# Q3 plan\\n\\nShip something people love.' \\\n  --style focus --slot 6 --json\n```\n\nRead, search, and inspect paths:\n\n```sh\nblink ls --json\nblink cat q3-plan\nblink search \"launch\" --json\nblink path q3-plan\n```\n\nAdd a visible update without replacing the note:\n\n```sh\nprintf '%s\\n' '- rollout is green' | blink type q3-plan --json\n```\n\nQuietly replace the body from a file:\n\n```sh\nblink write q3-plan < revised-q3-plan.md\n```\n\nCreate and use a named workspace:\n\n```sh\nblink workspace init \"Acme Docs\"\nblink new --workspace acme-docs \"# Q3 planning\"\nblink workspace notes acme-docs --json\n```\n\nA workspace is a generic group plus treatment. Its definition lives in `config.json`; note membership is one `blink.workspace` frontmatter key.\n\n## Configuration\n\nEdit:\n\n```text\n~/Library/Application Support/Blink/config.json\n```\n\nCanvas watches the file and hot-applies valid changes to behavior, hotkeys, panels, appearance, motion, editor styling, named styles, and workspaces. Prefer this file to driving the Settings UI.\n\nFull reference: [Configuration](https://lattices.dev/canvas/docs/config)\n\n## Safety rules\n\n1. Prefer CLI mutations. Canvas's write path is atomic: temporary file, `fsync`, rename.\n2. Preserve note content and frontmatter you do not own.\n3. Do not replace a full note when an append or presentation-only change is enough.\n4. If a user has unsaved text in an open panel, the user's text wins over an external edit.\n5. Never move or rewrite device-local panel geometry casually. Spatial memory is part of the product contract.\n6. Opening an open note should focus it, never create a duplicate.\n7. Use `CANVAS_HOME` for isolated automation that must not touch the user's real note files. Set `BLINK_HOME` instead on older releases.\n8. Treat workspaces as generic treatments; do not bake a third-party product identity into Canvas.\n\n## Runtime behavior\n\nExternal writes are first-class. The running app watches the Notes directory, calls `NoteStore.reconcile()`, and emits the same created/updated/deleted notifications as in-app changes. The menubar, command palette, and open panels then update from the same runtime path.\n\nIf Canvas is closed, CLI writes still succeed. The note is available when the app next opens.\n\n## Canonical references\n\n- Website: https://lattices.dev/canvas\n- LLM overview: https://lattices.dev/canvas/docs/llms.txt\n- Repository: https://github.com/arach/lattices/tree/main/products/blink\n- CLI: https://github.com/arach/lattices/blob/main/products/blink/docs/cli.md\n- Config: https://github.com/arach/lattices/blob/main/products/blink/docs/config.md\n- Workspaces: https://github.com/arach/lattices/blob/main/products/blink/docs/workspaces.md\n- Downloads: https://lattices.dev/canvas/download\n- Development instructions: https://github.com/arach/lattices/blob/main/products/blink/AGENTS.md"
    },
    {
      "id": "blink:api",
      "slug": "api",
      "product": "blink",
      "kind": "doc",
      "catalogKind": "guide",
      "current": true,
      "title": "Agent socket API",
      "description": "The implemented JSON-RPC methods and events on Canvas's local Unix socket.",
      "order": null,
      "sourcePath": "products/blink/docs/api.md",
      "url": "/canvas/docs/api",
      "rawUrl": "/canvas/docs/markdown/api.md",
      "markdownUrl": "/canvas/docs/api.md",
      "headings": [
        {
          "depth": 1,
          "text": "Agent socket API",
          "anchor": "agent-socket-api"
        },
        {
          "depth": 2,
          "text": "Connection",
          "anchor": "connection"
        },
        {
          "depth": 3,
          "text": "Private access",
          "anchor": "private-access"
        },
        {
          "depth": 2,
          "text": "Methods",
          "anchor": "methods"
        },
        {
          "depth": 3,
          "text": "placements.list",
          "anchor": "placements-list"
        },
        {
          "depth": 3,
          "text": "notes.present",
          "anchor": "notes-present"
        },
        {
          "depth": 3,
          "text": "desk.save and desk.restore",
          "anchor": "desk-save-and-desk-restore"
        },
        {
          "depth": 3,
          "text": "events.subscribe",
          "anchor": "events-subscribe"
        },
        {
          "depth": 2,
          "text": "Errors",
          "anchor": "errors"
        },
        {
          "depth": 2,
          "text": "Not implemented",
          "anchor": "not-implemented"
        }
      ],
      "tokensEstimate": 1398,
      "frontmatter": {
        "title": "Agent socket API",
        "description": "The implemented JSON-RPC methods and events on Canvas's local Unix socket."
      },
      "markdown": "---\ntitle: Agent socket API\ndescription: The implemented JSON-RPC methods and events on Canvas's local Unix socket.\n---\n\n# Agent socket API\n\nThe running Canvas app serves a small JSON-RPC 2.0 API on a local Unix domain\nsocket. Use it to read live panel state, present a note, save and restore desks,\nand receive change events.\n\nThis page documents only the methods that Canvas implements today. The broader\ndesign in [Agent API proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-api.md) is proposed and is not\nbuilt. Do not call a method from that proposal unless it is listed here.\n\nFor most work, prefer the [`canvas` CLI](/canvas/docs/cli). The CLI works when the\napp is closed. The socket works only while the app is running.\n\n## Connection\n\n| Property | Value |\n|---|---|\n| Transport | Unix domain socket, `SOCK_STREAM` |\n| Path | `~/Library/Application Support/Blink/blink.sock`, or the first nonempty `$CANVAS_HOME/blink.sock` / `$BLINK_HOME/blink.sock` override |\n| Access control | File mode `0600`. Only the user who runs Canvas can connect. There is no token. |\n| Framing | One JSON object per line, terminated by `\\n`, in both directions |\n| Protocol | JSON-RPC 2.0. Every request must include `\"jsonrpc\": \"2.0\"` and a string `method`. |\n| Lifetime | Canvas creates the socket at launch and removes it at quit. |\n\nThe client and the app must use the same home directory. If the socket file does\nnot exist, Canvas is not running with that home. The CLI reports this condition\nas `Blink is not running with the same BLINK_HOME (missing blink.sock)`.\n\n### Private access\n\nCanvas 2.3.0 keeps this socket path and protocol, with no token. It enforces\nthe following checks.\n\n- Canvas creates its home directory with mode `0700` and refuses symlinks in the\n  path.\n- The app accepts a connection only from a peer with the same user ID.\n- The CLI refuses a socket, or a socket directory, that another user owns or\n  that is group- or world-accessible.\n\nCanvas's agent socket does not move into `~/.lattices/run` or open a TCP port.\nCanvas's paired iOS note sync remains a separate Multipeer Connectivity service.\nA paired tailnet client can call a reviewed subset of agent methods through the\nopt-in Lattices gateway using the product identifier `blink`, one exact grant\nper method: `system.hello`,\n`placements.list`, `notes.list` and `notes.get` to read, and `notes.present`,\n`desk.save` and `desk.restore` to act. Canvas event streams stay local. See\n[Private IPC and the gateway](/lattices/docs/concepts#private-ipc-and-the-gateway).\n\nExample with `nc`:\n\n```sh\nprintf '%s\\n' '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"system.hello\"}' \\\n  | nc -U \"$HOME/Library/Application Support/Blink/blink.sock\"\n```\n\n## Methods\n\n| Method | Params | Result |\n|---|---|---|\n| `system.hello` | none | `{ \"protocol\": \"2.0\", \"version\": \"<app version>\", \"app\": \"Blink\" }` |\n| `placements.list` | none | Array of open panels (see below). |\n| `notes.list` | none | Array of `{ \"id\", \"title\", \"updated\" }`. `updated` is ISO 8601. |\n| `notes.get` | `id` (string, required), `content` (bool, optional) | `{ \"id\", \"title\", \"frontmatter\" }`, plus `content` when `content` is `true`. `frontmatter` holds the foreign frontmatter lines that Canvas does not own. |\n| `notes.present` | `id` (string, required), `content` (string), `slot` (integer), `style` (string) | `{ \"id\" }` |\n| `desk.save` | `name` (string, required) | `{ \"name\", \"updated\", \"panels\" }`. `panels` is the panel count. |\n| `desk.restore` | `name` (string, required) | `{ \"name\", \"updated\", \"panels\" }` |\n| `events.subscribe` | `topics` (optional) | `{ \"subscribed\": <topics> }` |\n\n### placements.list\n\nEach entry describes one open panel:\n\n```json\n{\n  \"id\": \"standup\",\n  \"frame\": { \"x\": 980, \"y\": 412, \"width\": 460, \"height\": 260 },\n  \"screen\": 1,\n  \"mode\": \"read\",\n  \"shaded\": false,\n  \"focused\": true,\n  \"z\": 0,\n  \"slot\": 6\n}\n```\n\n- `frame` is in AppKit screen coordinates (origin at the bottom left of the main\n  display). This differs from `blink desk open`, which takes display-local\n  coordinates from the top left.\n- `screen` is a one-based display index.\n- `z` is the front-to-back order. `0` is the frontmost panel.\n- `slot` is `null` when the panel has no grid slot.\n\n### notes.present\n\n`notes.present` creates the note if it does not exist, then opens or focuses its\npanel.\n\n- Canvas converts `id` to a slug.\n- `content` replaces the whole body. If you omit it, the body does not change.\n- `slot` sets `blink.slot`. `style` sets `blink.style`. Other presentation keys\n  do not change.\n- Canvas records the writer as `agent-socket` in `blink.lastWriter` and in the\n  edit ledger.\n\nThe CLI command `blink present` covers more presentation fields and does not\nneed the running app.\n\n### desk.save and desk.restore\n\n`desk.save` captures the open panels into `desks/<name>.json`. `desk.restore`\nreopens the panels in that file at their saved frames. A desk name must be 1 to\n80 characters of letters, digits, `-`, or `_`.\n\n`blink desk save <name>` and `blink desk restore <name>` call these methods.\n`blink desk ls`, `blink desk show`, and `blink desk rm` read and delete the desk\nfiles directly and do not use the socket.\n\n### events.subscribe\n\nAfter `events.subscribe`, the app sends notifications on the same connection.\nNotifications have no `id`:\n\n```json\n{ \"jsonrpc\": \"2.0\", \"method\": \"panel.moved\", \"params\": { \"id\": \"standup\" } }\n```\n\n| Event | Sent when |\n|---|---|\n| `note.created` | A note is created. |\n| `note.updated` | A note's content or metadata changes. |\n| `note.deleted` | A note is deleted. |\n| `panel.moved` | A panel moves, including a slot move. |\n| `panel.resized` | A panel is resized. |\n\nCanvas does not filter by `topics` today. A subscribed connection receives every\nevent in this table. Each event carries only the note `id`. Call `notes.get` or\n`placements.list` to read the new state.\n\n## Errors\n\nErrors use the JSON-RPC error object:\n\n```json\n{ \"jsonrpc\": \"2.0\", \"id\": 1, \"error\": { \"code\": -32004, \"message\": \"Note not found\" } }\n```\n\n| Code | Meaning |\n|---|---|\n| `-32600` | The line is not a valid JSON-RPC 2.0 request. The reply `id` is `null`. |\n| `-32601` | The method does not exist. |\n| `-32602` | A required parameter is missing. |\n| `-32004` | `notes.get` did not find the note. |\n| `-32000` | Another failure, such as an invalid desk name or a missing desk. The message gives the cause. |\n\n## Not implemented\n\nThese items appear in the [proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-api.md) and do not exist:\n\n- Authentication tokens, capability negotiation, and version ranges beyond\n  `system.hello`.\n- Topic filtering and richer event payloads.\n- `auto` placement, collision routing, scenes, and `tidy`. See\n  [Placement](/canvas/docs/placement).\n- An MCP server or SDK. See\n  [Agent integration proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-integration.md).\n",
      "content": "# Agent socket API\n\nThe running Canvas app serves a small JSON-RPC 2.0 API on a local Unix domain\nsocket. Use it to read live panel state, present a note, save and restore desks,\nand receive change events.\n\nThis page documents only the methods that Canvas implements today. The broader\ndesign in [Agent API proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-api.md) is proposed and is not\nbuilt. Do not call a method from that proposal unless it is listed here.\n\nFor most work, prefer the [`canvas` CLI](/canvas/docs/cli). The CLI works when the\napp is closed. The socket works only while the app is running.\n\n## Connection\n\n| Property | Value |\n|---|---|\n| Transport | Unix domain socket, `SOCK_STREAM` |\n| Path | `~/Library/Application Support/Blink/blink.sock`, or the first nonempty `$CANVAS_HOME/blink.sock` / `$BLINK_HOME/blink.sock` override |\n| Access control | File mode `0600`. Only the user who runs Canvas can connect. There is no token. |\n| Framing | One JSON object per line, terminated by `\\n`, in both directions |\n| Protocol | JSON-RPC 2.0. Every request must include `\"jsonrpc\": \"2.0\"` and a string `method`. |\n| Lifetime | Canvas creates the socket at launch and removes it at quit. |\n\nThe client and the app must use the same home directory. If the socket file does\nnot exist, Canvas is not running with that home. The CLI reports this condition\nas `Blink is not running with the same BLINK_HOME (missing blink.sock)`.\n\n### Private access\n\nCanvas 2.3.0 keeps this socket path and protocol, with no token. It enforces\nthe following checks.\n\n- Canvas creates its home directory with mode `0700` and refuses symlinks in the\n  path.\n- The app accepts a connection only from a peer with the same user ID.\n- The CLI refuses a socket, or a socket directory, that another user owns or\n  that is group- or world-accessible.\n\nCanvas's agent socket does not move into `~/.lattices/run` or open a TCP port.\nCanvas's paired iOS note sync remains a separate Multipeer Connectivity service.\nA paired tailnet client can call a reviewed subset of agent methods through the\nopt-in Lattices gateway using the product identifier `blink`, one exact grant\nper method: `system.hello`,\n`placements.list`, `notes.list` and `notes.get` to read, and `notes.present`,\n`desk.save` and `desk.restore` to act. Canvas event streams stay local. See\n[Private IPC and the gateway](/lattices/docs/concepts#private-ipc-and-the-gateway).\n\nExample with `nc`:\n\n```sh\nprintf '%s\\n' '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"system.hello\"}' \\\n  | nc -U \"$HOME/Library/Application Support/Blink/blink.sock\"\n```\n\n## Methods\n\n| Method | Params | Result |\n|---|---|---|\n| `system.hello` | none | `{ \"protocol\": \"2.0\", \"version\": \"<app version>\", \"app\": \"Blink\" }` |\n| `placements.list` | none | Array of open panels (see below). |\n| `notes.list` | none | Array of `{ \"id\", \"title\", \"updated\" }`. `updated` is ISO 8601. |\n| `notes.get` | `id` (string, required), `content` (bool, optional) | `{ \"id\", \"title\", \"frontmatter\" }`, plus `content` when `content` is `true`. `frontmatter` holds the foreign frontmatter lines that Canvas does not own. |\n| `notes.present` | `id` (string, required), `content` (string), `slot` (integer), `style` (string) | `{ \"id\" }` |\n| `desk.save` | `name` (string, required) | `{ \"name\", \"updated\", \"panels\" }`. `panels` is the panel count. |\n| `desk.restore` | `name` (string, required) | `{ \"name\", \"updated\", \"panels\" }` |\n| `events.subscribe` | `topics` (optional) | `{ \"subscribed\": <topics> }` |\n\n### placements.list\n\nEach entry describes one open panel:\n\n```json\n{\n  \"id\": \"standup\",\n  \"frame\": { \"x\": 980, \"y\": 412, \"width\": 460, \"height\": 260 },\n  \"screen\": 1,\n  \"mode\": \"read\",\n  \"shaded\": false,\n  \"focused\": true,\n  \"z\": 0,\n  \"slot\": 6\n}\n```\n\n- `frame` is in AppKit screen coordinates (origin at the bottom left of the main\n  display). This differs from `blink desk open`, which takes display-local\n  coordinates from the top left.\n- `screen` is a one-based display index.\n- `z` is the front-to-back order. `0` is the frontmost panel.\n- `slot` is `null` when the panel has no grid slot.\n\n### notes.present\n\n`notes.present` creates the note if it does not exist, then opens or focuses its\npanel.\n\n- Canvas converts `id` to a slug.\n- `content` replaces the whole body. If you omit it, the body does not change.\n- `slot` sets `blink.slot`. `style` sets `blink.style`. Other presentation keys\n  do not change.\n- Canvas records the writer as `agent-socket` in `blink.lastWriter` and in the\n  edit ledger.\n\nThe CLI command `blink present` covers more presentation fields and does not\nneed the running app.\n\n### desk.save and desk.restore\n\n`desk.save` captures the open panels into `desks/<name>.json`. `desk.restore`\nreopens the panels in that file at their saved frames. A desk name must be 1 to\n80 characters of letters, digits, `-`, or `_`.\n\n`blink desk save <name>` and `blink desk restore <name>` call these methods.\n`blink desk ls`, `blink desk show`, and `blink desk rm` read and delete the desk\nfiles directly and do not use the socket.\n\n### events.subscribe\n\nAfter `events.subscribe`, the app sends notifications on the same connection.\nNotifications have no `id`:\n\n```json\n{ \"jsonrpc\": \"2.0\", \"method\": \"panel.moved\", \"params\": { \"id\": \"standup\" } }\n```\n\n| Event | Sent when |\n|---|---|\n| `note.created` | A note is created. |\n| `note.updated` | A note's content or metadata changes. |\n| `note.deleted` | A note is deleted. |\n| `panel.moved` | A panel moves, including a slot move. |\n| `panel.resized` | A panel is resized. |\n\nCanvas does not filter by `topics` today. A subscribed connection receives every\nevent in this table. Each event carries only the note `id`. Call `notes.get` or\n`placements.list` to read the new state.\n\n## Errors\n\nErrors use the JSON-RPC error object:\n\n```json\n{ \"jsonrpc\": \"2.0\", \"id\": 1, \"error\": { \"code\": -32004, \"message\": \"Note not found\" } }\n```\n\n| Code | Meaning |\n|---|---|\n| `-32600` | The line is not a valid JSON-RPC 2.0 request. The reply `id` is `null`. |\n| `-32601` | The method does not exist. |\n| `-32602` | A required parameter is missing. |\n| `-32004` | `notes.get` did not find the note. |\n| `-32000` | Another failure, such as an invalid desk name or a missing desk. The message gives the cause. |\n\n## Not implemented\n\nThese items appear in the [proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-api.md) and do not exist:\n\n- Authentication tokens, capability negotiation, and version ranges beyond\n  `system.hello`.\n- Topic filtering and richer event payloads.\n- `auto` placement, collision routing, scenes, and `tidy`. See\n  [Placement](/canvas/docs/placement).\n- An MCP server or SDK. See\n  [Agent integration proposal](https://github.com/lattices-dev/lattices/blob/main/products/blink/docs/agent-integration.md)."
    }
  ],
  "prompts": []
}
