From 2a15650341ba7a7e22ecbab82d163173a48f02a3 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 17 Mar 2026 10:32:50 -0500 Subject: [PATCH 1/8] feat: reorganize agent detail tabs and add Prompts tab Rearrange tabs to: Dashboard, Prompts, Skills, Configuration, Budget. Move Prompt Template out of Configuration into a dedicated Prompts tab with its own save/cancel flow and dirty tracking. Co-Authored-By: Paperclip --- ui/src/components/AgentConfigForm.tsx | 4 +- ui/src/pages/AgentDetail.tsx | 163 +++++++++++++++++++++++--- 2 files changed, 152 insertions(+), 15 deletions(-) diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index c6e48cd0..78684cd8 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -65,6 +65,8 @@ type AgentConfigFormProps = { onSaveActionChange?: (save: (() => void) | null) => void; onCancelActionChange?: (cancel: (() => void) | null) => void; hideInlineSave?: boolean; + /** Hide the prompt template field from the Identity section (used when it's shown in a separate Prompts tab). */ + hidePromptTemplate?: boolean; /** "cards" renders each section as heading + bordered card (for settings pages). Default: "inline" (border-b dividers). */ sectionLayout?: "inline" | "cards"; } & ( @@ -473,7 +475,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { }} /> - {isLocal && ( + {isLocal && !props.hidePromptTemplate && ( <> ; } const isPendingApproval = agent.status === "pending_approval"; - const showConfigActionBar = activeView === "configuration" && (configDirty || configSaving); + const showConfigActionBar = (activeView === "configuration" || activeView === "prompts") && (configDirty || configSaving); return (
@@ -861,9 +868,9 @@ export function AgentDetail() { )} + {activeView === "prompts" && ( + + )} + {activeView === "configuration" && (

API Keys

@@ -1339,6 +1358,7 @@ function ConfigurationTab({ onCancelActionChange, onSavingChange, updatePermissions, + hidePromptTemplate, }: { agent: Agent; companyId?: string; @@ -1347,6 +1367,7 @@ function ConfigurationTab({ onCancelActionChange: (cancel: (() => void) | null) => void; onSavingChange: (saving: boolean) => void; updatePermissions: { mutate: (canCreate: boolean) => void; isPending: boolean }; + hidePromptTemplate?: boolean; }) { const queryClient = useQueryClient(); const [awaitingRefreshAfterSave, setAwaitingRefreshAfterSave] = useState(false); @@ -1401,6 +1422,7 @@ function ConfigurationTab({ onSaveActionChange={onSaveActionChange} onCancelActionChange={onCancelActionChange} hideInlineSave + hidePromptTemplate={hidePromptTemplate} sectionLayout="cards" /> @@ -1427,6 +1449,119 @@ function ConfigurationTab({ ); } +/* ---- Prompts Tab ---- */ + +function PromptsTab({ + agent, + companyId, + onDirtyChange, + onSaveActionChange, + onCancelActionChange, + onSavingChange, +}: { + agent: Agent; + companyId?: string; + onDirtyChange: (dirty: boolean) => void; + onSaveActionChange: (save: (() => void) | null) => void; + onCancelActionChange: (cancel: (() => void) | null) => void; + onSavingChange: (saving: boolean) => void; +}) { + const queryClient = useQueryClient(); + const { selectedCompanyId } = useCompany(); + const [draft, setDraft] = useState(null); + const [awaitingRefresh, setAwaitingRefresh] = useState(false); + const lastAgentRef = useRef(agent); + + const currentValue = String(agent.adapterConfig?.promptTemplate ?? ""); + const displayValue = draft ?? currentValue; + const isDirty = draft !== null && draft !== currentValue; + + const isLocal = + agent.adapterType === "claude_local" || + agent.adapterType === "codex_local" || + agent.adapterType === "opencode_local" || + agent.adapterType === "pi_local" || + agent.adapterType === "hermes_local" || + agent.adapterType === "cursor"; + + const updateAgent = useMutation({ + mutationFn: (data: Record) => agentsApi.update(agent.id, data, companyId), + onMutate: () => setAwaitingRefresh(true), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) }); + queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.urlKey) }); + }, + onError: () => setAwaitingRefresh(false), + }); + + const uploadMarkdownImage = useMutation({ + mutationFn: async ({ file, namespace }: { file: File; namespace: string }) => { + if (!selectedCompanyId) throw new Error("Select a company to upload images"); + return assetsApi.uploadImage(selectedCompanyId, file, namespace); + }, + }); + + useEffect(() => { + if (awaitingRefresh && agent !== lastAgentRef.current) { + setAwaitingRefresh(false); + setDraft(null); + } + lastAgentRef.current = agent; + }, [agent, awaitingRefresh]); + + const isSaving = updateAgent.isPending || awaitingRefresh; + + useEffect(() => { onSavingChange(isSaving); }, [onSavingChange, isSaving]); + useEffect(() => { onDirtyChange(isDirty); }, [onDirtyChange, isDirty]); + + useEffect(() => { + onSaveActionChange(isDirty ? () => { + updateAgent.mutate({ adapterConfig: { promptTemplate: draft } }); + } : null); + }, [onSaveActionChange, isDirty, draft, updateAgent]); + + useEffect(() => { + onCancelActionChange(isDirty ? () => setDraft(null) : null); + }, [onCancelActionChange, isDirty]); + + if (!isLocal) { + return ( +
+

+ Prompt templates are only available for local adapters. +

+
+ ); + } + + return ( +
+
+

Prompt Template

+
+

+ {help.promptTemplate} +

+ setDraft(v ?? "")} + placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..." + contentClassName="min-h-[88px] text-sm font-mono" + imageUploadHandler={async (file) => { + const namespace = `agents/${agent.id}/prompt-template`; + const asset = await uploadMarkdownImage.mutateAsync({ file, namespace }); + return asset.contentPath; + }} + /> +
+ Prompt template is replayed on every heartbeat. Keep it compact and dynamic to avoid recurring token cost and cache churn. +
+
+
+
+ ); +} + function SkillsTab({ agent }: { agent: Agent }) { const instructionsPath = typeof agent.adapterConfig?.instructionsFilePath === "string" && agent.adapterConfig.instructionsFilePath.trim().length > 0 From cd67bf1d3d39ad44e66be397c202291f0e7c388f Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 17 Mar 2026 11:12:56 -0500 Subject: [PATCH 2/8] Add copy-to-clipboard button on issue detail header Adds a copy icon button to the left of the properties panel toggle on the issue detail page. Clicking it copies a markdown representation of the issue (identifier, title, description) to the clipboard and shows a success toast. The icon briefly switches to a checkmark for visual feedback. Co-Authored-By: Paperclip --- ui/src/pages/IssueDetail.tsx | 49 +++++++++++++++++++++++++++++------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index 0eecd014..8e6c206a 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -9,6 +9,7 @@ import { authApi } from "../api/auth"; import { projectsApi } from "../api/projects"; import { useCompany } from "../context/CompanyContext"; import { usePanel } from "../context/PanelContext"; +import { useToast } from "../context/ToastContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { queryKeys } from "../lib/queryKeys"; import { readIssueDetailBreadcrumb } from "../lib/issueDetailBreadcrumb"; @@ -36,8 +37,10 @@ import { ScrollArea } from "@/components/ui/scroll-area"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Activity as ActivityIcon, + Check, ChevronDown, ChevronRight, + Copy, EyeOff, Hexagon, ListTree, @@ -196,7 +199,9 @@ export function IssueDetail() { const queryClient = useQueryClient(); const navigate = useNavigate(); const location = useLocation(); + const { pushToast } = useToast(); const [moreOpen, setMoreOpen] = useState(false); + const [copied, setCopied] = useState(false); const [mobilePropsOpen, setMobilePropsOpen] = useState(false); const [detailTab, setDetailTab] = useState("comments"); const [secondaryOpen, setSecondaryOpen] = useState({ @@ -585,6 +590,15 @@ export function IssueDetail() { return () => closePanel(); }, [issue]); // eslint-disable-line react-hooks/exhaustive-deps + const copyIssueToClipboard = async () => { + if (!issue) return; + const md = `# ${issue.identifier}: ${issue.title}\n\n${issue.description ?? ""}`; + await navigator.clipboard.writeText(md); + setCopied(true); + pushToast({ title: "Copied to clipboard", tone: "success" }); + setTimeout(() => setCopied(false), 2000); + }; + if (isLoading) return

Loading...

; if (error) return

{error.message}

; if (!issue) return null; @@ -737,17 +751,34 @@ export function IssueDetail() {
)} - +
+ + +
+