From 9a1d4b7983dfd50e8eb40ee9770e44999d405f60 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Thu, 9 Jul 2026 19:43:44 -0500 Subject: [PATCH] fix(ui): use prose editor for markdown agent instructions (#9332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Agent setup depends on instruction files that are readable and editable from the board UI > - The instructions tab already receives server-side metadata describing whether each file is Markdown > - The UI was deciding Markdown editor usage primarily from the file extension, which makes extensionless Markdown instruction files feel like raw code > - This pull request makes the instructions editor trust server Markdown metadata for existing files and keep extension fallback only for new unsaved files > - The benefit is that AGENTS-style prose instructions render and edit like prose while explicitly non-Markdown files still use the raw textarea ## Linked Issues or Issue Description - Refs #8201 - Refs #5652 - Refs #3427 - Refs #2068 - Related PRs: #2468, #2620 ## What Changed - Use server `markdown` metadata from instruction file details/summaries to choose the prose Markdown editor for existing instruction files. - Keep extension-based Markdown detection only for pending new files before server metadata exists. - Remove the monospace content styling from the Markdown editor path so prose instructions read like normal text. - Add focused tests for extensionless Markdown files, new `.md` files, and `.md` files explicitly marked non-Markdown by the server. ## Verification - `pnpm check:token-gates` - `pnpm exec vitest run ui/src/pages/AgentDetail.instructions.test.tsx` - `pnpm --filter @paperclipai/ui typecheck` ## Risks - Low risk. The editor selection now depends on server metadata for existing files, so incorrect server metadata would choose the wrong editor. The fallback still preserves extension-based behavior for newly created unsaved files. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI GPT-5 via Codex coding agent, tool-enabled terminal workflow. Context window details were not exposed by the runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip --- ui/src/index.css | 2 +- .../pages/AgentDetail.instructions.test.tsx | 389 ++++++++++++++++++ ui/src/pages/AgentDetail.tsx | 23 +- 3 files changed, 410 insertions(+), 4 deletions(-) create mode 100644 ui/src/pages/AgentDetail.instructions.test.tsx diff --git a/ui/src/index.css b/ui/src/index.css index 14e234e4e3..c76beed8ae 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -1470,7 +1470,7 @@ span.paperclip-mention-chip[data-mention-kind="external-object"] { --hex-eab308: #eab308; /* ActivityCharts.tsx — priority 'medium'; also the 0.5-0.8 success-rate bar tint. */ --hex-6b7280: #6b7280; /* ActivityCharts.tsx — priority 'low' + unknown-status statusColors fallback. */ --hex-10b981: #10b981; /* ActivityCharts.tsx — >=0.8 success-rate bar tint; also the run-activity 'succeeded' segment. */ - --hex-737373: #737373; /* ActivityCharts.tsx — run activity "other" segment tint. */ + --hex-737373: #737373; /* ActivityCharts.tsx — run activity 'other' segment tint. */ --project-none: #64748b; /* Semantic rename of --hex-64748b (DECISION-SHEET.md A3, value unchanged). 'No project assigned' muted-slate fallback (TOKEN-AUDIT.md 1.3) across Routines/MarkdownEditor/RoutineRunVariablesDialog/RoutineList/IssueColumns/editable-sections; also ActivityCharts.tsx status 'backlog'. */ --project-seed: #6366f1; /* Semantic rename of --hex-6366f1 (DECISION-SHEET.md A3, value unchanged). Project-color-fallback indigo seed default (ProjectDetail/PipelineSettings/IssueProperties/NewIssueDialog) — new-project-color-picker-seed family per TOKEN-AUDIT.md 1.3. */ --liveness-blue: #2563eb; /* DECISION-SHEET.md A6: IssueChatThread.tsx human-message 'liveness blue' bubble (PAP-95 rev 5). Same value as --status-task-in_progress by coincidence, decoupled on purpose so a future status-hue change doesn't drag the chat bubble along. */ diff --git a/ui/src/pages/AgentDetail.instructions.test.tsx b/ui/src/pages/AgentDetail.instructions.test.tsx new file mode 100644 index 0000000000..a255227770 --- /dev/null +++ b/ui/src/pages/AgentDetail.instructions.test.tsx @@ -0,0 +1,389 @@ +// @vitest-environment jsdom + +import type { ComponentProps } from "react"; +import { flushSync } from "react-dom"; +import { createRoot, type Root } from "react-dom/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { Agent, AgentInstructionsBundle, AgentInstructionsFileDetail, AgentInstructionsFileSummary } from "@paperclipai/shared"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { PromptsTab } from "./AgentDetail"; + +const mockAgentsApi = vi.hoisted(() => ({ + instructionsBundle: vi.fn(), + instructionsFile: vi.fn(), + updateInstructionsBundle: vi.fn(), + saveInstructionsFile: vi.fn(), + deleteInstructionsFile: vi.fn(), +})); + +const markdownEditorRenderMock = vi.hoisted(() => vi.fn()); + +vi.mock("../api/agents", () => ({ + agentsApi: mockAgentsApi, +})); + +vi.mock("../api/assets", () => ({ + assetsApi: { + uploadImage: vi.fn(async () => ({ contentPath: "/assets/uploaded-image.png" })), + }, +})); + +vi.mock("../context/CompanyContext", () => ({ + useCompany: () => ({ selectedCompanyId: "company-1" }), +})); + +vi.mock("../context/SidebarContext", () => ({ + useSidebar: () => ({ isMobile: false }), +})); + +vi.mock("@/adapters/use-adapter-capabilities", () => ({ + useAdapterCapabilities: () => () => ({ + supportsInstructionsBundle: true, + supportsSkills: true, + supportsLocalAgentJwt: true, + requiresMaterializedRuntimeSkills: false, + supportsModelProfiles: true, + }), +})); + +vi.mock("../components/MarkdownEditor", () => ({ + MarkdownEditor: ({ + value, + onChange, + placeholder, + contentClassName, + imageUploadHandler, + }: { + value: string; + onChange: (value: string) => void; + placeholder?: string; + contentClassName?: string; + imageUploadHandler?: (file: File) => Promise; + }) => { + markdownEditorRenderMock({ + value, + contentClassName, + hasImageUploadHandler: Boolean(imageUploadHandler), + }); + return ( +