Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,37 @@ under the new version heading.

## [Unreleased]

## [1.2.2] - 2026-06-30

### Added
- **Agent skills for any coding agent.** BonsAI's canvas API (`127.0.0.1:7337`) now ships a portable
skill doc, not just a Claude Code skill. On first launch, if Claude Code, Codex CLI, and/or Cursor
are detected on the Mac, BonsAI offers to install the matching doc into each one's own config
location, so any of them can read and write the board over HTTP. Reinstall or add more anytime
from **Settings ▸ Connectors ▸ Agent Skills**.
- **Model pickers for the agent chat and board description.** Choose which Claude model each runs on
(Opus / Sonnet / Haiku). The chat picker lives in the Agent panel header and mirrors a matching
control in **Settings ▸ Runtime ▸ Models**; describing the board has its own picker in the same
place. Chat defaults to **Opus**, describe defaults to **Sonnet**. The choice is passed to
`claude --model`; Refine and Compile stay on the CLI default. The Describe picker disables itself
(with a note) when Codex — not Claude — is the active engine, since Codex ignores the Claude model.
- **Describe board now preserves image references.** Image cards keep their absolute file paths in
the board graph, so the copied description can point at the exact picture without granting the
headless prompt broad local-file read access.

### Fixed
- **Copying a board no longer drops image cards.** An image card now contributes its file path to
the copied and compiled prompt, so a coding agent can open it — and a board that holds only an
image no longer copies as "Nothing to copy yet".
- **Board edits made right before quit are no longer lost.** The board autosaves on a ~400ms
debounce, so an edit landing just before the app closed — including a `delete`/`add_text` op from
an external agent over the canvas API — could be dropped before the pending save fired. BonsAI now
flushes the pending save on termination, and a bare `SIGTERM` (e.g. `pkill` from the dev-loop
relaunch script) is rerouted through the normal quit path so the flush still runs.
- **Image selection ring no longer double-borders.** An image card draws its own rounded border, so
the accent selection ring — which sat a few pixels outside — read as a second, gapped border. The
ring now hugs the image's own edge as a single clean outline. Other elements are unchanged.

## [1.2.1] - 2026-06-30

### Added
Expand All @@ -22,14 +53,7 @@ under the new version heading.
allowed tools, and include a Settings control to reset remembered permissions.

### Fixed
- **Shift+Enter in the Agent chat inserts a newline instead of sending.** The input used `.onSubmit`,
which fired on every Return — including Shift+Return — so holding Shift still sent the message. It
now follows the standard chat convention (Slack, Discord, Linear): plain **Enter sends**, and
**Shift+Enter** breaks the line at the caret. ([#27](https://github.com/ojowwalker77/BonsAI/issues/27))
- **Shift+Enter now actually breaks the line (follow-up).** The fix above intercepted Return but
returned `.ignored` for Shift+Return, so the event fell through to the field editor and **selected
all the text** instead of inserting a newline. Shift+Enter now inserts the break directly into the
focused field editor at the caret. ([#27](https://github.com/ojowwalker77/BonsAI/issues/27))
- **Shift+Enter in the Agent chat inserts a newline instead of sending.** Shift+Enter breaks the line at the caret. ([#27](https://github.com/ojowwalker77/BonsAI/issues/27))

## [1.2.0] - 2026-06-30

Expand Down
54 changes: 54 additions & 0 deletions Sources/ComposerApp/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let panelController = PanelController()
private let hotKeyManager = HotKeyManager()
private let menuBarController = MenuBarController()
/// Held strongly so it keeps firing — a `DispatchSourceSignal` is cancelled on dealloc.
private var sigtermSource: DispatchSourceSignal?

func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.regular)
Expand All @@ -15,6 +17,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
_ = UpdaterController.shared
MentionStyleCache.shared.preload()
CanvasServer.shared.start()
promptForAgentSkillsIfNeeded()
installSigtermHandler()

NSApp.servicesProvider = self

Expand All @@ -31,6 +35,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
panelController.show()
}

/// The board autosaves on a ~400ms debounce; without this, an edit made just before quit
/// (e.g. a `delete`/`add_text` op from an external agent over the canvas API) is silently
/// lost because the pending save's timer never gets to fire.
func applicationWillTerminate(_ notification: Notification) {
CanvasBridge.shared.flush()
}

func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
if !hasVisibleWindows { panelController.show() }
return true
Expand Down Expand Up @@ -59,6 +70,49 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
}
}

/// First launch only: if a coding agent we know how to teach (Claude Code, Codex CLI, Cursor) is
/// detected on this Mac, offer to install the canvas-API doc into its config so it can drive the
/// board over `127.0.0.1:7337` without the user hand-rolling curl commands. Silent no-op if none
/// are detected, or if the user has already been asked once — re-installs live in Settings ▸
/// Connectors instead of re-prompting every launch.
private func promptForAgentSkillsIfNeeded() {
let promptedKey = "app.agentSkills.hasPrompted"
guard !UserDefaults.standard.bool(forKey: promptedKey) else { return }
let detected = AgentSkillTarget.allCases.filter(\.isDetected)
guard !detected.isEmpty else { return }

UserDefaults.standard.set(true, forKey: promptedKey)

let alert = NSAlert()
alert.alertStyle = .informational
alert.messageText = "Teach your coding agent the BonsAI board?"
let names = detected.map(\.displayName).joined(separator: ", ")
alert.informativeText = "BonsAI found \(names) on this Mac. Install a short skill doc so it knows how to read and write your board over the local canvas API? You can redo this anytime in Settings ▸ Connectors."
alert.addButton(withTitle: "Install")
alert.addButton(withTitle: "Not Now")
guard alert.runModal() == .alertFirstButtonReturn else { return }

let failures = AgentSkillsInstaller.installAllDetected()
guard !failures.isEmpty else { return }
let failure = NSAlert()
failure.alertStyle = .warning
failure.messageText = "Couldn't install for everyone"
failure.informativeText = failures.map { "\($0.key.displayName): \($0.value.localizedDescription)" }.joined(separator: "\n")
failure.runModal()
}

/// A bare `SIGTERM` (e.g. `pkill`, used by the dev-loop relaunch script) bypasses AppKit's
/// termination delegate entirely by default, so `applicationWillTerminate` would never run and
/// a pending autosave would never flush. Disarm the default disposition and re-route the signal
/// through the normal `NSApp.terminate` path so it does.
private func installSigtermHandler() {
signal(SIGTERM, SIG_IGN)
let source = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main)
source.setEventHandler { NSApp.terminate(nil) }
source.resume()
sigtermSource = source
}

/// Summon the board (if hidden) and open its companion Settings window.
func showSettings() {
panelController.show()
Expand Down
30 changes: 30 additions & 0 deletions Sources/ComposerApp/Models/ComposerModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,33 @@ enum HeadlessEngine: String, Codable, CaseIterable, Identifiable, Hashable, Send
}
}
}

/// A Claude model the headless `claude` CLI can target via `--model`. The rawValue is the CLI
/// *alias* (`opus` / `sonnet` / `haiku`), which the CLI resolves to the latest model in that tier —
/// so this never pins a dated snapshot and never ships its own model. The two surfaces that pick a
/// model default differently: the in-canvas chat agent defaults to Opus, describing the board
/// defaults to Sonnet. See [[ModelPreferences]] and docs/agent-engines.md.
enum ClaudeModel: String, Codable, CaseIterable, Identifiable, Hashable, Sendable {
case opus
case sonnet
case haiku

var id: String { rawValue }
/// Passed verbatim as the value of `claude --model`.
var cliAlias: String { rawValue }
var title: String {
switch self {
case .opus: "Opus"
case .sonnet: "Sonnet"
case .haiku: "Haiku"
}
}
/// A one-line tier hint shown beneath the name in a picker.
var tagline: String {
switch self {
case .opus: "Most capable"
case .sonnet: "Balanced"
case .haiku: "Fastest"
}
}
}
81 changes: 81 additions & 0 deletions Sources/ComposerApp/Resources/AgentSkills/claude-code-SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: bonsai-board
description: Write to (or read) the user's BonsAI board — the spatial idea canvas in the BonsAI macOS app. Use when the user asks to put / drop / add / send something to their BonsAI board, capture an idea or note on the board, sketch a diagram or architecture onto the canvas, evolve an idea already on the board, or read what's currently on it. Works from any repo or directory: BonsAI runs a loopback-only canvas server on 127.0.0.1:7337. Requires the BonsAI app to be running with a board open.
---

# Writing to the BonsAI board

BonsAI is a spatial idea canvas (a macOS app). It exposes a tiny **loopback-only** HTTP
server on `http://127.0.0.1:7337` so any local process — including this agent session — can
read and shape the live board. Your writes appear on screen instantly and are tagged as
agent-authored (`whoWrote: 2`), so the user can tell your cards from theirs.

The board you write to is **whichever board is currently open** in BonsAI — there is no
board addressing.

## Before you write: check it's reachable

```bash
curl -s -m 3 http://127.0.0.1:7337/health
```

- `{"ok":true,...}` → good, proceed.
- Connection refused / timeout → **BonsAI isn't running.** Tell the user to open it; do not retry in a loop.
- A write that returns `{"ok":false,"error":"no active canvas"}` → BonsAI is running but no
board is open/registered. Ask the user to open a board.

## Reading the board

```bash
curl -s http://127.0.0.1:7337/canvas
```

Returns `{ nodes, edges, readingOrder }`. Each node has `id`, `kind`, `text`, `x/y/w/h`, and
`whoWrote` (**1 = the human wrote/edited it, 2 = you drew it, 0 = unknown**). Read the board
before acting on one you've touched before — `whoWrote: 1` nodes are exactly what the human
added or changed since you last looked.

## Writing: POST one op to `/canvas`

Every mutation is a single JSON object `{"op": "...", ...}` POSTed to `/canvas`. It returns
`{"ok": true, ...}` (often with the new `id`) or `{"ok": false, "error": "..."}`.

```bash
curl -s -X POST http://127.0.0.1:7337/canvas \
--data-binary '{"op":"add_text","text":"Ship the loopback skill"}'
```

For text with quotes, newlines, or any length, write the JSON payload to a file first so
escaping is correct, then send it.

### Op vocabulary

| op | required | optional | returns |
|----|----------|----------|---------|
| `add_text` | `text` | `x`, `y` | `id` |
| `add_shape` | `kind` (`rectangle`\|`ellipse`\|`diamond`\|`line`\|`arrow`) | `w`, `h`, `x`, `y` | `id` |
| `create_diagram` | `nodes` | `edges`, `direction` | `nodes` (key→id map) |
| `relayout` | — | `direction` | — |
| `update_text` | `id`, `text` | — | — |
| `move` | `id` | `x`, `y` | — |
| `resize` | `id` | `w`, `h` | — |
| `delete` | `id` | — | — |
| `connect` | `from`, `to` (node ids) | `reason` (becomes arrow label) | `id` |
| `set_archived` | `id` | `archived` (bool, default true) | — |
| `supersede` | `id`, `text`, `reason` | — | `id` |

`direction` is `"down"` (default) or `"right"`.

Prefer `create_diagram` for any structure (architecture, flow, tree, comparison, decision
graph) — declare nodes + edges in one call and let the board lay it out. Never invent x/y to
place cards yourself.

When an approach changes, call `supersede` instead of silently overwriting — it fades the
old card, adds the new one below, and links them with a `reason`.

## Etiquette

- Keep cards concise; the canvas holds the detail, not long prose.
- Read before mutating a board you've touched; respect `whoWrote: 1` cards.
- One op per request. Batch related cards with `create_diagram` rather than many `add_text`.
- If the server is unreachable, say so once and stop — don't spin retrying.
30 changes: 30 additions & 0 deletions Sources/ComposerApp/Resources/AgentSkills/codex-AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## BonsAI canvas API

BonsAI is a spatial idea board (macOS app). When it's running, it exposes a **loopback-only**
HTTP server at `http://127.0.0.1:7337` for reading and writing the board that's currently
open. Use this whenever the user asks to put/drop/add something on their BonsAI board,
sketch a diagram onto the canvas, or read what's on it.

Check it's up first: `curl -s -m 3 http://127.0.0.1:7337/health` — if that fails to connect,
BonsAI isn't running; tell the user and don't retry in a loop.

Read the board: `curl -s http://127.0.0.1:7337/canvas` → `{ nodes, edges, readingOrder }`.
Each node has `whoWrote` (1 = human, 2 = agent, 0 = unknown) — treat `whoWrote: 1` nodes as
the human's latest input.

Write by POSTing one JSON op per request to `/canvas`:

```bash
curl -s -X POST http://127.0.0.1:7337/canvas \
--data-binary '{"op":"add_text","text":"Ship the loopback skill"}'
```

Ops: `add_text {text,x?,y?}`, `add_shape {kind: rectangle|ellipse|diamond|line|arrow, w?,h?,x?,y?}`,
`create_diagram {nodes,edges?,direction?}` (preferred for any structure — don't hand-place x/y
yourself), `relayout {direction?}`, `update_text {id,text}`, `move {id,x,y}`, `resize {id,w,h}`,
`delete {id}`, `connect {from,to,reason?}`, `set_archived {id,archived?}`,
`supersede {id,text,reason}` (use this instead of overwriting when an idea evolves — it keeps
the old card, fades it, and links the new one with the reason).

Keep cards short — the canvas holds detail, not paragraphs. Batch related cards with
`create_diagram` rather than many sequential `add_text` calls.
35 changes: 35 additions & 0 deletions Sources/ComposerApp/Resources/AgentSkills/cursor-bonsai-board.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
description: BonsAI canvas API — read and write the user's spatial idea board over a local loopback HTTP server
globs:
alwaysApply: false
---

# BonsAI canvas API

BonsAI is a spatial idea board (macOS app). When it's running, it exposes a **loopback-only**
HTTP server at `http://127.0.0.1:7337` for reading and writing the board that's currently
open. Use this whenever asked to put/drop/add something on the BonsAI board, sketch a
diagram onto the canvas, or read what's on it.

Check it's up first: `curl -s -m 3 http://127.0.0.1:7337/health` — if that fails to connect,
BonsAI isn't running; say so and don't retry in a loop.

Read the board: `curl -s http://127.0.0.1:7337/canvas` → `{ nodes, edges, readingOrder }`.
Each node has `whoWrote` (1 = human, 2 = agent, 0 = unknown) — treat `whoWrote: 1` nodes as
the human's latest input.

Write by POSTing one JSON op per request to `/canvas`:

```bash
curl -s -X POST http://127.0.0.1:7337/canvas \
--data-binary '{"op":"add_text","text":"Ship the loopback skill"}'
```

Ops: `add_text {text,x?,y?}`, `add_shape {kind: rectangle|ellipse|diamond|line|arrow, w?,h?,x?,y?}`,
`create_diagram {nodes,edges?,direction?}` (preferred for any structure — don't hand-place x/y
yourself), `relayout {direction?}`, `update_text {id,text}`, `move {id,x,y}`, `resize {id,w,h}`,
`delete {id}`, `connect {from,to,reason?}`, `set_archived {id,archived?}`,
`supersede {id,text,reason}` (use instead of overwriting when an idea evolves).

Keep cards short — the canvas holds detail, not paragraphs. Batch related cards with
`create_diagram` rather than many sequential `add_text` calls.
Loading
Loading