Skip to content

Commit 7e8f914

Browse files
authored
feat(plan): unified header dropdown + Agent Instructions (#515)
* feat(plan): unify header settings into combined dropdown Replace the desktop sprawl (ModeToggle, Settings button, Export sub-dropdown) and the separate MobileMenu with a single PlanHeaderMenu modeled on ReviewHeaderMenu — one ActionMenu rendered at all breakpoints. Move version + release notes into the dropdown footer, drop the origin/agent badge, and stub an "Agent Instructions" item that copies the current plan plus an /api/external-annotations URL for external agents (real protocol body lands in a follow-up). Also fix the annotate-folder Send button: it was hidden whenever a file was opened via linkedDoc, so users in `plannotator annotate ./` couldn't submit feedback. Gate now allows linked-doc state through in annotate mode. Settings is always mounted instead of skipping when a linked doc is active, so the dropdown's Settings item works in every state. For provenance purposes, this commit was AI assisted. * feat(plan): real Agent Instructions clipboard payload Replace the placeholder string in handleCopyAgentInstructions with a real, contract-accurate markdown payload that teaches an external agent (Claude Code, Codex, custom scripts) how to: - read the current plan via GET /api/plan - POST single or batch annotations to /api/external-annotations - choose COMMENT vs DELETION vs GLOBAL_COMMENT and use originalText for inline highlighting (or skip it for sidebar-only) - list, delete-by-id, and delete-by-source for cleanup before reposting The payload lives in packages/ui/utils/planAgentInstructions.ts as buildPlanAgentInstructions(origin) — a single pure function that takes the base URL and returns the markdown. Plan and code-review modes have different annotation shapes, so each owns its own instructions module; the review counterpart will live alongside this file when it's added. For provenance purposes, this commit was AI assisted. * docs: list planAgentInstructions.ts in AGENTS.md utils enumeration For provenance purposes, this commit was AI assisted. * fix(plan): restore 'system' theme option in header dropdown The unified header dropdown only exposed light/dark, dropping the 'system' (follow OS) option that the previous ModeToggle and MobileMenu both supported. Clicking either explicit mode would silently overwrite a prior 'system' setting. Add it back as a third segmented control, matching the old MobileMenu's three-way layout. For provenance purposes, this commit was AI assisted. * refactor(ui): extract shared theme-mode icons PlanHeaderMenu and ThemeTab each inlined their own Sun/Moon/System SVGs (and the deleted MobileMenu had its own copy too). Pull them into a shared icons/themeIcons.tsx module so the iconography stays consistent across the dropdown and the settings tab. Use the cleaner Heroicons paths from ThemeTab as the canonical art. ReviewHeaderMenu still inlines its own copies — left alone for this PR since the broader review/plan menu unification is a separate follow-up. For provenance purposes, this commit was AI assisted. * fix(plan): use ReviewAgentsIcon for Agent Instructions menu item Reuse the magnifying-glass-with-cog icon already used for the Review Agents tab so the same visual marker identifies agent-related affordances across plan and review modes. For provenance purposes, this commit was AI assisted. * fix(plan): drop DELETION + unanchored COMMENT from agent instructions Plannotator's existing render and export pipeline only handles two shapes well: an anchored COMMENT with originalText, and a GLOBAL_COMMENT with no anchor. The previous instructions also documented: - COMMENT without originalText — sidebar shows an empty quote bubble ("") and the export header reads `Feedback on: ""`. The text survives but the framing is broken. - DELETION with explanatory text — the text is invisible in the sidebar and silently dropped on export, replaced by a generic template. Real information loss. Rather than expand the export pipeline to honor those edge cases, narrow the documented surface to the two shapes that work today: inline COMMENT (requires originalText) and GLOBAL_COMMENT (no anchor). DELETION is omitted entirely; agents that need to suggest a removal can post an inline COMMENT pointing at the phrase. For provenance purposes, this commit was AI assisted. * fix(plan): clear current new-settings hints, surface pulse on menu trigger The new-settings indicator infrastructure (hasNewSettings / version constant / markNewSettingsSeen) stays — it's the scaffolding for future hint flagging — but every current marker is removed: - Drop the four inline 'new' badges in Settings.tsx (Plan Width row, Quick Labels row, mobile + desktop tab navigators). - Drop the dead pulse on the standalone gear button — that button now lives inside <div className="hidden"> after the header refactor, so the pulse was unreachable anyway. Surface the pulse on the new PlanHeaderMenu trigger instead. App.tsx seeds local hasNewSettingsHints state from hasNewSettings(), passes it to the menu, and clears it (plus calls markNewSettingsSeen) when the user opens Settings from the dropdown. For provenance purposes, this commit was AI assisted. * docs: list icons/ subdirectory in AGENTS.md components tree For provenance purposes, this commit was AI assisted. * fix(server): require originalText for plan COMMENT annotations The plan-mode external-annotation contract documents that COMMENT annotations must carry an originalText anchor; sidebar-only feedback uses GLOBAL_COMMENT instead. transformPlanInput already enforced this for DELETION but silently defaulted COMMENT's originalText to "" when missing — producing a broken render (empty quote bubble in the sidebar, exported as `Feedback on: ""`). Reject COMMENT without a non-empty originalText and steer the caller toward GLOBAL_COMMENT in the error message so the fix is self-evident to misbehaving agents. For provenance purposes, this commit was AI assisted. * chore: drop dead new-hints scaffolding leftovers After clearing the four current inline hint markers, two pieces of support code were unreachable: - The destructured setShowNewHints setter in Settings.tsx — the state itself stays as a stable mount-time snapshot for any future hint markers, but the setter has no callers. Drop it and add a comment explaining why the useState wrapper is intentional scaffolding. - The settings-ping keyframe in editor/index.css — only the deleted gear-button pulse referenced it; the new pulse on PlanHeaderMenu's trigger uses Tailwind's built-in animate-ping instead. For provenance purposes, this commit was AI assisted.
1 parent 57495ec commit 7e8f914

11 files changed

Lines changed: 603 additions & 428 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ plannotator/
4949
│ ├── ui/ # Shared React components + theme
5050
│ │ ├── theme.css # Single source of truth for color tokens + Tailwind bridge
5151
│ │ ├── components/ # Viewer, Toolbar, Settings, etc.
52+
│ │ │ ├── icons/ # Shared SVG icon components (themeIcons, etc.)
5253
│ │ │ ├── plan-diff/ # PlanDiffBadge, PlanDiffViewer, clean/raw diff views
5354
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser, ArchiveBrowser
54-
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts
55+
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts, planAgentInstructions.ts
5556
│ │ ├── hooks/ # useAnnotationHighlighter.ts, useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts, useArchive.ts
5657
│ │ └── types.ts
5758
│ ├── ai/ # Provider-agnostic AI backbone (providers, sessions, endpoints)

packages/editor/App.tsx

Lines changed: 78 additions & 186 deletions
Large diffs are not rendered by default.

packages/editor/index.css

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,3 @@ pre code.hljs .hljs-code {
182182
.sidebar-tab-flag:hover {
183183
transform: translateX(2px);
184184
}
185-
186-
@keyframes settings-ping {
187-
0%, 100% { opacity: 0; transform: scale(1); }
188-
50% { opacity: 0.5; transform: scale(2); }
189-
}

packages/shared/external-annotation.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ export function transformPlanInput(
144144
return { error: `annotations[${i}] DELETION type requires non-empty "originalText" field` };
145145
}
146146

147+
// COMMENT requires originalText so the renderer can pin it to a phrase.
148+
// External agents that want sidebar-only feedback should use GLOBAL_COMMENT
149+
// instead — without a phrase to anchor to, a COMMENT renders as an empty
150+
// quote bubble in the sidebar and exports as `Feedback on: ""`.
151+
if (type === "COMMENT" && (typeof obj.originalText !== "string" || obj.originalText.length === 0)) {
152+
return {
153+
error: `annotations[${i}] COMMENT requires non-empty "originalText" field. Use GLOBAL_COMMENT for sidebar-only feedback.`,
154+
};
155+
}
156+
147157
annotations.push({
148158
id: crypto.randomUUID(),
149159
blockId: "external",

packages/ui/components/ActionMenu.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,30 @@ interface ActionMenuItemProps {
6262
onClick: () => void;
6363
icon: React.ReactNode;
6464
label: string;
65+
subtitle?: string;
6566
badge?: React.ReactNode;
6667
}
6768

6869
export const ActionMenuItem: React.FC<ActionMenuItemProps> = ({
6970
onClick,
7071
icon,
7172
label,
73+
subtitle,
7274
badge,
7375
}) => (
7476
<button
7577
onClick={onClick}
7678
className="flex w-full items-center gap-2 px-3 py-2 text-left text-xs transition-colors hover:bg-muted"
7779
>
7880
<span className="text-muted-foreground">{icon}</span>
79-
<span className="flex-1">{label}</span>
81+
{subtitle ? (
82+
<span className="flex flex-1 flex-col gap-0.5">
83+
<span>{label}</span>
84+
<span className="text-[10px] text-muted-foreground">{subtitle}</span>
85+
</span>
86+
) : (
87+
<span className="flex-1">{label}</span>
88+
)}
8089
{badge}
8190
</button>
8291
);

packages/ui/components/MobileMenu.tsx

Lines changed: 0 additions & 211 deletions
This file was deleted.

0 commit comments

Comments
 (0)