mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 03:10:38 +09:00
[codex] improve issue and routine UI responsiveness (#3744)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Operators rely on issue, inbox, and routine views to understand what the company is doing in real time > - Those views need to stay fast and readable even when issue lists, markdown comments, and run metadata get large > - The current branch had a coherent set of UI and live-update improvements spread across issue search, issue detail rendering, routine affordances, and workspace lookups > - This pull request groups those board-facing changes into one standalone branch that can merge independently of the heartbeat/runtime work > - The benefit is a faster, clearer issue and routine workflow without changing the underlying task model ## What Changed - Show routine execution issues by default and rename the filter to `Hide routine runs` so the default state no longer looks like an active filter. - Show the routine name in the run dialog and tighten the issue properties pane with a workspace link, copy-on-click behavior, and an inline parent arrow. - Reduce issue detail rerenders, keep queued issue chat mounted, improve issues page search responsiveness, and speed up issues first paint. - Add inbox "other search results", refresh visible issue runs after status updates, and optimize workspace lookups through summary-mode execution workspace queries. - Improve markdown wrapping and scrolling behavior for long strings and self-comment code blocks. - Relax the markdown sanitizer assertion so the test still validates safety after the new wrap-friendly inline styles. ## Verification - `pnpm vitest run ui/src/components/IssuesList.test.tsx ui/src/lib/inbox.test.ts ui/src/pages/Issues.test.tsx ui/src/context/BreadcrumbContext.test.tsx ui/src/context/LiveUpdatesProvider.test.ts ui/src/components/MarkdownBody.test.tsx ui/src/api/execution-workspaces.test.ts server/src/__tests__/execution-workspaces-routes.test.ts` ## Risks - This touches several issue-facing UI surfaces at once, so regressions would most likely show up as stale rendering, search result mismatches, or small markdown presentation differences. - The workspace lookup optimization depends on the summary-mode route shape staying aligned between server and UI. ## Model Used - OpenAI Codex, GPT-5-based coding agent in the Codex CLI environment. Exact backend model deployment ID was not exposed in-session. Tool-assisted editing and shell execution were used. ## 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 run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] 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
7463479fc8
commit
d4c3899ca4
34 changed files with 1035 additions and 241 deletions
|
|
@ -97,8 +97,8 @@ import type { Approval, HeartbeatRun, Issue, JoinRequest } from "@paperclipai/sh
|
|||
import {
|
||||
ACTIONABLE_APPROVAL_STATUSES,
|
||||
DEFAULT_INBOX_ISSUE_COLUMNS,
|
||||
buildGroupedInboxSections,
|
||||
buildInboxKeyboardNavEntries,
|
||||
buildInboxNesting,
|
||||
getAvailableInboxIssueColumns,
|
||||
getInboxWorkItemKey,
|
||||
getApprovalsForTab,
|
||||
|
|
@ -109,7 +109,6 @@ import {
|
|||
getLatestFailedRunsByAgent,
|
||||
matchesInboxIssueSearch,
|
||||
getRecentTouchedIssues,
|
||||
groupInboxWorkItems,
|
||||
isInboxEntityDismissed,
|
||||
isMineInboxTab,
|
||||
loadCollapsedInboxGroupKeys,
|
||||
|
|
@ -135,6 +134,7 @@ import {
|
|||
type InboxKeyboardNavEntry,
|
||||
saveLastInboxTab,
|
||||
shouldShowInboxSection,
|
||||
type InboxGroupedSection,
|
||||
type InboxTab,
|
||||
type InboxWorkItem,
|
||||
type InboxWorkItemGroupBy,
|
||||
|
|
@ -150,38 +150,6 @@ type SectionKey =
|
|||
/** A flat navigation entry for keyboard j/k traversal that includes expanded children. */
|
||||
type NavEntry = InboxKeyboardNavEntry;
|
||||
|
||||
type InboxGroupedSection = {
|
||||
key: string;
|
||||
label: string | null;
|
||||
displayItems: InboxWorkItem[];
|
||||
childrenByIssueId: Map<string, Issue[]>;
|
||||
isArchivedSearch: boolean;
|
||||
};
|
||||
|
||||
function buildGroupedInboxSections(
|
||||
items: InboxWorkItem[],
|
||||
groupBy: InboxWorkItemGroupBy,
|
||||
nestingEnabled: boolean,
|
||||
workspaceGrouping: InboxWorkspaceGroupingOptions,
|
||||
options?: { keyPrefix?: string; isArchivedSearch?: boolean },
|
||||
): InboxGroupedSection[] {
|
||||
const keyPrefix = options?.keyPrefix ?? "";
|
||||
const isArchivedSearch = options?.isArchivedSearch ?? false;
|
||||
return groupInboxWorkItems(items, groupBy, workspaceGrouping).map((group) => {
|
||||
const nestedGroup = nestingEnabled && group.items.some((item) => item.kind === "issue")
|
||||
? buildInboxNesting(group.items)
|
||||
: { displayItems: group.items, childrenByIssueId: new Map<string, Issue[]>() };
|
||||
|
||||
return {
|
||||
key: `${keyPrefix}${group.key}`,
|
||||
label: group.label,
|
||||
displayItems: nestedGroup.displayItems,
|
||||
childrenByIssueId: nestedGroup.childrenByIssueId,
|
||||
isArchivedSearch,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function firstNonEmptyLine(value: string | null | undefined): string | null {
|
||||
if (!value) return null;
|
||||
const line = value.split("\n").map((chunk) => chunk.trim()).find(Boolean);
|
||||
|
|
@ -1081,19 +1049,12 @@ export function Inbox() {
|
|||
remoteIssueSearchResults,
|
||||
],
|
||||
);
|
||||
const effectiveWorkItems = useMemo(
|
||||
() =>
|
||||
issueSearchSupplementResults.length > 0
|
||||
? [
|
||||
...filteredWorkItems,
|
||||
...getInboxWorkItems({ issues: issueSearchSupplementResults, approvals: [] }),
|
||||
]
|
||||
: filteredWorkItems,
|
||||
[filteredWorkItems, issueSearchSupplementResults],
|
||||
);
|
||||
const archivedSearchIssueIds = useMemo(
|
||||
() => new Set(archivedSearchIssues.map((issue) => issue.id)),
|
||||
[archivedSearchIssues],
|
||||
const nonInboxSearchIssueIds = useMemo(
|
||||
() => new Set([
|
||||
...archivedSearchIssues.map((issue) => issue.id),
|
||||
...issueSearchSupplementResults.map((issue) => issue.id),
|
||||
]),
|
||||
[archivedSearchIssues, issueSearchSupplementResults],
|
||||
);
|
||||
|
||||
// --- Parent-child nesting for inbox issues ---
|
||||
|
|
@ -1123,15 +1084,27 @@ export function Inbox() {
|
|||
});
|
||||
}, [selectedCompanyId]);
|
||||
const groupedSections = useMemo<InboxGroupedSection[]>(() => [
|
||||
...buildGroupedInboxSections(effectiveWorkItems, groupBy, nestingEnabled, inboxWorkspaceGrouping),
|
||||
...buildGroupedInboxSections(filteredWorkItems, groupBy, inboxWorkspaceGrouping, { nestingEnabled }),
|
||||
...buildGroupedInboxSections(
|
||||
getInboxWorkItems({ issues: archivedSearchIssues, approvals: [] }),
|
||||
groupBy,
|
||||
nestingEnabled,
|
||||
inboxWorkspaceGrouping,
|
||||
{ keyPrefix: "archived-search:", isArchivedSearch: true },
|
||||
{ keyPrefix: "archived-search:", searchSection: "archived", nestingEnabled },
|
||||
),
|
||||
], [archivedSearchIssues, effectiveWorkItems, groupBy, inboxWorkspaceGrouping, nestingEnabled]);
|
||||
...buildGroupedInboxSections(
|
||||
getInboxWorkItems({ issues: issueSearchSupplementResults, approvals: [] }),
|
||||
groupBy,
|
||||
inboxWorkspaceGrouping,
|
||||
{ keyPrefix: "other-search:", searchSection: "other", nestingEnabled },
|
||||
),
|
||||
], [
|
||||
archivedSearchIssues,
|
||||
filteredWorkItems,
|
||||
groupBy,
|
||||
inboxWorkspaceGrouping,
|
||||
issueSearchSupplementResults,
|
||||
nestingEnabled,
|
||||
]);
|
||||
const totalVisibleWorkItems = useMemo(
|
||||
() => groupedSections.reduce((count, group) => count + group.displayItems.length, 0),
|
||||
[groupedSections],
|
||||
|
|
@ -1500,7 +1473,7 @@ export function Inbox() {
|
|||
flatNavItems,
|
||||
selectedIndex,
|
||||
canArchive: canArchiveFromTab,
|
||||
archivedSearchIssueIds,
|
||||
nonInboxSearchIssueIds,
|
||||
archivingIssueIds,
|
||||
undoableArchiveIssueIds,
|
||||
unarchivingIssueIds,
|
||||
|
|
@ -1513,7 +1486,7 @@ export function Inbox() {
|
|||
flatNavItems,
|
||||
selectedIndex,
|
||||
canArchive: canArchiveFromTab,
|
||||
archivedSearchIssueIds,
|
||||
nonInboxSearchIssueIds,
|
||||
archivingIssueIds,
|
||||
undoableArchiveIssueIds,
|
||||
unarchivingIssueIds,
|
||||
|
|
@ -1616,10 +1589,10 @@ export function Inbox() {
|
|||
e.preventDefault();
|
||||
const { issue, item } = resolveNavEntry(st.selectedIndex);
|
||||
if (issue) {
|
||||
if (!st.archivedSearchIssueIds.has(issue.id) && !st.archivingIssueIds.has(issue.id)) act.archiveIssue(issue.id);
|
||||
if (!st.nonInboxSearchIssueIds.has(issue.id) && !st.archivingIssueIds.has(issue.id)) act.archiveIssue(issue.id);
|
||||
} else if (item) {
|
||||
if (item.kind === "issue") {
|
||||
if (!st.archivedSearchIssueIds.has(item.issue.id) && !st.archivingIssueIds.has(item.issue.id)) {
|
||||
if (!st.nonInboxSearchIssueIds.has(item.issue.id) && !st.archivingIssueIds.has(item.issue.id)) {
|
||||
act.archiveIssue(item.issue.id);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -2113,15 +2086,18 @@ export function Inbox() {
|
|||
return groupedSections.flatMap((group, groupIndex) => {
|
||||
const elements: ReactNode[] = [];
|
||||
const isGroupCollapsed = collapsedGroupKeys.has(group.key);
|
||||
if (group.isArchivedSearch && (groupIndex === 0 || !groupedSections[groupIndex - 1]?.isArchivedSearch)) {
|
||||
if (
|
||||
group.searchSection !== "none"
|
||||
&& group.searchSection !== groupedSections[groupIndex - 1]?.searchSection
|
||||
) {
|
||||
elements.push(
|
||||
<div
|
||||
key="archived-search-divider"
|
||||
key={`${group.searchSection}-search-divider`}
|
||||
className="flex items-center gap-3 border-y border-border/70 bg-muted/30 px-4 py-2"
|
||||
>
|
||||
<div className="h-px flex-1 bg-border/80" />
|
||||
<span className="shrink-0 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Archived
|
||||
{group.searchSection === "archived" ? "Archived" : "Other results"}
|
||||
</span>
|
||||
<div className="h-px flex-1 bg-border/80" />
|
||||
</div>,
|
||||
|
|
@ -2292,7 +2268,7 @@ export function Inbox() {
|
|||
const childIssues = group.childrenByIssueId.get(issue.id) ?? [];
|
||||
const hasChildren = childIssues.length > 0;
|
||||
const isExpanded = hasChildren && !collapsedInboxParents.has(issue.id);
|
||||
const canArchiveIssue = canArchiveFromTab && !group.isArchivedSearch;
|
||||
const canArchiveIssue = canArchiveFromTab && group.searchSection === "none";
|
||||
const parentRow = renderInboxIssue({
|
||||
issue,
|
||||
depth: 0,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue