forked from farhoodlabs/paperclip
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:
@@ -41,7 +41,7 @@ import {
|
||||
resolveIssueWorkspaceName,
|
||||
type InboxIssueColumn,
|
||||
} from "../lib/inbox";
|
||||
import { cn } from "../lib/utils";
|
||||
import { cn, formatDurationMs, formatTokens } from "../lib/utils";
|
||||
import {
|
||||
InboxIssueMetaLeading,
|
||||
InboxIssueTrailingColumns,
|
||||
@@ -114,7 +114,7 @@ export type IssueSortField = "status" | "priority" | "title" | "created" | "upda
|
||||
export type IssueViewState = IssueFilterState & {
|
||||
sortField: IssueSortField;
|
||||
sortDir: "asc" | "desc";
|
||||
groupBy: "status" | "priority" | "assignee" | "workspace" | "parent" | "none";
|
||||
groupBy: "status" | "priority" | "assignee" | "project" | "workspace" | "parent" | "none";
|
||||
viewMode: "list" | "board";
|
||||
nestingEnabled: boolean;
|
||||
collapsedGroups: string[];
|
||||
@@ -364,6 +364,12 @@ interface IssuesListProps {
|
||||
createIssueLabel?: string;
|
||||
defaultSortField?: IssueSortField;
|
||||
showProgressSummary?: boolean;
|
||||
/**
|
||||
* When set together with `showProgressSummary`, the progress strip fetches
|
||||
* the recursive cost-summary for this parent issue and renders aggregate
|
||||
* tokens + wall-clock runtime for every run in the tree.
|
||||
*/
|
||||
parentIssueIdForCostSummary?: string;
|
||||
enableRoutineVisibilityFilter?: boolean;
|
||||
hasMoreIssues?: boolean;
|
||||
isLoadingMoreIssues?: boolean;
|
||||
@@ -439,9 +445,11 @@ function IssueSearchInput({
|
||||
function SubIssueProgressSummaryStrip({
|
||||
summary,
|
||||
issueLinkState,
|
||||
parentIssueIdForCostSummary,
|
||||
}: {
|
||||
summary: SubIssueProgressSummary;
|
||||
issueLinkState?: unknown;
|
||||
parentIssueIdForCostSummary?: string;
|
||||
}) {
|
||||
const target = summary.target;
|
||||
const targetIssue = target?.issue ?? null;
|
||||
@@ -451,6 +459,21 @@ function SubIssueProgressSummaryStrip({
|
||||
.map((status) => ({ status, count: summary.countsByStatus[status] ?? 0 }))
|
||||
.filter((entry) => entry.count > 0);
|
||||
|
||||
// Refresh fast enough that the runtime ticks up while a sub-issue is still
|
||||
// running, but slow enough not to hammer the recursive CTE on idle trees.
|
||||
const hasInProgress = summary.inProgressCount > 0;
|
||||
const { data: costSummary } = useQuery({
|
||||
queryKey: queryKeys.issues.costSummary(parentIssueIdForCostSummary ?? "pending", { excludeRoot: true }),
|
||||
queryFn: () => issuesApi.getCostSummary(parentIssueIdForCostSummary!, { excludeRoot: true }),
|
||||
enabled: !!parentIssueIdForCostSummary,
|
||||
refetchInterval: hasInProgress ? 5_000 : false,
|
||||
});
|
||||
|
||||
const totalTokens = costSummary
|
||||
? costSummary.inputTokens + costSummary.cachedInputTokens + costSummary.outputTokens
|
||||
: 0;
|
||||
const showCostSummary = !!costSummary && (costSummary.runCount > 0 || totalTokens > 0);
|
||||
|
||||
return (
|
||||
<div className="border border-border bg-background p-3">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
@@ -465,6 +488,23 @@ function SubIssueProgressSummaryStrip({
|
||||
<span className="text-muted-foreground">
|
||||
{summary.blockedCount} blocked
|
||||
</span>
|
||||
{showCostSummary && (
|
||||
<>
|
||||
<span
|
||||
className="text-muted-foreground tabular-nums"
|
||||
title={`${costSummary.runCount.toLocaleString()} run${
|
||||
costSummary.runCount === 1 ? "" : "s"
|
||||
} across ${costSummary.issueCount} sub-issue${
|
||||
costSummary.issueCount === 1 ? "" : "s"
|
||||
}`}
|
||||
>
|
||||
{formatTokens(totalTokens)} tokens
|
||||
</span>
|
||||
<span className="text-muted-foreground tabular-nums">
|
||||
{formatDurationMs(costSummary.runtimeMs)} runtime
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
role="progressbar"
|
||||
@@ -536,6 +576,7 @@ export function IssuesList({
|
||||
createIssueLabel,
|
||||
defaultSortField,
|
||||
showProgressSummary = false,
|
||||
parentIssueIdForCostSummary,
|
||||
enableRoutineVisibilityFilter = false,
|
||||
hasMoreIssues = false,
|
||||
isLoadingMoreIssues = false,
|
||||
@@ -996,6 +1037,22 @@ export function IssuesList({
|
||||
items: groups[key]!,
|
||||
}));
|
||||
}
|
||||
if (viewState.groupBy === "project") {
|
||||
const groups = groupBy(filtered, (issue) => issue.projectId ?? "__no_project");
|
||||
return Object.keys(groups)
|
||||
.sort((a, b) => {
|
||||
if (a === "__no_project") return 1;
|
||||
if (b === "__no_project") return -1;
|
||||
const labelA = projectById.get(a)?.name ?? a;
|
||||
const labelB = projectById.get(b)?.name ?? b;
|
||||
return labelA.localeCompare(labelB);
|
||||
})
|
||||
.map((key) => ({
|
||||
key,
|
||||
label: key === "__no_project" ? "No Project" : (projectById.get(key)?.name ?? key.slice(0, 8)),
|
||||
items: groups[key]!,
|
||||
}));
|
||||
}
|
||||
if (viewState.groupBy === "parent") {
|
||||
const groups = groupBy(filtered, (i) => i.parentId ?? "__no_parent");
|
||||
return Object.keys(groups)
|
||||
@@ -1036,6 +1093,7 @@ export function IssuesList({
|
||||
workspaceNameMap,
|
||||
issueTitleMap,
|
||||
companyUserLabelMap,
|
||||
projectById,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1131,6 +1189,7 @@ export function IssuesList({
|
||||
if (groupKey.startsWith("__user:")) defaults.assigneeUserId = groupKey.slice("__user:".length);
|
||||
else defaults.assigneeAgentId = groupKey;
|
||||
}
|
||||
else if (viewState.groupBy === "project" && groupKey !== "__no_project") defaults.projectId = groupKey;
|
||||
else if (viewState.groupBy === "parent" && groupKey !== "__no_parent") {
|
||||
const parentIssue = issueById.get(groupKey);
|
||||
if (parentIssue) Object.assign(defaults, buildSubIssueDefaultsForViewer(parentIssue, currentUserId));
|
||||
@@ -1175,7 +1234,11 @@ export function IssuesList({
|
||||
return (
|
||||
<div ref={rootRef} className="space-y-4">
|
||||
{progressSummary ? (
|
||||
<SubIssueProgressSummaryStrip summary={progressSummary} issueLinkState={issueLinkState} />
|
||||
<SubIssueProgressSummaryStrip
|
||||
summary={progressSummary}
|
||||
issueLinkState={issueLinkState}
|
||||
parentIssueIdForCostSummary={parentIssueIdForCostSummary}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* Toolbar */}
|
||||
@@ -1307,6 +1370,7 @@ export function IssuesList({
|
||||
["status", "Status"],
|
||||
["priority", "Priority"],
|
||||
["assignee", "Assignee"],
|
||||
["project", "Project"],
|
||||
["workspace", "Workspace"],
|
||||
["parent", "Parent Issue"],
|
||||
["none", "None"],
|
||||
|
||||
Reference in New Issue
Block a user