mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-19 04:00:38 +09:00
Improve operator workflow QoL (#5291)
## 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>
This commit is contained in:
parent
11ffd6f2c5
commit
424e81d087
47 changed files with 1739 additions and 250 deletions
|
|
@ -10,7 +10,6 @@ import { instanceSettingsApi } from "../api/instanceSettings";
|
|||
import { issuesApi } from "../api/issues";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { resolveIssueFilterWorkspaceId } from "../lib/issue-filters";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { buildCompanyUserInlineOptions, buildCompanyUserLabelMap } from "../lib/company-members";
|
||||
import { useProjectOrder } from "../hooks/useProjectOrder";
|
||||
|
|
@ -32,9 +31,19 @@ import { Identity } from "./Identity";
|
|||
import { IssueReferencePill } from "./IssueReferencePill";
|
||||
import { formatDate, cn, projectUrl } from "../lib/utils";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { User, Hexagon, ArrowUpRight, Tag, Plus, GitBranch, FolderOpen, Check, ExternalLink, Clock } from "lucide-react";
|
||||
import { User, Hexagon, ArrowUpRight, Tag, Plus, GitBranch, FolderOpen, Check, ExternalLink, X, Clock } from "lucide-react";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
|
||||
function TruncatedCopyable({ value, icon: Icon }: { value: string; icon: React.ComponentType<{ className?: string }> }) {
|
||||
|
|
@ -113,10 +122,8 @@ function runningRuntimeServiceWithUrl(
|
|||
return runtimeServices?.find((service) => service.status === "running" && service.url?.trim()) ?? null;
|
||||
}
|
||||
|
||||
function issuesWorkspaceFilterHref(workspaceId: string) {
|
||||
const params = new URLSearchParams();
|
||||
params.append("workspace", workspaceId);
|
||||
return `/issues?${params.toString()}`;
|
||||
function executionWorkspaceIssuesHref(workspaceId: string) {
|
||||
return `/execution-workspaces/${workspaceId}/issues`;
|
||||
}
|
||||
|
||||
function toDateTimeLocalValue(value: string | null | undefined) {
|
||||
|
|
@ -144,6 +151,87 @@ function PropertyRow({ label, children }: { label: string; children: React.React
|
|||
);
|
||||
}
|
||||
|
||||
function RemovableIssueReferencePill({
|
||||
issue,
|
||||
onRemove,
|
||||
}: {
|
||||
issue: NonNullable<Issue["blockedBy"]>[number];
|
||||
onRemove: (issueId: string) => void;
|
||||
}) {
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||
const issueLabel = issue.identifier ?? issue.title;
|
||||
const confirmLabel = issue.identifier ? `${issue.identifier}: ${issue.title}` : issue.title;
|
||||
const content = (
|
||||
<>
|
||||
<StatusIcon status={issue.status} className="h-3 w-3 shrink-0" />
|
||||
<span className="truncate">{issueLabel}</span>
|
||||
</>
|
||||
);
|
||||
const removeLabel = `Remove ${issueLabel} as blocker`;
|
||||
const handleRemove = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setIsConfirmOpen(true);
|
||||
};
|
||||
const confirmRemove = () => {
|
||||
onRemove(issue.id);
|
||||
setIsConfirmOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
data-mention-kind="issue"
|
||||
className={cn(
|
||||
"paperclip-mention-chip paperclip-mention-chip--issue group",
|
||||
"inline-flex items-center gap-1 rounded-full border border-border py-0.5 pl-1 pr-2 text-xs",
|
||||
)}
|
||||
title={issue.title}
|
||||
aria-label={`Issue ${issueLabel}: ${issue.title}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-muted-foreground opacity-0 transition-colors transition-opacity hover:bg-destructive/10 hover:text-destructive focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-[2px] focus-visible:ring-ring group-hover:opacity-100"
|
||||
aria-label={removeLabel}
|
||||
title={removeLabel}
|
||||
onClick={handleRemove}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
{issue.identifier ? (
|
||||
<Link
|
||||
to={`/issues/${issueLabel}`}
|
||||
className="inline-flex min-w-0 items-center gap-1 no-underline hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring"
|
||||
aria-label={`Issue ${issueLabel}: ${issue.title}`}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="inline-flex min-w-0 items-center gap-1">{content}</span>
|
||||
)}
|
||||
</span>
|
||||
<Dialog open={isConfirmOpen} onOpenChange={setIsConfirmOpen}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Remove blocker?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Remove {confirmLabel} as a blocker for this issue.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button type="button" variant="destructive" onClick={confirmRemove}>
|
||||
Remove blocker
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders a Popover on desktop, or an inline collapsible section on mobile (inline mode). */
|
||||
function PropertyPicker({
|
||||
inline,
|
||||
|
|
@ -331,10 +419,10 @@ export function IssueProperties({
|
|||
() => isMainIssueWorkspace({ issue, project: issueProject }),
|
||||
[issue, issueProject],
|
||||
);
|
||||
const workspaceFilterId = useMemo(() => {
|
||||
const workspaceTasksExecutionWorkspaceId = useMemo(() => {
|
||||
if (!isolatedWorkspacesEnabled) return null;
|
||||
if (issueUsesMainWorkspace) return null;
|
||||
return resolveIssueFilterWorkspaceId(issue);
|
||||
return issue.executionWorkspaceId ?? issue.currentExecutionWorkspace?.id ?? null;
|
||||
}, [isolatedWorkspacesEnabled, issue, issueUsesMainWorkspace]);
|
||||
const showWorkspaceDetailLink = Boolean(issue.executionWorkspaceId) && !issueUsesMainWorkspace;
|
||||
const liveWorkspaceService = useMemo(() => {
|
||||
|
|
@ -1137,6 +1225,9 @@ export function IssueProperties({
|
|||
: [...blockedByIds, blockedByIssueId];
|
||||
onUpdate({ blockedByIssueIds: nextBlockedByIds });
|
||||
};
|
||||
const removeBlockedBy = (blockedByIssueId: string) => {
|
||||
onUpdate({ blockedByIssueIds: blockedByIds.filter((candidate) => candidate !== blockedByIssueId) });
|
||||
};
|
||||
|
||||
const blockedByContent = (
|
||||
<>
|
||||
|
|
@ -1284,7 +1375,7 @@ export function IssueProperties({
|
|||
<div>
|
||||
<PropertyRow label="Blocked by">
|
||||
{(issue.blockedBy ?? []).map((relation) => (
|
||||
<IssueReferencePill key={relation.id} issue={relation} />
|
||||
<RemovableIssueReferencePill key={relation.id} issue={relation} onRemove={removeBlockedBy} />
|
||||
))}
|
||||
{renderAddBlockedByButton(() => setBlockedByOpen((open) => !open))}
|
||||
</PropertyRow>
|
||||
|
|
@ -1297,7 +1388,7 @@ export function IssueProperties({
|
|||
) : (
|
||||
<PropertyRow label="Blocked by">
|
||||
{(issue.blockedBy ?? []).map((relation) => (
|
||||
<IssueReferencePill key={relation.id} issue={relation} />
|
||||
<RemovableIssueReferencePill key={relation.id} issue={relation} onRemove={removeBlockedBy} />
|
||||
))}
|
||||
<Popover
|
||||
open={blockedByOpen}
|
||||
|
|
@ -1448,10 +1539,10 @@ export function IssueProperties({
|
|||
</Link>
|
||||
</PropertyRow>
|
||||
)}
|
||||
{workspaceFilterId && (
|
||||
{workspaceTasksExecutionWorkspaceId && (
|
||||
<PropertyRow label="Tasks">
|
||||
<Link
|
||||
to={issuesWorkspaceFilterHref(workspaceFilterId)}
|
||||
to={executionWorkspaceIssuesHref(workspaceTasksExecutionWorkspaceId)}
|
||||
className="text-sm text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
View workspace tasks
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue