From d6024b3ca5343d6a1aeb1abc776846f0dcee8ba4 Mon Sep 17 00:00:00 2001 From: Forgotten Date: Wed, 18 Feb 2026 13:02:23 -0600 Subject: [PATCH] Enhance UI: favicon, AgentDetail overhaul, PageTabBar, and config form Add favicon and web manifest branding assets. Major AgentDetail page rework with tabbed sections, run history, and live status. Add PageTabBar component for consistent page-level tabs. Expand AgentConfigForm with more adapter fields. Improve NewAgentDialog, OnboardingWizard, and Issues page layouts. Co-Authored-By: Claude Opus 4.6 --- ui/index.html | 7 + ui/public/android-chrome-192x192.png | Bin 0 -> 8982 bytes ui/public/android-chrome-512x512.png | Bin 0 -> 29374 bytes ui/public/apple-touch-icon.png | Bin 0 -> 8062 bytes ui/public/favicon-16x16.png | Bin 0 -> 669 bytes ui/public/favicon-32x32.png | Bin 0 -> 1609 bytes ui/public/favicon.ico | Bin 0 -> 15086 bytes ui/public/favicon.svg | 9 + ui/public/site.webmanifest | 19 + ui/src/App.tsx | 1 + ui/src/components/AgentConfigForm.tsx | 180 +++- ui/src/components/NewAgentDialog.tsx | 30 +- ui/src/components/OnboardingWizard.tsx | 26 +- ui/src/components/PageTabBar.tsx | 19 + ui/src/components/agent-config-primitives.tsx | 5 +- ui/src/pages/AgentDetail.tsx | 810 +++++++++++++++--- ui/src/pages/Issues.tsx | 36 +- 17 files changed, 982 insertions(+), 160 deletions(-) create mode 100644 ui/public/android-chrome-192x192.png create mode 100644 ui/public/android-chrome-512x512.png create mode 100644 ui/public/apple-touch-icon.png create mode 100644 ui/public/favicon-16x16.png create mode 100644 ui/public/favicon-32x32.png create mode 100644 ui/public/favicon.ico create mode 100644 ui/public/favicon.svg create mode 100644 ui/public/site.webmanifest create mode 100644 ui/src/components/PageTabBar.tsx diff --git a/ui/index.html b/ui/index.html index 8b5d8752..8f0a47e3 100644 --- a/ui/index.html +++ b/ui/index.html @@ -3,7 +3,14 @@ + Paperclip + + + + + +
diff --git a/ui/public/android-chrome-192x192.png b/ui/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..32120f8a74b233aed7504f6ef20a93e5207b82a1 GIT binary patch literal 8982 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 literal 0 HcmV?d00001 diff --git a/ui/public/android-chrome-512x512.png b/ui/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..a7493ed46e367723edc6daebe3b50014677bf07c GIT binary patch literal 29374 zcmeAS@N?(olHy`uVBq!ia0y~yU}6Aa4iHr><-C@Gfq^Z_+ueoXKL{?^yL>VOg93x6 zi(^Q|oHuvV9fH$uANVLN)FyCKp<|JT(xU^sTluzJ3*CAnOE)w-JJjQlTvLM)3yY$I zf^c;7>ae$yx=OFC+Tj>I^^uD!t6&O8R)Cw+J^q)s?Q&&zPA!-#{qFwt@3klPYh9mO zTvWt&_}H;NC5|SAU2?K77@8C~nAjp2SR4gnMFy7QwrWt|SesxVF%1mV)YO(OTc)O_ zcI?CPRWz>s1P`26^IzqFK;oL$Y27Z(?|^UH^Y zhMs&{^y0sWQ}hWY1&%i_U%dG8=H}cU+>c<|o6d)KaAI|c%7wlCr*$LOtJwQAKG z5ZF`t`r6v)?Rj^0T)c4M!!P@p497QY+}NC_z`@kp*VotFeE7mb=XM@Rqk;zq>i+(E z`TBKv%7JEf{=9p8Dt~@@`smT4ZgKr<*RGX4SuOmwRfnPJ0CV?IDVvH7vuEG_@bGYK zOw5fOvyzMVSFKw0@5kf*`*pw9ZrHG4(IO_R_6y*^&o(nNdwjgVyu4iAw#sB}+rQ6u z_EvA-uwlaj5P0zN^70FoN4h7lItY}cq^Fw~K02~8csZZ6STniCdS}+;PTb0wU2wv)6UL%`tadJz3J~ROpejBFMD(2y#4>0XJ;g*da=%R2AR1a zCpTAIC*r}&mzg;^Ja;b@dU<=7zqzro{QbRU%hX;>)9ck?XlgLbxS*h~zdrNwveHM} z|LChtKmGm9&CR*Dx1BoW^x ztlY9iWY^NuirftfRvgU-XBZ?N>Jrsvtvmqo2%|WZ_3G(@bz)6c`uz#Fe!BO z8Kj;P$FrHFH^(yL!UAq_JrgrCr{^VRPdFMBZuNF{dOA29m}6ONu(z6j z?#ZV`6Ft5h=C|Lnb*tm{5;?G%_T9U82WZGx7OBkqcWc4&<>qyNer!xWKF_-R-NnW3 z7cA$pL^7~^-1X(_*XE|CLx&Fa^zisfjvlT3^>X!N=gyrsZ{PaQw_6*av7q>avlxTpf{!0QoH%#x-NnW1B^)2s-QACK z3aibrsSMJa?z}yrO=hnU3zNS7`YBUHoSmIt2t1N!OQ=nF-1aO&2&O zpXA}?jop~UYWdRNzpbqe?{DK%jO#m?u;=43>0iHorJtMAn0Jr?6t+sw zo;?%lY?%{t?DU6+htHlp>p$0OY0%0A(HWrRUZKa$&E4r@lyHFIg#gpc?r!h*_xAS7 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 literal 0 HcmV?d00001 diff --git a/ui/public/apple-touch-icon.png b/ui/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0104f97e5cf414f2a0dc6816c83eceffabb16999 GIT binary patch literal 8062 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 literal 0 HcmV?d00001 diff --git a/ui/public/favicon-16x16.png b/ui/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..a8884bff578f90b057a422de2bf0a088e09c5cc6 GIT binary patch literal 669 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7*pj^6T^Rm@;DWu&Co?cG z1$nwShFJ9WPV)5#DHJ(c|NZ~HGa8TG4Vm|SWZfnZ?x>qJU3X2-qKrUCL9rxhaY2r0 zBDx{J62x-7qCC78dMN%<((>d`y7fd!@RrKe8jZ=_F0$$Os<$7tSTyxoah`{J#lpFi8Dhkbt%pK|Q^aNbK_$EF<)Nhr8oQUz4;zj?lbW<%Y3Z;zd5yXF~h$6_gi&V zbAI9cP*P=8D;B`Kz-uyxJfnZ;t5`q5sp-`J zfAhjvq@<;li;KI>U0<*$oqoDUtNigtUt3|}Z1ew$0d22$8J;LI*|Fd0)mzVeJ)vS;0E-7m})`d-V zIASHiqR!8BGTZAw;hkv5eLQR4J+RZ6RHMYvv^<`h>5;`fbBBCe-#eNUQ%pFTmfw=S zw%So&qGGHj&!;R^c+ks!c0xuS*IS%z3nT)uy{MmWSMwXj>Z3%)?ieFSKz! zLM|U%-sjIJE^+%ESI(M8X<8jSUxb09bwV5$So@J>#GY`+6g#yPvN*E;l@sF66r!)1`@}NgY^_jj)#QPrkb>hmE z`|(-FD~`@Km|woV>qPSsEmM~##}qVdCkYj^9juah@|HpAPhR4}ve2bLGZ{5Ry5@&I zeXVx2X;s~hl~KQ6R0xW49hG({ll;K4LzCz9eddDLa)+x`GEZdH{(LXo!!hrC^?b%z zt8_a~JTuUF-?}Bm)^@er%eS$mnOm(spYG$b=qopTG1&o=2$nJu#Mp7gc?4+`nJX_d<@D z@5S=j4-Owboc(A9v+jfXm#uM1eg91CL))sH%%JNM)sPxdXipx&(TLwbv8 zqS@`uODpGud_Ab~NuJ@wYwP^4e`<2=H<+cK-#qQ;)W?q>2d{kdE-du#Hb(!3wSghG z=B|3g%Nj*X1A|~R`+~qW%@8D>v31n&OiMn z&yPw+%`y$V>fe_5GqE9QNQrcE;%O?>Rc7+Ha1<$Mqr# zf0{U^&wTDaIVI6qqHS{8bNS$>yVd%;Ht2T!iJefVXeuQW@bqq4wg38viL$x&UAn3p z&c}x|rOz%7U1xr|(_))hh?a1mR_;fu2Z@&^FIljiwei2Khj-Ze>))8gO+Tj^#oW9T z;?AUW=|0o1^G5{=9RsKC57Scr8zP!Nc~#y1opCMa*&F}sdc3c@z3wklMZf2VIURW# zQ%~tyT5BojH<^h_M5!!pcirLRa$TJ3sCB4d=w9IyvpTGs4&3K7u&}r>`((*m=6l)G z#p$-<{=J3(cuxX1eFkez#457;g_W3=jy`!BaQMuqWQ&i}I+*1i6gViar7 zelLBRo&51{zcjkk;y7zK8&94*skPz!jMqh$?_(K+h5tD|a0`ukZO)q0x#&|kqs{r~ zm0Hz}rN`>eY*>)4_KnWBeP`5pWllblc>0oe&Hee!A@_33sudj;U#w8;S`^tZ zapJ^xS*3aUCsug=Y)VyqU2|XFUiQan4u(e-N9Qw1Glu-kHPfE$y)_ySOl8k3G*};nR%oT^b%fd}#XlGj~1Ptm8#oH(0On=SKHOPh0wRSD(YC$f>hk zB9b5ZsdavB?ChL;d^-2Ni405Pw<&$gOZaPa*+%sE%(JIgR{nnR!)o5#xpUWsrI&sG zwr&<{rSaVQoeaku_Y1W>^*?)duSxB)2OIXpZ|-?qvG{k e&p*DOF+Q+4dF}IUXBZe57(8A5T-G@yGywo>2L&bo literal 0 HcmV?d00001 diff --git a/ui/public/favicon.ico b/ui/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0b205ec2c057c83d4220b1235bba9405a53e1f12 GIT binary patch literal 15086 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 + + + diff --git a/ui/public/site.webmanifest b/ui/public/site.webmanifest new file mode 100644 index 00000000..8861bd64 --- /dev/null +++ b/ui/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Paperclip", + "short_name": "Paperclip", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#18181b", + "background_color": "#18181b", + "display": "standalone" +} diff --git a/ui/src/App.tsx b/ui/src/App.tsx index f0876ccc..01b26557 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -27,6 +27,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/ui/src/components/AgentConfigForm.tsx b/ui/src/components/AgentConfigForm.tsx index 0f94586f..ee941215 100644 --- a/ui/src/components/AgentConfigForm.tsx +++ b/ui/src/components/AgentConfigForm.tsx @@ -37,6 +37,8 @@ export interface CreateConfigValues { dangerouslyBypassSandbox: boolean; command: string; args: string; + extraArgs: string; + envVars: string; url: string; bootstrapPrompt: string; maxTurnsPerRun: number; @@ -54,6 +56,8 @@ export const defaultCreateValues: CreateConfigValues = { dangerouslyBypassSandbox: false, command: "", args: "", + extraArgs: "", + envVars: "", url: "", bootstrapPrompt: "", maxTurnsPerRun: 80, @@ -65,6 +69,10 @@ export const defaultCreateValues: CreateConfigValues = { type AgentConfigFormProps = { adapterModels?: AdapterModel[]; + onDirtyChange?: (dirty: boolean) => void; + onSaveActionChange?: (save: (() => void) | null) => void; + onCancelActionChange?: (cancel: (() => void) | null) => void; + hideInlineSave?: boolean; } & ( | { mode: "create"; @@ -110,6 +118,51 @@ function isOverlayDirty(o: Overlay): boolean { const inputClass = "w-full rounded-md border border-border px-2.5 py-1.5 bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40"; +function parseCommaArgs(value: string): string[] { + return value + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + +function formatArgList(value: unknown): string { + if (Array.isArray(value)) { + return value + .filter((item): item is string => typeof item === "string") + .join(", "); + } + return typeof value === "string" ? value : ""; +} + +function parseEnvVars(text: string): Record { + const env: Record = {}; + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq <= 0) continue; + const key = trimmed.slice(0, eq).trim(); + const value = trimmed.slice(eq + 1); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; + env[key] = value; + } + return env; +} + +function formatEnvVars(value: unknown): string { + if (typeof value !== "object" || value === null || Array.isArray(value)) return ""; + return Object.entries(value as Record) + .filter(([, v]) => typeof v === "string") + .map(([k, v]) => `${k}=${String(v)}`) + .join("\n"); +} + +function extractPickedDirectoryPath(handle: unknown): string | null { + if (typeof handle !== "object" || handle === null) return null; + const maybePath = (handle as { path?: unknown }).path; + return typeof maybePath === "string" && maybePath.length > 0 ? maybePath : null; +} + /* ---- Form ---- */ export function AgentConfigForm(props: AgentConfigFormProps) { @@ -175,6 +228,20 @@ export function AgentConfigForm(props: AgentConfigFormProps) { props.onSave(patch); } + useEffect(() => { + if (!isCreate) { + props.onDirtyChange?.(isDirty); + props.onSaveActionChange?.(() => handleSave()); + props.onCancelActionChange?.(() => setOverlay({ ...emptyOverlay })); + return () => { + props.onSaveActionChange?.(null); + props.onCancelActionChange?.(null); + props.onDirtyChange?.(false); + }; + } + return; + }, [isCreate, isDirty, props.onDirtyChange, props.onSaveActionChange, props.onCancelActionChange, overlay]); // eslint-disable-line react-hooks/exhaustive-deps + // ---- Resolve values ---- const config = !isCreate ? ((props.agent.adapterConfig ?? {}) as Record) : {}; const runtimeConfig = !isCreate ? ((props.agent.runtimeConfig ?? {}) as Record) : {}; @@ -195,6 +262,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { // Section toggle state — advanced always starts collapsed const [adapterAdvancedOpen, setAdapterAdvancedOpen] = useState(false); const [heartbeatOpen, setHeartbeatOpen] = useState(!isCreate); + const [cwdPickerNotice, setCwdPickerNotice] = useState(null); // Popover states const [modelOpen, setModelOpen] = useState(false); @@ -213,7 +281,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { return (
{/* ---- Floating Save button (edit mode, when dirty) ---- */} - {isDirty && ( + {isDirty && !props.hideInlineSave && (
Unsaved changes @@ -237,6 +305,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { mark("identity", "name", v)} + immediate className={inputClass} placeholder="Agent name" /> @@ -245,6 +314,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { mark("identity", "title", v || null)} + immediate className={inputClass} placeholder="e.g. VP of Engineering" /> @@ -253,6 +323,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { mark("identity", "capabilities", v || null)} + immediate placeholder="Describe what this agent can do..." minRows={2} /> @@ -303,7 +374,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { ? set!({ cwd: v }) : mark("adapterConfig", "cwd", v || undefined) } - immediate={isCreate} + immediate className="w-full bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/40" placeholder="/path/to/project" /> @@ -312,10 +383,24 @@ export function AgentConfigForm(props: AgentConfigFormProps) { className="inline-flex items-center rounded-md border border-border px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent/50 transition-colors shrink-0" onClick={async () => { try { + setCwdPickerNotice(null); // @ts-expect-error -- showDirectoryPicker is not in all TS lib defs yet const handle = await window.showDirectoryPicker({ mode: "read" }); - if (isCreate) set!({ cwd: handle.name }); - else mark("adapterConfig", "cwd", handle.name); + const absolutePath = extractPickedDirectoryPath(handle); + if (absolutePath) { + if (isCreate) set!({ cwd: absolutePath }); + else mark("adapterConfig", "cwd", absolutePath); + return; + } + const selectedName = + typeof handle === "object" && + handle !== null && + typeof (handle as { name?: unknown }).name === "string" + ? String((handle as { name: string }).name) + : "selected folder"; + setCwdPickerNotice( + `Directory picker only exposed "${selectedName}". Paste the absolute path manually.`, + ); } catch { // user cancelled or API unsupported } @@ -324,6 +409,9 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Choose
+ {cwdPickerNotice && ( +

{cwdPickerNotice}

+ )} )} @@ -347,6 +435,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { onCommit={(v) => mark("adapterConfig", "promptTemplate", v || undefined) } + immediate placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..." minRows={4} /> @@ -429,7 +518,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { ? set!({ command: v }) : mark("adapterConfig", "command", v || undefined) } - immediate={isCreate} + immediate className={inputClass} placeholder="e.g. node, python" /> @@ -439,7 +528,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { value={ isCreate ? val!.args - : eff("adapterConfig", "args", String(config.args ?? "")) + : eff("adapterConfig", "args", formatArgList(config.args)) } onCommit={(v) => isCreate @@ -447,15 +536,10 @@ export function AgentConfigForm(props: AgentConfigFormProps) { : mark( "adapterConfig", "args", - v - ? v - .split(",") - .map((a) => a.trim()) - .filter(Boolean) - : undefined, + v ? parseCommaArgs(v) : undefined, ) } - immediate={isCreate} + immediate className={inputClass} placeholder="e.g. script.js, --flag" /> @@ -477,7 +561,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { ? set!({ url: v }) : mark("adapterConfig", "url", v || undefined) } - immediate={isCreate} + immediate className={inputClass} placeholder="https://..." /> @@ -492,6 +576,24 @@ export function AgentConfigForm(props: AgentConfigFormProps) { onToggle={() => setAdapterAdvancedOpen(!adapterAdvancedOpen)} >
+ + + isCreate + ? set!({ command: v }) + : mark("adapterConfig", "command", v || undefined) + } + immediate + className={inputClass} + placeholder={adapterType === "codex_local" ? "codex" : "claude"} + /> + + mark("adapterConfig", "bootstrapPromptTemplate", v || undefined) } + immediate placeholder="Optional initial setup prompt for the first run" minRows={2} /> @@ -543,12 +646,57 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Number(config.maxTurnsPerRun ?? 80), )} onCommit={(v) => mark("adapterConfig", "maxTurnsPerRun", v || 80)} + immediate className={inputClass} /> )} )} + + + isCreate + ? set!({ extraArgs: v }) + : mark("adapterConfig", "extraArgs", v ? parseCommaArgs(v) : undefined) + } + immediate + className={inputClass} + placeholder="e.g. --verbose, --foo=bar" + /> + + + + {isCreate ? ( + set!({ envVars: v })} + minRows={3} + /> + ) : ( + { + const parsed = parseEnvVars(v); + mark( + "adapterConfig", + "env", + Object.keys(parsed).length > 0 ? parsed : undefined, + ); + }} + immediate + placeholder={"ANTHROPIC_API_KEY=...\nPAPERCLIP_API_URL=http://localhost:3100"} + minRows={3} + /> + )} + + {/* Edit-only: timeout + grace period */} {!isCreate && ( <> @@ -560,6 +708,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Number(config.timeoutSec ?? 0), )} onCommit={(v) => mark("adapterConfig", "timeoutSec", v)} + immediate className={inputClass} /> @@ -571,6 +720,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Number(config.graceSec ?? 15), )} onCommit={(v) => mark("adapterConfig", "graceSec", v)} + immediate className={inputClass} /> @@ -669,6 +819,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { Number(heartbeat.cooldownSec ?? 10), )} onCommit={(v) => mark("heartbeat", "cooldownSec", v)} + immediate className={inputClass} /> @@ -695,6 +846,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) { props.agent.budgetMonthlyCents, )} onCommit={(v) => mark("runtime", "budgetMonthlyCents", v)} + immediate className={inputClass} /> diff --git a/ui/src/components/NewAgentDialog.tsx b/ui/src/components/NewAgentDialog.tsx index 23a23b60..5cce8abc 100644 --- a/ui/src/components/NewAgentDialog.tsx +++ b/ui/src/components/NewAgentDialog.tsx @@ -30,6 +30,28 @@ import { type CreateConfigValues, } from "./AgentConfigForm"; +function parseCommaArgs(value: string): string[] { + return value + .split(",") + .map((item) => item.trim()) + .filter(Boolean); +} + +function parseEnvVars(text: string): Record { + const env: Record = {}; + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) continue; + const eq = trimmed.indexOf("="); + if (eq <= 0) continue; + const key = trimmed.slice(0, eq).trim(); + const valueAtKey = trimmed.slice(eq + 1); + if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue; + env[key] = valueAtKey; + } + return env; +} + export function NewAgentDialog() { const { newAgentOpen, closeNewAgent } = useDialog(); const { selectedCompanyId, selectedCompany } = useCompany(); @@ -102,16 +124,22 @@ export function NewAgentDialog() { if (v.model) ac.model = v.model; ac.timeoutSec = 0; ac.graceSec = 15; + const env = parseEnvVars(v.envVars); + if (Object.keys(env).length > 0) ac.env = env; if (v.adapterType === "claude_local") { ac.maxTurnsPerRun = v.maxTurnsPerRun; ac.dangerouslySkipPermissions = v.dangerouslySkipPermissions; + if (v.command) ac.command = v.command; + if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs); } else if (v.adapterType === "codex_local") { ac.search = v.search; ac.dangerouslyBypassApprovalsAndSandbox = v.dangerouslyBypassSandbox; + if (v.command) ac.command = v.command; + if (v.extraArgs) ac.extraArgs = parseCommaArgs(v.extraArgs); } else if (v.adapterType === "process") { if (v.command) ac.command = v.command; - if (v.args) ac.args = v.args.split(",").map((a) => a.trim()).filter(Boolean); + if (v.args) ac.args = parseCommaArgs(v.args); } else if (v.adapterType === "http") { if (v.url) ac.url = v.url; } diff --git a/ui/src/components/OnboardingWizard.tsx b/ui/src/components/OnboardingWizard.tsx index 4ab5f953..91015774 100644 --- a/ui/src/components/OnboardingWizard.tsx +++ b/ui/src/components/OnboardingWizard.tsx @@ -58,6 +58,7 @@ export function OnboardingWizard() { const [command, setCommand] = useState(""); const [args, setArgs] = useState(""); const [url, setUrl] = useState(""); + const [cwdPickerNotice, setCwdPickerNotice] = useState(null); // Step 3 const [taskTitle, setTaskTitle] = useState("Create your CEO HEARTBEAT.md"); @@ -88,6 +89,7 @@ export function OnboardingWizard() { setCommand(""); setArgs(""); setUrl(""); + setCwdPickerNotice(null); setTaskTitle("Create your CEO HEARTBEAT.md"); setTaskDescription("You're the CEO of the company, make sure you have a file agents/ceo/HEARTBEAT.md that tells you your core loop. You MUST use the Paperclip SKILL."); setCreatedCompanyId(null); @@ -406,9 +408,28 @@ export function OnboardingWizard() { className="inline-flex items-center rounded-md border border-border px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent/50 transition-colors shrink-0" onClick={async () => { try { + setCwdPickerNotice(null); // @ts-expect-error -- showDirectoryPicker is not in all TS lib defs yet const handle = await window.showDirectoryPicker({ mode: "read" }); - setCwd(handle.name); + const pickedPath = + typeof handle === "object" && + handle !== null && + typeof (handle as { path?: unknown }).path === "string" + ? String((handle as { path: string }).path) + : ""; + if (pickedPath) { + setCwd(pickedPath); + return; + } + const selectedName = + typeof handle === "object" && + handle !== null && + typeof (handle as { name?: unknown }).name === "string" + ? String((handle as { name: string }).name) + : "selected folder"; + setCwdPickerNotice( + `Directory picker only exposed "${selectedName}". Paste the absolute path manually.`, + ); } catch { // user cancelled or API unsupported } @@ -417,6 +438,9 @@ export function OnboardingWizard() { Choose
+ {cwdPickerNotice && ( +

{cwdPickerNotice}

+ )}
+ - - {agent.status === "active" || agent.status === "running" ? ( - - ) : ( + {agent.status === "paused" ? ( + ) : ( + )} @@ -247,15 +439,43 @@ export function AgentDetail() { {actionError &&

{actionError}

} - - - Overview - Configuration - Runs{heartbeats ? ` (${heartbeats.length})` : ""} - Issues ({assignedIssues.length}) - Costs - API Keys - + +
+ +
+ + +
+
{/* OVERVIEW TAB */} @@ -354,12 +574,18 @@ export function AgentDetail() { {/* CONFIGURATION TAB */} - + {/* RUNS TAB */} - + {/* ISSUES TAB */} @@ -408,7 +634,19 @@ function SummaryRow({ label, children }: { label: string; children: React.ReactN /* ---- Configuration Tab ---- */ -function ConfigurationTab({ agent }: { agent: Agent }) { +function ConfigurationTab({ + agent, + onDirtyChange, + onSaveActionChange, + onCancelActionChange, + onSavingChange, +}: { + agent: Agent; + onDirtyChange: (dirty: boolean) => void; + onSaveActionChange: (save: (() => void) | null) => void; + onCancelActionChange: (cancel: (() => void) | null) => void; + onSavingChange: (saving: boolean) => void; +}) { const queryClient = useQueryClient(); const { data: adapterModels } = useQuery({ @@ -423,6 +661,10 @@ function ConfigurationTab({ agent }: { agent: Agent }) { }, }); + useEffect(() => { + onSavingChange(updateAgent.isPending); + }, [onSavingChange, updateAgent.isPending]); + return (
updateAgent.mutate(patch)} isSaving={updateAgent.isPending} adapterModels={adapterModels} + onDirtyChange={onDirtyChange} + onSaveActionChange={onSaveActionChange} + onCancelActionChange={onCancelActionChange} + hideInlineSave />
); @@ -438,8 +684,8 @@ function ConfigurationTab({ agent }: { agent: Agent }) { /* ---- Runs Tab ---- */ -function RunsTab({ runs, companyId }: { runs: HeartbeatRun[]; companyId: string }) { - const [expandedRunId, setExpandedRunId] = useState(null); +function RunsTab({ runs, companyId, agentId, selectedRunId }: { runs: HeartbeatRun[]; companyId: string; agentId: string; selectedRunId: string | null }) { + const navigate = useNavigate(); if (runs.length === 0) { return

No runs yet.

; @@ -450,61 +696,75 @@ function RunsTab({ runs, companyId }: { runs: HeartbeatRun[]; companyId: string (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); - return ( -
- {sorted.map((run) => { - const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" }; - const StatusIcon = statusInfo.icon; - const isExpanded = expandedRunId === run.id; - const usage = run.usageJson as Record | null; - const totalTokens = usage - ? (Number(usage.input_tokens ?? 0) + Number(usage.output_tokens ?? 0)) - : 0; - const cost = usage ? Number(usage.cost_usd ?? usage.total_cost_usd ?? 0) : 0; - const summary = run.resultJson - ? String((run.resultJson as Record).summary ?? (run.resultJson as Record).result ?? "") - : run.error ?? ""; + // Auto-select latest run when no run is selected + const effectiveRunId = selectedRunId ?? sorted[0]?.id ?? null; + const selectedRun = sorted.find((r) => r.id === effectiveRunId) ?? null; - return ( -
+ return ( +
+ {/* Left: run list */} +
+ {sorted.map((run) => { + const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" }; + const StatusIcon = statusInfo.icon; + const isSelected = run.id === effectiveRunId; + const metrics = runMetrics(run); + const summary = run.resultJson + ? String((run.resultJson as Record).summary ?? (run.resultJson as Record).result ?? "") + : run.error ?? ""; + + return ( + ); + })} +
- {isExpanded && } -
- ); - })} + {/* Right: run detail */} + {selectedRun && ( +
+ +
+ )}
); } @@ -513,7 +773,7 @@ function RunsTab({ runs, companyId }: { runs: HeartbeatRun[]; companyId: string function RunDetail({ run }: { run: HeartbeatRun }) { const queryClient = useQueryClient(); - const usage = run.usageJson as Record | null; + const metrics = runMetrics(run); const cancelRun = useMutation({ mutationFn: () => heartbeatsApi.cancel(run.id), @@ -523,9 +783,9 @@ function RunDetail({ run }: { run: HeartbeatRun }) { }); return ( -
+
{/* Status timeline */} -
+
Status: @@ -551,26 +811,26 @@ function RunDetail({ run }: { run: HeartbeatRun }) {
{/* Token breakdown */} - {usage && ( + {(metrics.input > 0 || metrics.output > 0 || metrics.cached > 0 || metrics.cost > 0) && (
Input: - {formatTokens(Number(usage.input_tokens ?? 0))} + {formatTokens(metrics.input)}
Output: - {formatTokens(Number(usage.output_tokens ?? 0))} + {formatTokens(metrics.output)}
- {Number(usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? 0) > 0 && ( + {metrics.cached > 0 && (
Cached: - {formatTokens(Number(usage.cached_input_tokens ?? usage.cache_read_input_tokens ?? 0))} + {formatTokens(metrics.cached)}
)} - {Number(usage.cost_usd ?? usage.total_cost_usd ?? 0) > 0 && ( + {metrics.cost > 0 && (
Cost: - ${Number(usage.cost_usd ?? usage.total_cost_usd ?? 0).toFixed(4)} + ${metrics.cost.toFixed(4)}
)}
@@ -582,13 +842,25 @@ function RunDetail({ run }: { run: HeartbeatRun }) { {run.sessionIdBefore && (
Session before: - {run.sessionIdBefore.slice(0, 16)}... +
)} {run.sessionIdAfter && (
Session after: - {run.sessionIdAfter.slice(0, 16)}... +
)}
@@ -612,6 +884,22 @@ function RunDetail({ run }: { run: HeartbeatRun }) {
)} + {/* stderr excerpt for failed runs */} + {run.stderrExcerpt && ( +
+ stderr +
{run.stderrExcerpt}
+
+ )} + + {/* stdout excerpt when no log is available */} + {run.stdoutExcerpt && !run.logRef && ( +
+ stdout +
{run.stdoutExcerpt}
+
+ )} + {/* Cancel button for running */} {(run.status === "running" || run.status === "queued") && (
); } /* ---- Log Viewer ---- */ -function LogViewer({ runId, status }: { runId: string; status: string }) { +function LogViewer({ run }: { run: HeartbeatRun }) { const [events, setEvents] = useState([]); + const [logLines, setLogLines] = useState>([]); const [loading, setLoading] = useState(true); + const [logLoading, setLogLoading] = useState(!!run.logRef); + const [logError, setLogError] = useState(null); + const [logOffset, setLogOffset] = useState(0); const logEndRef = useRef(null); - const isLive = status === "running" || status === "queued"; + const pendingLogLineRef = useRef(""); + const isLive = run.status === "running" || run.status === "queued"; + + function appendLogContent(content: string, finalize = false) { + if (!content && !finalize) return; + const combined = `${pendingLogLineRef.current}${content}`; + const split = combined.split("\n"); + pendingLogLineRef.current = split.pop() ?? ""; + if (finalize && pendingLogLineRef.current) { + split.push(pendingLogLineRef.current); + pendingLogLineRef.current = ""; + } + + const parsed: Array<{ ts: string; stream: "stdout" | "stderr" | "system"; chunk: string }> = []; + for (const line of split) { + const trimmed = line.trim(); + if (!trimmed) continue; + try { + const raw = JSON.parse(trimmed) as { ts?: unknown; stream?: unknown; chunk?: unknown }; + const stream = + raw.stream === "stderr" || raw.stream === "system" ? raw.stream : "stdout"; + const chunk = typeof raw.chunk === "string" ? raw.chunk : ""; + const ts = typeof raw.ts === "string" ? raw.ts : new Date().toISOString(); + if (!chunk) continue; + parsed.push({ ts, stream, chunk }); + } catch { + // ignore malformed lines + } + } + + if (parsed.length > 0) { + setLogLines((prev) => [...prev, ...parsed]); + } + } // Fetch events const { data: initialEvents } = useQuery({ - queryKey: ["run-events", runId], - queryFn: () => heartbeatsApi.events(runId, 0, 200), + queryKey: ["run-events", run.id], + queryFn: () => heartbeatsApi.events(run.id, 0, 200), }); useEffect(() => { @@ -657,7 +982,56 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { // Auto-scroll useEffect(() => { logEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [events]); + }, [events, logLines]); + + // Fetch persisted shell log + useEffect(() => { + let cancelled = false; + pendingLogLineRef.current = ""; + setLogLines([]); + setLogOffset(0); + setLogError(null); + + if (!run.logRef) { + setLogLoading(false); + return () => { + cancelled = true; + }; + } + + setLogLoading(true); + const firstLimit = + typeof run.logBytes === "number" && run.logBytes > 0 + ? Math.min(Math.max(run.logBytes + 1024, 256_000), 2_000_000) + : 256_000; + + const load = async () => { + try { + let offset = 0; + let first = true; + while (!cancelled) { + const result = await heartbeatsApi.log(run.id, offset, first ? firstLimit : 256_000); + appendLogContent(result.content, result.nextOffset === undefined); + const next = result.nextOffset ?? offset + result.content.length; + setLogOffset(next); + offset = next; + first = false; + if (result.nextOffset === undefined || isLive) break; + } + } catch (err) { + if (!cancelled) { + setLogError(err instanceof Error ? err.message : "Failed to load run log"); + } + } finally { + if (!cancelled) setLogLoading(false); + } + }; + + void load(); + return () => { + cancelled = true; + }; + }, [run.id, run.logRef, run.logBytes, isLive]); // Poll for live updates useEffect(() => { @@ -665,7 +1039,7 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { const interval = setInterval(async () => { const maxSeq = events.length > 0 ? Math.max(...events.map((e) => e.seq)) : 0; try { - const newEvents = await heartbeatsApi.events(runId, maxSeq, 100); + const newEvents = await heartbeatsApi.events(run.id, maxSeq, 100); if (newEvents.length > 0) { setEvents((prev) => [...prev, ...newEvents]); } @@ -674,13 +1048,41 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { } }, 2000); return () => clearInterval(interval); - }, [runId, isLive, events]); + }, [run.id, isLive, events]); - if (loading) { - return

Loading events...

; + // Poll shell log for running runs + useEffect(() => { + if (!isLive || !run.logRef) return; + const interval = setInterval(async () => { + try { + const result = await heartbeatsApi.log(run.id, logOffset, 256_000); + if (result.content) { + appendLogContent(result.content, result.nextOffset === undefined); + } + if (result.nextOffset !== undefined) { + setLogOffset(result.nextOffset); + } else if (result.content.length > 0) { + setLogOffset((prev) => prev + result.content.length); + } + } catch { + // ignore polling errors + } + }, 2000); + return () => clearInterval(interval); + }, [run.id, run.logRef, isLive, logOffset]); + + const adapterInvokePayload = useMemo(() => { + const evt = events.find((e) => e.eventType === "adapter.invoke"); + return asRecord(evt?.payload ?? null); + }, [events]); + + const transcript = useMemo(() => buildTranscript(logLines), [logLines]); + + if (loading && logLoading) { + return

Loading run logs...

; } - if (events.length === 0) { + if (events.length === 0 && logLines.length === 0 && !logError) { return

No log events.

; } @@ -697,9 +1099,62 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { }; return ( -
-
- Events ({events.length}) +
+ {adapterInvokePayload && ( +
+
Invocation
+ {typeof adapterInvokePayload.adapterType === "string" && ( +
Adapter: {adapterInvokePayload.adapterType}
+ )} + {typeof adapterInvokePayload.cwd === "string" && ( +
Working dir: {adapterInvokePayload.cwd}
+ )} + {typeof adapterInvokePayload.command === "string" && ( +
+ Command: + + {[ + adapterInvokePayload.command, + ...(Array.isArray(adapterInvokePayload.commandArgs) + ? adapterInvokePayload.commandArgs.filter((v): v is string => typeof v === "string") + : []), + ].join(" ")} + +
+ )} + {adapterInvokePayload.prompt !== undefined && ( +
+
Prompt
+
+                {typeof adapterInvokePayload.prompt === "string"
+                  ? adapterInvokePayload.prompt
+                  : JSON.stringify(adapterInvokePayload.prompt, null, 2)}
+              
+
+ )} + {adapterInvokePayload.context !== undefined && ( +
+
Context
+
+                {JSON.stringify(adapterInvokePayload.context, null, 2)}
+              
+
+ )} + {adapterInvokePayload.env !== undefined && ( +
+
Environment
+
+                {JSON.stringify(adapterInvokePayload.env, null, 2)}
+              
+
+ )} +
+ )} + +
+ + Transcript ({transcript.length}) + {isLive && ( @@ -711,30 +1166,119 @@ function LogViewer({ runId, status }: { runId: string; status: string }) { )}
- {events.map((evt) => { - const color = evt.color - ?? (evt.level ? levelColors[evt.level] : null) - ?? (evt.stream ? streamColors[evt.stream] : null) - ?? "text-foreground"; + {transcript.length === 0 && !run.logRef && ( +
No persisted transcript for this run.
+ )} + {transcript.map((entry, idx) => { + const time = new Date(entry.ts).toLocaleTimeString("en-US", { hour12: false }); + if (entry.kind === "assistant") { + return ( +
+
+ {time} + assistant + {entry.text} +
+
+ ); + } + if (entry.kind === "tool_call") { + return ( +
+
+ {time} + tool + {entry.name} +
+
+                  {JSON.stringify(entry.input, null, 2)}
+                
+
+ ); + } + + if (entry.kind === "init") { + return ( +
+ {time} + init + Claude initialized (model: {entry.model}{entry.sessionId ? `, session: ${entry.sessionId}` : ""}) +
+ ); + } + + if (entry.kind === "result") { + return ( +
+
+ {time} + result + + tokens in={formatTokens(entry.inputTokens)} out={formatTokens(entry.outputTokens)} cached={formatTokens(entry.cachedTokens)} cost=${entry.costUsd.toFixed(6)} + +
+ {entry.text && ( +
{entry.text}
+ )} +
+ ); + } + + const rawText = entry.text; + const label = + entry.kind === "stderr" ? "stderr" : + entry.kind === "system" ? "system" : + "stdout"; + const color = + entry.kind === "stderr" ? "text-red-300" : + entry.kind === "system" ? "text-blue-300" : + "text-foreground"; return ( -
+
- {new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })} + {time} - {evt.stream && ( - - [{evt.stream}] - - )} - - {evt.message ?? (evt.payload ? JSON.stringify(evt.payload) : "")} + + {label} + + + {rawText}
- ); + ) })} + {logError &&
{logError}
}
+ + {events.length > 0 && ( +
+
Events ({events.length})
+
+ {events.map((evt) => { + const color = evt.color + ?? (evt.level ? levelColors[evt.level] : null) + ?? (evt.stream ? streamColors[evt.stream] : null) + ?? "text-foreground"; + + return ( +
+ + {new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })} + + + {evt.stream ? `[${evt.stream}]` : ""} + + + {evt.message ?? (evt.payload ? JSON.stringify(evt.payload) : "")} + +
+ ); + })} +
+
+ )}
); } diff --git a/ui/src/pages/Issues.tsx b/ui/src/pages/Issues.tsx index ea7b9f3e..2dec2246 100644 --- a/ui/src/pages/Issues.tsx +++ b/ui/src/pages/Issues.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; +import { useEffect } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { issuesApi } from "../api/issues"; import { agentsApi } from "../api/agents"; @@ -12,8 +12,9 @@ import { StatusIcon } from "../components/StatusIcon"; import { PriorityIcon } from "../components/PriorityIcon"; import { EntityRow } from "../components/EntityRow"; import { EmptyState } from "../components/EmptyState"; +import { PageTabBar } from "../components/PageTabBar"; import { Button } from "@/components/ui/button"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tabs } from "@/components/ui/tabs"; import { CircleDot, Plus } from "lucide-react"; import { formatDate } from "../lib/utils"; import type { Issue } from "@paperclip/shared"; @@ -26,6 +27,18 @@ function statusLabel(status: string): string { type TabFilter = "all" | "active" | "backlog" | "done"; +const issueTabItems = [ + { value: "all", label: "All Issues" }, + { value: "active", label: "Active" }, + { value: "backlog", label: "Backlog" }, + { value: "done", label: "Done" }, +] as const; + +function parseIssueTab(value: string | null): TabFilter { + if (value === "active" || value === "backlog" || value === "done") return value; + return "all"; +} + function filterIssues(issues: Issue[], tab: TabFilter): Issue[] { switch (tab) { case "active": @@ -45,7 +58,8 @@ export function Issues() { const { setBreadcrumbs } = useBreadcrumbs(); const navigate = useNavigate(); const queryClient = useQueryClient(); - const [tab, setTab] = useState("all"); + const [searchParams, setSearchParams] = useSearchParams(); + const tab = parseIssueTab(searchParams.get("tab")); const { data: agents } = useQuery({ queryKey: queryKeys.agents.list(selectedCompanyId!), @@ -86,16 +100,18 @@ export function Issues() { .filter((s) => grouped[s]?.length) .map((s) => ({ status: s, items: grouped[s]! })); + const setTab = (nextTab: TabFilter) => { + const next = new URLSearchParams(searchParams); + if (nextTab === "all") next.delete("tab"); + else next.set("tab", nextTab); + setSearchParams(next); + }; + return (
setTab(v as TabFilter)}> - - All Issues - Active - Backlog - Done - +