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
10 changes: 8 additions & 2 deletions apps/hook/server/inbox-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { handleInboxServerReady, startInboxServer } from "@plannotator/server/in
import { exposureWarning, type InboxTailscaleState } from "@plannotator/server/inbox-tailscale";
import { openBrowser } from "@plannotator/server/browser";
import { agentSpec, createAgentEnvironment, formatConnectReport, inboxConnectDisabled } from "@plannotator/server/inbox-agents";
import { addInboxNote, inboxNotesDisabled, markNotesAsked } from "@plannotator/server/inbox-agent-notes";
import { addInboxNote, INBOX_NOTE_BODY, inboxNotesDisabled, markNotesAsked } from "@plannotator/server/inbox-agent-notes";
import { INBOX_AGENT_LABELS, INBOX_AUTO_SEND_AFTER_MS, INBOX_NOTE_AGENT_IDS, type InboxConnectReport, type InboxNoteAgentId } from "@plannotator/core/inbox-types";
import { writeUrlQr } from "@plannotator/server/qr";
import { getCliVersion } from "./cli";
Expand Down Expand Up @@ -314,6 +314,12 @@ async function writeNotes(dataDir: string, list: string): Promise<number> {
const added: InboxNoteAgentId[] = [];
for (const id of ids as InboxNoteAgentId[]) {
const label = INBOX_AGENT_LABELS[id];
if (id === "cursor") {
process.stdout.write(`${INBOX_NOTE_BODY}\n\n`);
process.stderr.write("Cursor User Rules are managed in Cursor Settings → Rules. Copy the block above there; Plannotator cannot detect, update or remove a copied rule.\n");
added.push(id);
continue;
}
// Never a file for an agent that is not on this computer.
if (!agentSpec(id).detect(environment)) {
ok = false;
Expand Down Expand Up @@ -342,7 +348,7 @@ async function writeNotes(dataDir: string, list: string): Promise<number> {
}
// The window never asks about an agent whose note is there now; the others are still asked, once each.
await markNotesAsked(added, { dataDir, selfCommand: selfCommand(), environment });
if (ok) process.stderr.write("Each agent reads it from its next session. Remove it in the Inbox's Settings or with plannotator uninstall.\n");
if (ok && added.some((id) => id !== "cursor")) process.stderr.write("Managed notes are read from each agent's next session. Remove them in the Inbox's Settings or with plannotator uninstall.\n");
return ok ? 0 : 1;
}

Expand Down
10 changes: 7 additions & 3 deletions apps/hook/server/inbox-connect-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,19 @@ test("plannotator inbox --notes adds only the notes named, starts nothing, and a
mkdirSync(join(w.home, ".codex"), { recursive: true });
writeFileSync(join(w.home, ".codex", "AGENTS.md"), "Mine.\n");
mkdirSync(join(w.home, ".gemini"), { recursive: true });
const result = await cli(w, ["inbox", "--notes", "codex,gemini"]);
const result = await cli(w, ["inbox", "--notes", "codex,gemini,cursor"]);
expect(result.code).toBe(0);
expect(result.stdout).toBe("");
expect(result.stdout).toContain("## Plannotator Inbox");
expect(result.stdout).not.toContain("plannotator-inbox:start");
expect(result.stderr).toContain("Cursor User Rules are managed in Cursor Settings → Rules");
expect(result.stderr).toContain("Plannotator cannot detect, update or remove a copied rule.");
expect(result.stderr).toContain("Codex: Added a marked section to ~/.codex/AGENTS.md.");
expect(result.stderr).toContain("Gemini CLI: Created ~/.gemini/GEMINI.md.");
expect(readFileSync(join(w.home, ".codex", "AGENTS.md"), "utf8")).toContain("<!-- plannotator-inbox:start v1");
expect(existsSync(join(w.home, ".pi"))).toBe(false);
expect(existsSync(join(w.home, ".cursor"))).toBe(false);
expect(existsSync(join(w.home, ".plannotator", "inbox", "inbox.json"))).toBe(false);
expect(JSON.parse(readFileSync(join(w.home, ".plannotator", "config.json"), "utf8")).inboxAgentNotes).toEqual({ offered: true, asked: ["codex", "gemini"] });
expect(JSON.parse(readFileSync(join(w.home, ".plannotator", "config.json"), "utf8")).inboxAgentNotes).toEqual({ offered: true, asked: ["codex", "gemini", "cursor"] });

const bad = await cli(w, ["inbox", "--notes", "vscode"]);
expect(bad.code).toBe(1);
Expand Down
3 changes: 2 additions & 1 deletion apps/hook/server/inbox-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export function formatInboxHelp(options: InboxHelpOptions): string {
const noteLines = INBOX_NOTE_AGENT_IDS.map((id) => {
const target = noteTarget(id, agents);
const shown = displayPath(target.path, agents);
if (id === "cursor") return `- ${INBOX_AGENT_LABELS[id]} (\`${id}\`): manual copy to Cursor Settings → Rules. \`plannotator inbox --notes cursor\` prints the note; Plannotator cannot detect, update or remove a copied rule.`;
if (target.reason) return `- ${INBOX_AGENT_LABELS[id]} (\`${id}\`): none. ${target.reason}`;
return `- ${INBOX_AGENT_LABELS[id]} (\`${id}\`): ${code(shown ?? "")}, ${target.create ? "creates the file" : "adds a marked section"}.`;
});
Expand Down Expand Up @@ -386,7 +387,7 @@ export function formatInboxHelp(options: InboxHelpOptions): string {
"",
"## A note in each agent's instructions",
"",
"Plannotator can add a short, marked note about the Inbox to an agent's own instructions file, so it sends decisions and finished work here while the person is away. Only with the person's yes: show them this list, then run `plannotator inbox --notes <ids>` with only the agents they agreed to (comma-separated). It never overwrites a note the person edited; the Inbox's Settings and `plannotator uninstall` remove it. On this computer:",
"Plannotator can add a short note about the Inbox to an agent's instructions, so it sends decisions and finished work here while the person is away. Only with the person's yes: show them this list, then run `plannotator inbox --notes <ids>` with only the agents they agreed to (comma-separated). Managed notes never overwrite a note the person edited and can be removed in Settings or with `plannotator uninstall`; Cursor instead requires manual copy/paste and Plannotator cannot track or remove it. On this computer:",
"",
...noteLines,
"",
Expand Down
5 changes: 3 additions & 2 deletions apps/marketing/src/content/docs/reference/inbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,13 @@ The first time you open the Inbox after it connected your agents, a dialog asks
| Gemini CLI | `~/.gemini/GEMINI.md` (or your `context.fileName`) |
| Pi | the one instructions file Pi reads in `~/.pi/agent/` |
| OpenCode | `~/.config/opencode/AGENTS.md`, only when you already have it: creating it would stop OpenCode 1 reading your `~/.claude/CLAUDE.md` |
| Cursor | Cursor User Rules in Settings → Rules (manual copy; no supported global rules file) |

The note sits between two marker lines at the end of the file. Plannotator never overwrites a note you edited, and removing it removes only that section. `plannotator inbox --notes codex,gemini` adds it from the terminal (the agent install prompt runs this only after you say yes). `plannotator inbox --help` lists the exact files on your computer.
Managed notes sit between two marker lines at the end of the file. Plannotator never overwrites a note you edited, and removing it removes only that section. `plannotator inbox --notes codex,gemini` adds managed notes from the terminal (the agent install prompt runs this only after you say yes). For Cursor, `plannotator inbox --notes cursor` prints the note to copy into Cursor Settings → Rules. Plannotator cannot detect whether it was pasted, update it or remove it; a copied Cursor rule is never shown as installed. `plannotator inbox --help` lists exact files and manual steps.

### Settings → Agents

Settings lists each agent found on this computer with up to three switches: **Reaches the Inbox** (its MCP entry, or the inbox tool for Claude Code, Pi and OpenCode), **Reminder at session start** (Claude Code, Pi and OpenCode: one line at the start of each session that has the inbox tool) and **Note in its instructions**. A change applies to that agent's next session. An agent you turn off here is never connected again by `plannotator inbox`. Agents not found are folded under **Not found on this computer**, with the command or one-click link for each, editors included.
Settings lists each agent found on this computer with up to three switches: **Reaches the Inbox** (its MCP entry, or the inbox tool for Claude Code, Pi and OpenCode), **Reminder at session start** (Claude Code, Pi and OpenCode: one line at the start of each session that has the inbox tool) and **Note in its instructions**. Cursor's note is a manual Show note workflow, not a managed switch; copy it into Settings → Rules. Plannotator cannot detect, update or remove that copy. Managed note changes apply to that agent's next session. An agent you turn off here is never connected again by `plannotator inbox`. Agents not found are folded under **Not found on this computer**, with the command or one-click link for each, editors included.

## What you see

Expand Down
6 changes: 3 additions & 3 deletions packages/core/inbox-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1439,8 +1439,8 @@ export interface InboxConnectReport {
}

/** The agents whose own instructions file can carry the Inbox note (packages/server/inbox-agent-notes.ts). */
export type InboxNoteAgentId = "claude-code" | "codex" | "opencode" | "pi" | "gemini";
export const INBOX_NOTE_AGENT_IDS: readonly InboxNoteAgentId[] = ["claude-code", "codex", "gemini", "pi", "opencode"];
export type InboxNoteAgentId = "claude-code" | "codex" | "opencode" | "pi" | "gemini" | "cursor";
export const INBOX_NOTE_AGENT_IDS: readonly InboxNoteAgentId[] = ["claude-code", "codex", "gemini", "pi", "opencode", "cursor"];

/**
* Where an agent's note would go and how it stands:
Expand All @@ -1457,7 +1457,7 @@ export interface InboxAgentNoteStatus {
path: string | null;
/** The file is a link; `target` is where it leads (shown). */
target: string | null;
state: "absent" | "added" | "edited" | "damaged" | "refused" | "unavailable";
state: "absent" | "added" | "edited" | "damaged" | "refused" | "unavailable" | "manual";
/** Adding it creates the file. */
create: boolean;
/** Plannotator created the file (removing the note deletes it when nothing else is left). */
Expand Down
2 changes: 1 addition & 1 deletion packages/inbox/agentNoteDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ test.skipIf(!hasDom)('Add: only the ticked notes are sent; results in the same r
// The results view: focus on Done, the live region says what was written, the band is gone.
expect(document.activeElement).toBe(button(el, 'Done'));
expect(el.querySelector('[data-anote-band]')).toBeNull();
expect(el.querySelector('.ib-anote-doneline')?.textContent).toBe('Each agent reads it from its next session. Fix the file, then try again here or in Settings → Agents.');
expect(el.querySelector('.ib-anote-doneline')?.textContent).toBe('Managed notes are read from each agent’s next session. Fix the file, then try again here or in Settings → Agents.');
expect(el.querySelector('[aria-live="polite"]')?.textContent).toBe("Added the note to Codex. Couldn't add the note to Gemini CLI.");

act(() => button(el, 'Try again').click());
Expand Down
5 changes: 3 additions & 2 deletions packages/inbox/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
export function noteCandidates(model: InboxAgentsModel): InboxAgentRow[] {
const failed = new Set(model.last_report?.failed.map((f) => f.agent) ?? []);
return model.agents.filter(
(row) => row.detected && row.note !== null && row.note.state === 'absent' && row.reach.on && !failed.has(row.id),
(row) => row.detected && row.note !== null && (row.note.state === 'absent' || row.note.state === 'manual') && row.reach.on && !failed.has(row.id),
);
}

Expand Down Expand Up @@ -119,8 +119,9 @@ export function defaultPicks(rows: readonly InboxAgentRow[]): Record<string, boo
}

/** What a note result shows in its row. */
export function noteResultLabel(result: { ok: true; action: string } | { ok: false }): 'Created' | 'Added' | 'Not added' {
export function noteResultLabel(result: { ok: true; action: string } | { ok: false }): 'Created' | 'Added' | 'Copy manually' | 'Not added' {
if (!result.ok) return 'Not added';
if (result.action === 'manual') return 'Copy manually';
return result.action === 'created' ? 'Created' : 'Added';
}

Expand Down
7 changes: 5 additions & 2 deletions packages/inbox/agentsSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ function model(over: Partial<InboxAgentsModel> = {}): InboxAgentsModel {
tool('opencode', 'OpenCode', {
note: note('opencode', { path: '~/.config/opencode/AGENTS.md', state: 'unavailable', reason: 'Creating this file would stop OpenCode 1 from reading your ~/.claude/CLAUDE.md.' }),
}),
mcp('cursor', 'Cursor', '~/.cursor/mcp.json', { reach: { kind: 'mcp', on: false, state: 'removed', path: '~/.cursor/mcp.json', locked: false, reason: null } }),
mcp('cursor', 'Cursor', '~/.cursor/mcp.json', {
reach: { kind: 'mcp', on: false, state: 'removed', path: '~/.cursor/mcp.json', locked: false, reason: null },
note: note('cursor', { state: 'manual', reason: 'Cursor User Rules are managed in Settings → Rules; Plannotator cannot install, update or remove a copied rule.' }),
}),
{ ...mcp('amp', 'Amp', '~/.config/amp/settings.json'), detected: false },
],
last_report: null,
Expand Down Expand Up @@ -124,7 +127,7 @@ test.skipIf(!hasDom)('one group per agent found, with its switches and the state
expect(group(el, 'cursor').querySelector('.ib-agstate')?.textContent).toBe('Removed outside Plannotator');
expect(rows(group(el, 'cursor'))).toEqual([
{ k: 'Reaches the Inbox', v: "plannotator-inbox is no longer in ~/.cursor/mcp.json. Plannotator won't add it back on its own; turn this on to add it.", sw: 'false', sub: null },
{ k: 'Note in its instructions', v: 'Cursor keeps its rules in its own settings, so there is no file to add a note to.', sw: null, sub: null },
{ k: 'Note in its instructions', v: 'Cursor User Rules · copy into Settings → RulesShow note', sw: null, sub: 'Cursor User Rules are managed in Settings → Rules; Plannotator cannot install, update or remove a copied rule.' }
]);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/inbox/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,7 @@ export const inboxApi = {
};

/** One note the dialog asked to add: written, or why not. */
export type InboxNoteResult = { ok: true; agent: InboxNoteAgentId; action: string; path: string } | { ok: false; agent: InboxNoteAgentId; reason: string; path: string | null };
export type InboxNoteResult = { ok: true; agent: InboxNoteAgentId; action: string; path: string | null } | { ok: false; agent: InboxNoteAgentId; reason: string; path: string | null };

/** One store line as the event stream carries it (packages/server/inbox.ts, `eventPayload`). */
export type InboxEvent =
Expand Down
Loading
Loading