424e81d087
## Thinking Path > - Paperclip is a control plane operators use repeatedly to supervise agent companies. > - Common operator workflows depend on fast scanning of inboxes, issue sidebars, workspaces, cost totals, and runtime services. > - Several small UI and service gaps made those workflows slower or less clear. > - This pull request groups the operator-facing QoL changes that can stand alone from recovery and adapter work. > - The benefit is a denser, clearer board experience for issue triage and workspace operation. ## What Changed - Added inbox assignee/project grouping and issue list token/runtime totals. - Improved issue properties with removable blocker chips and workspace task links. - Improved execution workspace layout, runtime controls, issues tab default, and stopped-port reuse behavior. - Added mobile markdown/routine dialog fixes, page title company names, sidebar polish, and dashboard run task label cleanup. ## Verification - `pnpm install --frozen-lockfile` - `pnpm exec vitest run ui/src/lib/inbox.test.ts ui/src/components/IssueProperties.test.tsx ui/src/components/WorkspaceRuntimeControls.test.tsx server/src/__tests__/workspace-runtime.test.ts server/src/__tests__/costs-service.test.ts` ## Risks - Medium UI risk because this touches several operator surfaces. The branch is intentionally grouped around workflow/QoL files and keeps the file count below the Greptile limit. ## Model Used - OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with shell/git/GitHub CLI tool use. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from "react";
|
|
|
|
export interface Breadcrumb {
|
|
label: string;
|
|
href?: string;
|
|
}
|
|
|
|
interface BreadcrumbContextValue {
|
|
breadcrumbs: Breadcrumb[];
|
|
setBreadcrumbs: (crumbs: Breadcrumb[]) => void;
|
|
mobileToolbar: ReactNode | null;
|
|
setMobileToolbar: (node: ReactNode | null) => void;
|
|
}
|
|
|
|
interface BreadcrumbProviderProps {
|
|
children: ReactNode;
|
|
companyName?: string | null;
|
|
}
|
|
|
|
const BreadcrumbContext = createContext<BreadcrumbContextValue | null>(null);
|
|
|
|
function breadcrumbsEqual(left: Breadcrumb[], right: Breadcrumb[]) {
|
|
if (left === right) return true;
|
|
if (left.length !== right.length) return false;
|
|
for (let index = 0; index < left.length; index += 1) {
|
|
if (left[index]?.label !== right[index]?.label || left[index]?.href !== right[index]?.href) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function buildDocumentTitle(breadcrumbs: Breadcrumb[], companyName?: string | null) {
|
|
const pageParts = breadcrumbs.length === 0
|
|
? []
|
|
: [...breadcrumbs].reverse().map((breadcrumb) => breadcrumb.label);
|
|
const companyPart = companyName?.trim() ? [companyName.trim()] : [];
|
|
const parts = [...pageParts, ...companyPart, "Paperclip"];
|
|
return parts.join(" • ");
|
|
}
|
|
|
|
export function BreadcrumbProvider({ children, companyName }: BreadcrumbProviderProps) {
|
|
const [breadcrumbs, setBreadcrumbsState] = useState<Breadcrumb[]>([]);
|
|
const [mobileToolbar, setMobileToolbarState] = useState<ReactNode | null>(null);
|
|
|
|
const setBreadcrumbs = useCallback((crumbs: Breadcrumb[]) => {
|
|
setBreadcrumbsState((current) => (breadcrumbsEqual(current, crumbs) ? current : crumbs));
|
|
}, []);
|
|
|
|
const setMobileToolbar = useCallback((node: ReactNode | null) => {
|
|
setMobileToolbarState(node);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
document.title = buildDocumentTitle(breadcrumbs, companyName);
|
|
}, [breadcrumbs, companyName]);
|
|
|
|
return (
|
|
<BreadcrumbContext.Provider value={{ breadcrumbs, setBreadcrumbs, mobileToolbar, setMobileToolbar }}>
|
|
{children}
|
|
</BreadcrumbContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useBreadcrumbs() {
|
|
const ctx = useContext(BreadcrumbContext);
|
|
if (!ctx) {
|
|
throw new Error("useBreadcrumbs must be used within BreadcrumbProvider");
|
|
}
|
|
return ctx;
|
|
}
|