2026-04-04 13:38:34 -05:00
|
|
|
import { startTransition, useEffect, useMemo, useRef, useState } from "react";
|
2026-03-19 11:36:01 -05:00
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
2026-04-10 22:26:21 -05:00
|
|
|
import { Link, useNavigate, useSearchParams } from "@/lib/router";
|
2026-04-04 13:38:34 -05:00
|
|
|
import { Check, ChevronDown, ChevronRight, Layers, MoreHorizontal, Plus, Repeat } from "lucide-react";
|
2026-03-19 08:39:24 -05:00
|
|
|
import { routinesApi } from "../api/routines";
|
|
|
|
|
import { agentsApi } from "../api/agents";
|
|
|
|
|
import { projectsApi } from "../api/projects";
|
2026-04-04 13:38:34 -05:00
|
|
|
import { issuesApi } from "../api/issues";
|
|
|
|
|
import { heartbeatsApi } from "../api/heartbeats";
|
2026-03-19 08:39:24 -05:00
|
|
|
import { useCompany } from "../context/CompanyContext";
|
|
|
|
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
[codex] Harden execution reliability and heartbeat tooling (#3679)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Reliable execution depends on heartbeat routing, issue lifecycle
semantics, telemetry, and a fast enough local verification loop to keep
regressions visible
> - The remaining commits on this branch were mostly server/runtime
correctness fixes plus test and documentation follow-ups in that area
> - Those changes are logically separate from the UI-focused
issue-detail and workspace/navigation branches even when they touch
overlapping issue APIs
> - This pull request groups the execution reliability, heartbeat,
telemetry, and tooling changes into one standalone branch
> - The benefit is a focused review of the control-plane correctness
work, including the follow-up fix that restored the implicit
comment-reopen helpers after branch splitting
## What Changed
- Hardened issue/heartbeat execution behavior, including self-review
stage skipping, deferred mention wakes during active execution, stranded
execution recovery, active-run scoping, assignee resolution, and
blocked-to-todo wake resumption
- Reduced noisy polling/logging overhead by trimming issue run payloads,
compacting persisted run logs, silencing high-volume request logs, and
capping heartbeat-run queries in dashboard/inbox surfaces
- Expanded telemetry and status semantics with adapter/model fields on
task completion plus clearer status guidance in docs/onboarding material
- Updated test infrastructure and verification defaults with faster
route-test module isolation, cheaper default `pnpm test`, e2e isolation
from local state, and repo verification follow-ups
- Included docs/release housekeeping from the branch and added a small
follow-up commit restoring the implicit comment-reopen helpers that were
dropped during branch reconstruction
## Verification
- `pnpm vitest run
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts`
- `pnpm vitest run server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/heartbeat-run-log.test.ts
server/src/__tests__/health.test.ts`
- `server/src/__tests__/activity-service.test.ts`,
`server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and
`server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted
on this host but the embedded Postgres harness reported
init-script/data-dir problems and skipped or failed to start, so they
are noted as environment-limited
## Risks
- Medium: this branch changes core issue/heartbeat routing and
reopen/wakeup behavior, so regressions would affect agent execution flow
rather than isolated UI polish
- Because it also updates verification infrastructure, reviewers should
pay attention to whether the new tests are asserting the right failure
modes and not just reshaping harness behavior
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## 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)
- [ ] 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>
2026-04-14 13:34:52 -05:00
|
|
|
import { useToastActions } from "../context/ToastContext";
|
2026-03-19 08:39:24 -05:00
|
|
|
import { queryKeys } from "../lib/queryKeys";
|
2026-04-04 13:38:34 -05:00
|
|
|
import { groupBy } from "../lib/groupBy";
|
|
|
|
|
import { createIssueDetailLocationState } from "../lib/issueDetailBreadcrumb";
|
2026-03-19 11:36:01 -05:00
|
|
|
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
import { getRecentProjectIds, trackRecentProject } from "../lib/recent-projects";
|
2026-04-04 10:00:39 -05:00
|
|
|
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
2026-03-19 08:39:24 -05:00
|
|
|
import { EmptyState } from "../components/EmptyState";
|
2026-04-04 13:38:34 -05:00
|
|
|
import { IssuesList } from "../components/IssuesList";
|
2026-03-19 08:39:24 -05:00
|
|
|
import { PageSkeleton } from "../components/PageSkeleton";
|
2026-04-04 13:38:34 -05:00
|
|
|
import { PageTabBar } from "../components/PageTabBar";
|
2026-03-19 11:36:01 -05:00
|
|
|
import { AgentIcon } from "../components/AgentIconPicker";
|
|
|
|
|
import { InlineEntitySelector, type InlineEntityOption } from "../components/InlineEntitySelector";
|
|
|
|
|
import { MarkdownEditor, type MarkdownEditorRef } from "../components/MarkdownEditor";
|
2026-04-02 11:38:57 -05:00
|
|
|
import {
|
|
|
|
|
RoutineRunVariablesDialog,
|
|
|
|
|
type RoutineRunDialogSubmitData,
|
|
|
|
|
} from "../components/RoutineRunVariablesDialog";
|
|
|
|
|
import { RoutineVariablesEditor, RoutineVariablesHint } from "../components/RoutineVariablesEditor";
|
2026-03-19 08:39:24 -05:00
|
|
|
import { Button } from "@/components/ui/button";
|
2026-03-19 11:36:01 -05:00
|
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
|
|
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
2026-03-19 12:07:49 -05:00
|
|
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
|
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuSeparator,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "@/components/ui/dropdown-menu";
|
2026-04-04 13:38:34 -05:00
|
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
2026-03-19 08:39:24 -05:00
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select";
|
2026-04-04 13:38:34 -05:00
|
|
|
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
2026-04-02 11:38:57 -05:00
|
|
|
import type { RoutineListItem, RoutineVariable } from "@paperclipai/shared";
|
2026-03-19 08:39:24 -05:00
|
|
|
|
|
|
|
|
const concurrencyPolicies = ["coalesce_if_active", "always_enqueue", "skip_if_active"];
|
|
|
|
|
const catchUpPolicies = ["skip_missed", "enqueue_missed_with_cap"];
|
|
|
|
|
const concurrencyPolicyDescriptions: Record<string, string> = {
|
|
|
|
|
coalesce_if_active: "If a run is already active, keep just one follow-up run queued.",
|
|
|
|
|
always_enqueue: "Queue every trigger occurrence, even if the routine is already running.",
|
|
|
|
|
skip_if_active: "Drop new trigger occurrences while a run is still active.",
|
|
|
|
|
};
|
|
|
|
|
const catchUpPolicyDescriptions: Record<string, string> = {
|
|
|
|
|
skip_missed: "Ignore windows that were missed while the scheduler or routine was paused.",
|
2026-03-20 16:26:29 -05:00
|
|
|
enqueue_missed_with_cap: "Catch up missed schedule windows in capped batches after recovery.",
|
2026-03-19 08:39:24 -05:00
|
|
|
};
|
|
|
|
|
|
2026-03-19 11:36:01 -05:00
|
|
|
function autoResizeTextarea(element: HTMLTextAreaElement | null) {
|
|
|
|
|
if (!element) return;
|
|
|
|
|
element.style.height = "auto";
|
|
|
|
|
element.style.height = `${element.scrollHeight}px`;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 12:07:49 -05:00
|
|
|
function formatLastRunTimestamp(value: Date | string | null | undefined) {
|
|
|
|
|
if (!value) return "Never";
|
|
|
|
|
return new Date(value).toLocaleString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function nextRoutineStatus(currentStatus: string, enabled: boolean) {
|
|
|
|
|
if (currentStatus === "archived" && enabled) return "active";
|
|
|
|
|
return enabled ? "active" : "paused";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 13:38:34 -05:00
|
|
|
type RoutinesTab = "routines" | "runs";
|
|
|
|
|
type RoutineGroupBy = "none" | "project" | "assignee";
|
|
|
|
|
|
|
|
|
|
type RoutineViewState = {
|
|
|
|
|
groupBy: RoutineGroupBy;
|
|
|
|
|
collapsedGroups: string[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type RoutineGroup = {
|
|
|
|
|
key: string;
|
|
|
|
|
label: string | null;
|
|
|
|
|
items: RoutineListItem[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const defaultRoutineViewState: RoutineViewState = {
|
|
|
|
|
groupBy: "none",
|
|
|
|
|
collapsedGroups: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function getRoutineViewState(key: string): RoutineViewState {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(key);
|
|
|
|
|
if (raw) return { ...defaultRoutineViewState, ...JSON.parse(raw) };
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore malformed local state and fall back to defaults.
|
|
|
|
|
}
|
|
|
|
|
return { ...defaultRoutineViewState };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function saveRoutineViewState(key: string, state: RoutineViewState) {
|
|
|
|
|
localStorage.setItem(key, JSON.stringify(state));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatRoutineRunStatus(value: string | null | undefined) {
|
|
|
|
|
if (!value) return null;
|
|
|
|
|
return value.replaceAll("_", " ");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 10:19:52 -05:00
|
|
|
function buildRoutineMutationPayload(input: {
|
|
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
projectId: string;
|
|
|
|
|
assigneeAgentId: string;
|
|
|
|
|
priority: string;
|
|
|
|
|
concurrencyPolicy: string;
|
|
|
|
|
catchUpPolicy: string;
|
|
|
|
|
variables: RoutineVariable[];
|
|
|
|
|
}) {
|
|
|
|
|
return {
|
|
|
|
|
...input,
|
|
|
|
|
description: input.description.trim() || null,
|
|
|
|
|
projectId: input.projectId || null,
|
|
|
|
|
assigneeAgentId: input.assigneeAgentId || null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-04 13:38:34 -05:00
|
|
|
export function buildRoutineGroups(
|
|
|
|
|
routines: RoutineListItem[],
|
|
|
|
|
groupByValue: RoutineGroupBy,
|
|
|
|
|
projectById: Map<string, { name: string }>,
|
|
|
|
|
agentById: Map<string, { name: string }>,
|
|
|
|
|
): RoutineGroup[] {
|
|
|
|
|
if (groupByValue === "none") {
|
|
|
|
|
return [{ key: "__all", label: null, items: routines }];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (groupByValue === "project") {
|
|
|
|
|
const groups = groupBy(routines, (routine) => routine.projectId ?? "__no_project");
|
|
|
|
|
return Object.keys(groups)
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
const leftLabel = left === "__no_project" ? "No project" : (projectById.get(left)?.name ?? "Unknown project");
|
|
|
|
|
const rightLabel = right === "__no_project" ? "No project" : (projectById.get(right)?.name ?? "Unknown project");
|
|
|
|
|
return leftLabel.localeCompare(rightLabel);
|
|
|
|
|
})
|
|
|
|
|
.map((key) => ({
|
|
|
|
|
key,
|
|
|
|
|
label: key === "__no_project" ? "No project" : (projectById.get(key)?.name ?? "Unknown project"),
|
|
|
|
|
items: groups[key]!,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const groups = groupBy(routines, (routine) => routine.assigneeAgentId ?? "__unassigned");
|
|
|
|
|
return Object.keys(groups)
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
const leftLabel = left === "__unassigned" ? "Unassigned" : (agentById.get(left)?.name ?? "Unknown agent");
|
|
|
|
|
const rightLabel = right === "__unassigned" ? "Unassigned" : (agentById.get(right)?.name ?? "Unknown agent");
|
|
|
|
|
return leftLabel.localeCompare(rightLabel);
|
|
|
|
|
})
|
|
|
|
|
.map((key) => ({
|
|
|
|
|
key,
|
|
|
|
|
label: key === "__unassigned" ? "Unassigned" : (agentById.get(key)?.name ?? "Unknown agent"),
|
|
|
|
|
items: groups[key]!,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildRoutinesTabHref(tab: RoutinesTab) {
|
|
|
|
|
return tab === "runs" ? "/routines?tab=runs" : "/routines";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function RoutineListRow({
|
|
|
|
|
routine,
|
|
|
|
|
projectById,
|
|
|
|
|
agentById,
|
|
|
|
|
runningRoutineId,
|
|
|
|
|
statusMutationRoutineId,
|
2026-04-10 22:26:21 -05:00
|
|
|
href,
|
2026-04-04 13:38:34 -05:00
|
|
|
onRunNow,
|
|
|
|
|
onToggleEnabled,
|
|
|
|
|
onToggleArchived,
|
|
|
|
|
}: {
|
|
|
|
|
routine: RoutineListItem;
|
|
|
|
|
projectById: Map<string, { name: string; color?: string | null }>;
|
|
|
|
|
agentById: Map<string, { name: string; icon?: string | null }>;
|
|
|
|
|
runningRoutineId: string | null;
|
|
|
|
|
statusMutationRoutineId: string | null;
|
2026-04-10 22:26:21 -05:00
|
|
|
href: string;
|
2026-04-04 13:38:34 -05:00
|
|
|
onRunNow: (routine: RoutineListItem) => void;
|
|
|
|
|
onToggleEnabled: (routine: RoutineListItem, enabled: boolean) => void;
|
|
|
|
|
onToggleArchived: (routine: RoutineListItem) => void;
|
|
|
|
|
}) {
|
|
|
|
|
const enabled = routine.status === "active";
|
|
|
|
|
const isArchived = routine.status === "archived";
|
|
|
|
|
const isStatusPending = statusMutationRoutineId === routine.id;
|
|
|
|
|
const project = routine.projectId ? projectById.get(routine.projectId) ?? null : null;
|
|
|
|
|
const agent = routine.assigneeAgentId ? agentById.get(routine.assigneeAgentId) ?? null : null;
|
2026-04-09 10:19:52 -05:00
|
|
|
const isDraft = !isArchived && !routine.assigneeAgentId;
|
2026-04-04 13:38:34 -05:00
|
|
|
|
|
|
|
|
return (
|
2026-04-10 22:26:21 -05:00
|
|
|
<Link
|
|
|
|
|
to={href}
|
|
|
|
|
className="group flex flex-col gap-3 border-b border-border px-3 py-3 transition-colors hover:bg-accent/50 last:border-b-0 sm:flex-row sm:items-center no-underline text-inherit"
|
2026-04-04 13:38:34 -05:00
|
|
|
>
|
|
|
|
|
<div className="min-w-0 flex-1 space-y-1.5">
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
|
|
|
<span className="truncate text-sm font-medium">{routine.title}</span>
|
2026-04-09 10:19:52 -05:00
|
|
|
{(isArchived || routine.status === "paused" || isDraft) ? (
|
2026-04-04 13:38:34 -05:00
|
|
|
<span className="text-xs text-muted-foreground">
|
2026-04-09 10:19:52 -05:00
|
|
|
{isArchived ? "archived" : isDraft ? "draft" : "paused"}
|
2026-04-04 13:38:34 -05:00
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
|
|
|
|
<span className="flex items-center gap-2">
|
|
|
|
|
<span
|
|
|
|
|
className="h-2.5 w-2.5 shrink-0 rounded-sm"
|
|
|
|
|
style={{ backgroundColor: project?.color ?? "#64748b" }}
|
|
|
|
|
/>
|
2026-04-09 10:19:52 -05:00
|
|
|
<span>{routine.projectId ? (project?.name ?? "Unknown project") : "No project"}</span>
|
2026-04-04 13:38:34 -05:00
|
|
|
</span>
|
|
|
|
|
<span className="flex items-center gap-2">
|
|
|
|
|
{agent?.icon ? <AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0" /> : null}
|
2026-04-09 10:19:52 -05:00
|
|
|
<span>{routine.assigneeAgentId ? (agent?.name ?? "Unknown agent") : "No default agent"}</span>
|
2026-04-04 13:38:34 -05:00
|
|
|
</span>
|
|
|
|
|
<span>
|
|
|
|
|
{formatLastRunTimestamp(routine.lastRun?.triggeredAt)}
|
|
|
|
|
{routine.lastRun ? ` · ${formatRoutineRunStatus(routine.lastRun.status)}` : ""}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-10 22:26:21 -05:00
|
|
|
<div className="flex items-center gap-3" onClick={(event) => { event.preventDefault(); event.stopPropagation(); }}>
|
2026-04-04 13:38:34 -05:00
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<ToggleSwitch
|
|
|
|
|
size="lg"
|
|
|
|
|
checked={enabled}
|
|
|
|
|
onCheckedChange={() => onToggleEnabled(routine, enabled)}
|
|
|
|
|
disabled={isStatusPending || isArchived}
|
|
|
|
|
aria-label={enabled ? `Disable ${routine.title}` : `Enable ${routine.title}`}
|
|
|
|
|
/>
|
|
|
|
|
<span className="w-12 text-xs text-muted-foreground">
|
2026-04-09 10:19:52 -05:00
|
|
|
{isArchived ? "Archived" : isDraft ? "Draft" : enabled ? "On" : "Off"}
|
2026-04-04 13:38:34 -05:00
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon-sm" aria-label={`More actions for ${routine.title}`}>
|
|
|
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent align="end">
|
2026-04-10 22:26:21 -05:00
|
|
|
<DropdownMenuItem asChild>
|
|
|
|
|
<Link to={href}>Edit</Link>
|
2026-04-04 13:38:34 -05:00
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
disabled={runningRoutineId === routine.id || isArchived}
|
|
|
|
|
onClick={() => onRunNow(routine)}
|
|
|
|
|
>
|
|
|
|
|
{runningRoutineId === routine.id ? "Running..." : "Run now"}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => onToggleEnabled(routine, enabled)}
|
|
|
|
|
disabled={isStatusPending || isArchived}
|
|
|
|
|
>
|
|
|
|
|
{enabled ? "Pause" : "Enable"}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => onToggleArchived(routine)}
|
|
|
|
|
disabled={isStatusPending}
|
|
|
|
|
>
|
|
|
|
|
{routine.status === "archived" ? "Restore" : "Archive"}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
</div>
|
2026-04-10 22:26:21 -05:00
|
|
|
</Link>
|
2026-04-04 13:38:34 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 08:39:24 -05:00
|
|
|
export function Routines() {
|
|
|
|
|
const { selectedCompanyId } = useCompany();
|
|
|
|
|
const { setBreadcrumbs } = useBreadcrumbs();
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const navigate = useNavigate();
|
2026-04-04 13:38:34 -05:00
|
|
|
const [searchParams] = useSearchParams();
|
[codex] Harden execution reliability and heartbeat tooling (#3679)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - Reliable execution depends on heartbeat routing, issue lifecycle
semantics, telemetry, and a fast enough local verification loop to keep
regressions visible
> - The remaining commits on this branch were mostly server/runtime
correctness fixes plus test and documentation follow-ups in that area
> - Those changes are logically separate from the UI-focused
issue-detail and workspace/navigation branches even when they touch
overlapping issue APIs
> - This pull request groups the execution reliability, heartbeat,
telemetry, and tooling changes into one standalone branch
> - The benefit is a focused review of the control-plane correctness
work, including the follow-up fix that restored the implicit
comment-reopen helpers after branch splitting
## What Changed
- Hardened issue/heartbeat execution behavior, including self-review
stage skipping, deferred mention wakes during active execution, stranded
execution recovery, active-run scoping, assignee resolution, and
blocked-to-todo wake resumption
- Reduced noisy polling/logging overhead by trimming issue run payloads,
compacting persisted run logs, silencing high-volume request logs, and
capping heartbeat-run queries in dashboard/inbox surfaces
- Expanded telemetry and status semantics with adapter/model fields on
task completion plus clearer status guidance in docs/onboarding material
- Updated test infrastructure and verification defaults with faster
route-test module isolation, cheaper default `pnpm test`, e2e isolation
from local state, and repo verification follow-ups
- Included docs/release housekeeping from the branch and added a small
follow-up commit restoring the implicit comment-reopen helpers that were
dropped during branch reconstruction
## Verification
- `pnpm vitest run
server/src/__tests__/issue-comment-reopen-routes.test.ts
server/src/__tests__/issue-telemetry-routes.test.ts`
- `pnpm vitest run server/src/__tests__/http-log-policy.test.ts
server/src/__tests__/heartbeat-run-log.test.ts
server/src/__tests__/health.test.ts`
- `server/src/__tests__/activity-service.test.ts`,
`server/src/__tests__/heartbeat-comment-wake-batching.test.ts`, and
`server/src/__tests__/heartbeat-process-recovery.test.ts` were attempted
on this host but the embedded Postgres harness reported
init-script/data-dir problems and skipped or failed to start, so they
are noted as environment-limited
## Risks
- Medium: this branch changes core issue/heartbeat routing and
reopen/wakeup behavior, so regressions would affect agent execution flow
rather than isolated UI polish
- Because it also updates verification infrastructure, reviewers should
pay attention to whether the new tests are asserting the right failure
modes and not just reshaping harness behavior
## Model Used
- OpenAI Codex coding agent (GPT-5-class runtime in Codex CLI; exact
deployed model ID is not exposed in this environment), reasoning
enabled, tool use and local code execution enabled
## 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)
- [ ] 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>
2026-04-14 13:34:52 -05:00
|
|
|
const { pushToast } = useToastActions();
|
2026-03-19 11:36:01 -05:00
|
|
|
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
|
|
|
|
const titleInputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
|
|
|
const assigneeSelectorRef = useRef<HTMLButtonElement | null>(null);
|
|
|
|
|
const projectSelectorRef = useRef<HTMLButtonElement | null>(null);
|
2026-03-19 08:39:24 -05:00
|
|
|
const [runningRoutineId, setRunningRoutineId] = useState<string | null>(null);
|
2026-03-19 12:07:49 -05:00
|
|
|
const [statusMutationRoutineId, setStatusMutationRoutineId] = useState<string | null>(null);
|
2026-04-02 11:38:57 -05:00
|
|
|
const [runDialogRoutine, setRunDialogRoutine] = useState<RoutineListItem | null>(null);
|
2026-03-19 11:36:01 -05:00
|
|
|
const [composerOpen, setComposerOpen] = useState(false);
|
|
|
|
|
const [advancedOpen, setAdvancedOpen] = useState(false);
|
2026-04-04 13:38:34 -05:00
|
|
|
const activeTab: RoutinesTab = searchParams.get("tab") === "runs" ? "runs" : "routines";
|
2026-04-02 11:38:57 -05:00
|
|
|
const [draft, setDraft] = useState<{
|
|
|
|
|
title: string;
|
|
|
|
|
description: string;
|
|
|
|
|
projectId: string;
|
|
|
|
|
assigneeAgentId: string;
|
|
|
|
|
priority: string;
|
|
|
|
|
concurrencyPolicy: string;
|
|
|
|
|
catchUpPolicy: string;
|
|
|
|
|
variables: RoutineVariable[];
|
|
|
|
|
}>({
|
2026-03-19 08:39:24 -05:00
|
|
|
title: "",
|
|
|
|
|
description: "",
|
|
|
|
|
projectId: "",
|
|
|
|
|
assigneeAgentId: "",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
concurrencyPolicy: "coalesce_if_active",
|
|
|
|
|
catchUpPolicy: "skip_missed",
|
2026-04-02 11:38:57 -05:00
|
|
|
variables: [],
|
2026-03-19 08:39:24 -05:00
|
|
|
});
|
2026-04-04 13:38:34 -05:00
|
|
|
const routineViewStateKey = selectedCompanyId
|
|
|
|
|
? `paperclip:routines-view:${selectedCompanyId}`
|
|
|
|
|
: "paperclip:routines-view";
|
|
|
|
|
const [routineViewState, setRoutineViewState] = useState<RoutineViewState>(() => getRoutineViewState(routineViewStateKey));
|
2026-03-19 08:39:24 -05:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setBreadcrumbs([{ label: "Routines" }]);
|
|
|
|
|
}, [setBreadcrumbs]);
|
|
|
|
|
|
2026-04-04 13:38:34 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
setRoutineViewState(getRoutineViewState(routineViewStateKey));
|
|
|
|
|
}, [routineViewStateKey]);
|
|
|
|
|
|
2026-03-19 08:39:24 -05:00
|
|
|
const { data: routines, isLoading, error } = useQuery({
|
|
|
|
|
queryKey: queryKeys.routines.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => routinesApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId,
|
|
|
|
|
});
|
|
|
|
|
const { data: agents } = useQuery({
|
|
|
|
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId,
|
|
|
|
|
});
|
|
|
|
|
const { data: projects } = useQuery({
|
|
|
|
|
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => projectsApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId,
|
|
|
|
|
});
|
2026-04-04 13:38:34 -05:00
|
|
|
const { data: routineExecutionIssues, isLoading: recentRunsLoading, error: recentRunsError } = useQuery({
|
|
|
|
|
queryKey: [...queryKeys.issues.list(selectedCompanyId!), "routine-executions"],
|
|
|
|
|
queryFn: () => issuesApi.list(selectedCompanyId!, { originKind: "routine_execution" }),
|
|
|
|
|
enabled: !!selectedCompanyId && activeTab === "runs",
|
|
|
|
|
});
|
|
|
|
|
const { data: liveRuns } = useQuery({
|
|
|
|
|
queryKey: queryKeys.liveRuns(selectedCompanyId!),
|
|
|
|
|
queryFn: () => heartbeatsApi.liveRunsForCompany(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId && activeTab === "runs",
|
|
|
|
|
refetchInterval: 5000,
|
|
|
|
|
});
|
2026-03-19 08:39:24 -05:00
|
|
|
|
2026-03-19 11:36:01 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
autoResizeTextarea(titleInputRef.current);
|
|
|
|
|
}, [draft.title, composerOpen]);
|
|
|
|
|
|
2026-03-19 08:39:24 -05:00
|
|
|
const createRoutine = useMutation({
|
|
|
|
|
mutationFn: () =>
|
2026-04-09 10:19:52 -05:00
|
|
|
routinesApi.create(selectedCompanyId!, buildRoutineMutationPayload(draft)),
|
2026-03-19 08:39:24 -05:00
|
|
|
onSuccess: async (routine) => {
|
|
|
|
|
setDraft({
|
|
|
|
|
title: "",
|
|
|
|
|
description: "",
|
|
|
|
|
projectId: "",
|
|
|
|
|
assigneeAgentId: "",
|
|
|
|
|
priority: "medium",
|
|
|
|
|
concurrencyPolicy: "coalesce_if_active",
|
|
|
|
|
catchUpPolicy: "skip_missed",
|
2026-04-02 11:38:57 -05:00
|
|
|
variables: [],
|
2026-03-19 08:39:24 -05:00
|
|
|
});
|
2026-03-19 11:36:01 -05:00
|
|
|
setComposerOpen(false);
|
|
|
|
|
setAdvancedOpen(false);
|
2026-03-19 08:39:24 -05:00
|
|
|
await queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) });
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Routine created",
|
2026-04-09 10:19:52 -05:00
|
|
|
body: routine.assigneeAgentId
|
|
|
|
|
? "Add the first trigger to turn it into a live workflow."
|
|
|
|
|
: "Draft saved. Add a default agent before enabling automation.",
|
2026-03-19 08:39:24 -05:00
|
|
|
tone: "success",
|
|
|
|
|
});
|
|
|
|
|
navigate(`/routines/${routine.id}?tab=triggers`);
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-04-04 13:38:34 -05:00
|
|
|
const updateIssue = useMutation({
|
|
|
|
|
mutationFn: ({ id, data }: { id: string; data: Record<string, unknown> }) =>
|
|
|
|
|
issuesApi.update(id, data),
|
|
|
|
|
onSuccess: async () => {
|
|
|
|
|
await queryClient.invalidateQueries({ queryKey: [...queryKeys.issues.list(selectedCompanyId!), "routine-executions"] });
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-03-19 08:39:24 -05:00
|
|
|
|
2026-03-19 12:07:49 -05:00
|
|
|
const updateRoutineStatus = useMutation({
|
|
|
|
|
mutationFn: ({ id, status }: { id: string; status: string }) => routinesApi.update(id, { status }),
|
|
|
|
|
onMutate: ({ id }) => {
|
|
|
|
|
setStatusMutationRoutineId(id);
|
|
|
|
|
},
|
|
|
|
|
onSuccess: async (_, variables) => {
|
|
|
|
|
await Promise.all([
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }),
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(variables.id) }),
|
|
|
|
|
]);
|
|
|
|
|
},
|
|
|
|
|
onSettled: () => {
|
|
|
|
|
setStatusMutationRoutineId(null);
|
|
|
|
|
},
|
|
|
|
|
onError: (mutationError) => {
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Failed to update routine",
|
|
|
|
|
body: mutationError instanceof Error ? mutationError.message : "Paperclip could not update the routine.",
|
|
|
|
|
tone: "error",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-19 08:39:24 -05:00
|
|
|
const runRoutine = useMutation({
|
2026-04-02 11:38:57 -05:00
|
|
|
mutationFn: ({ id, data }: { id: string; data?: RoutineRunDialogSubmitData }) => routinesApi.run(id, {
|
|
|
|
|
...(data?.variables && Object.keys(data.variables).length > 0 ? { variables: data.variables } : {}),
|
2026-04-09 10:19:52 -05:00
|
|
|
...(data?.assigneeAgentId !== undefined ? { assigneeAgentId: data.assigneeAgentId } : {}),
|
|
|
|
|
...(data?.projectId !== undefined ? { projectId: data.projectId } : {}),
|
2026-04-02 11:38:57 -05:00
|
|
|
...(data?.executionWorkspaceId !== undefined ? { executionWorkspaceId: data.executionWorkspaceId } : {}),
|
|
|
|
|
...(data?.executionWorkspacePreference !== undefined
|
|
|
|
|
? { executionWorkspacePreference: data.executionWorkspacePreference }
|
|
|
|
|
: {}),
|
|
|
|
|
...(data?.executionWorkspaceSettings !== undefined
|
|
|
|
|
? { executionWorkspaceSettings: data.executionWorkspaceSettings }
|
|
|
|
|
: {}),
|
|
|
|
|
}),
|
|
|
|
|
onMutate: ({ id }) => {
|
2026-03-19 08:39:24 -05:00
|
|
|
setRunningRoutineId(id);
|
|
|
|
|
},
|
2026-04-02 11:38:57 -05:00
|
|
|
onSuccess: async (_, { id }) => {
|
|
|
|
|
setRunDialogRoutine(null);
|
2026-03-19 12:07:49 -05:00
|
|
|
await Promise.all([
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.routines.list(selectedCompanyId!) }),
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.routines.detail(id) }),
|
|
|
|
|
]);
|
2026-03-19 08:39:24 -05:00
|
|
|
},
|
|
|
|
|
onSettled: () => {
|
|
|
|
|
setRunningRoutineId(null);
|
|
|
|
|
},
|
2026-03-19 11:36:01 -05:00
|
|
|
onError: (mutationError) => {
|
2026-03-19 08:39:24 -05:00
|
|
|
pushToast({
|
|
|
|
|
title: "Routine run failed",
|
2026-03-19 11:36:01 -05:00
|
|
|
body: mutationError instanceof Error ? mutationError.message : "Paperclip could not start the routine run.",
|
2026-03-19 08:39:24 -05:00
|
|
|
tone: "error",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-19 11:36:01 -05:00
|
|
|
const recentAssigneeIds = useMemo(() => getRecentAssigneeIds(), [composerOpen]);
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
const recentProjectIds = useMemo(() => getRecentProjectIds(), [composerOpen]);
|
2026-03-19 11:36:01 -05:00
|
|
|
const assigneeOptions = useMemo<InlineEntityOption[]>(
|
|
|
|
|
() =>
|
|
|
|
|
sortAgentsByRecency(
|
|
|
|
|
(agents ?? []).filter((agent) => agent.status !== "terminated"),
|
|
|
|
|
recentAssigneeIds,
|
|
|
|
|
).map((agent) => ({
|
|
|
|
|
id: agent.id,
|
|
|
|
|
label: agent.name,
|
|
|
|
|
searchText: `${agent.name} ${agent.role} ${agent.title ?? ""}`,
|
|
|
|
|
})),
|
|
|
|
|
[agents, recentAssigneeIds],
|
|
|
|
|
);
|
|
|
|
|
const projectOptions = useMemo<InlineEntityOption[]>(
|
|
|
|
|
() =>
|
|
|
|
|
(projects ?? []).map((project) => ({
|
|
|
|
|
id: project.id,
|
|
|
|
|
label: project.name,
|
|
|
|
|
searchText: project.description ?? "",
|
|
|
|
|
})),
|
|
|
|
|
[projects],
|
|
|
|
|
);
|
|
|
|
|
const agentById = useMemo(
|
|
|
|
|
() => new Map((agents ?? []).map((agent) => [agent.id, agent])),
|
|
|
|
|
[agents],
|
|
|
|
|
);
|
|
|
|
|
const projectById = useMemo(
|
|
|
|
|
() => new Map((projects ?? []).map((project) => [project.id, project])),
|
|
|
|
|
[projects],
|
|
|
|
|
);
|
2026-04-04 13:38:34 -05:00
|
|
|
const liveIssueIds = useMemo(() => {
|
|
|
|
|
const ids = new Set<string>();
|
|
|
|
|
for (const run of liveRuns ?? []) {
|
|
|
|
|
if (run.issueId) ids.add(run.issueId);
|
|
|
|
|
}
|
|
|
|
|
return ids;
|
|
|
|
|
}, [liveRuns]);
|
|
|
|
|
const routineGroups = useMemo(
|
|
|
|
|
() => buildRoutineGroups(routines ?? [], routineViewState.groupBy, projectById, agentById),
|
|
|
|
|
[agentById, projectById, routineViewState.groupBy, routines],
|
|
|
|
|
);
|
|
|
|
|
const recentRunsIssueLinkState = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
createIssueDetailLocationState(
|
|
|
|
|
"Recent Runs",
|
|
|
|
|
buildRoutinesTabHref("runs"),
|
|
|
|
|
"issues",
|
|
|
|
|
),
|
|
|
|
|
[],
|
|
|
|
|
);
|
2026-03-19 11:36:01 -05:00
|
|
|
const currentAssignee = draft.assigneeAgentId ? agentById.get(draft.assigneeAgentId) ?? null : null;
|
|
|
|
|
const currentProject = draft.projectId ? projectById.get(draft.projectId) ?? null : null;
|
|
|
|
|
|
2026-04-04 13:38:34 -05:00
|
|
|
function updateRoutineView(patch: Partial<RoutineViewState>) {
|
|
|
|
|
setRoutineViewState((current) => {
|
|
|
|
|
const next = { ...current, ...patch };
|
|
|
|
|
saveRoutineViewState(routineViewStateKey, next);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleTabChange(tab: string) {
|
|
|
|
|
const nextTab = tab === "runs" ? "runs" : "routines";
|
|
|
|
|
startTransition(() => {
|
|
|
|
|
navigate(buildRoutinesTabHref(nextTab));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 11:38:57 -05:00
|
|
|
function handleRunNow(routine: RoutineListItem) {
|
2026-04-09 10:19:52 -05:00
|
|
|
setRunDialogRoutine(routine);
|
2026-04-02 11:38:57 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 13:38:34 -05:00
|
|
|
function handleToggleEnabled(routine: RoutineListItem, enabled: boolean) {
|
2026-04-09 10:19:52 -05:00
|
|
|
if (!enabled && !routine.assigneeAgentId) {
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Default agent required",
|
|
|
|
|
body: "Set a default agent before enabling routine automation.",
|
|
|
|
|
tone: "warn",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-04 13:38:34 -05:00
|
|
|
updateRoutineStatus.mutate({
|
|
|
|
|
id: routine.id,
|
|
|
|
|
status: nextRoutineStatus(routine.status, !enabled),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleToggleArchived(routine: RoutineListItem) {
|
|
|
|
|
updateRoutineStatus.mutate({
|
|
|
|
|
id: routine.id,
|
|
|
|
|
status: routine.status === "archived" ? "active" : "archived",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 08:39:24 -05:00
|
|
|
if (!selectedCompanyId) {
|
|
|
|
|
return <EmptyState icon={Repeat} message="Select a company to view routines." />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return <PageSkeleton variant="issues-list" />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
2026-03-19 11:36:01 -05:00
|
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
|
|
|
|
<div className="space-y-1">
|
2026-04-10 22:26:21 -05:00
|
|
|
<h1 className="text-2xl font-semibold tracking-tight">
|
2026-03-20 13:10:45 -05:00
|
|
|
Routines
|
|
|
|
|
</h1>
|
2026-03-19 11:36:01 -05:00
|
|
|
<p className="text-sm text-muted-foreground">
|
2026-03-19 12:07:49 -05:00
|
|
|
Recurring work definitions that materialize into auditable execution issues.
|
2026-03-19 11:36:01 -05:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-03-19 12:07:49 -05:00
|
|
|
<Button onClick={() => setComposerOpen(true)}>
|
2026-03-19 11:36:01 -05:00
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
2026-03-19 12:07:49 -05:00
|
|
|
Create routine
|
2026-03-19 11:36:01 -05:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-04 13:38:34 -05:00
|
|
|
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
|
|
|
|
<PageTabBar
|
|
|
|
|
align="start"
|
|
|
|
|
value={activeTab}
|
|
|
|
|
onValueChange={handleTabChange}
|
|
|
|
|
items={[
|
|
|
|
|
{ value: "routines", label: "Routines" },
|
|
|
|
|
{ value: "runs", label: "Recent Runs" },
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
<TabsContent value="routines" className="space-y-4">
|
|
|
|
|
<div className="flex items-center justify-between gap-3">
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{(routines ?? []).length} routine{(routines ?? []).length === 1 ? "" : "s"}
|
|
|
|
|
</p>
|
|
|
|
|
<Popover>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="sm" className="text-xs">
|
|
|
|
|
<Layers className="h-3.5 w-3.5 sm:h-3 sm:w-3 sm:mr-1" />
|
|
|
|
|
<span className="hidden sm:inline">Group</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent align="end" className="w-44 p-0">
|
|
|
|
|
<div className="p-2 space-y-0.5">
|
|
|
|
|
{([
|
|
|
|
|
["project", "Project"],
|
|
|
|
|
["assignee", "Agent"],
|
|
|
|
|
["none", "None"],
|
|
|
|
|
] as const).map(([value, label]) => (
|
|
|
|
|
<button
|
|
|
|
|
key={value}
|
|
|
|
|
className={`flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm ${
|
|
|
|
|
routineViewState.groupBy === value
|
|
|
|
|
? "bg-accent/50 text-foreground"
|
|
|
|
|
: "text-muted-foreground hover:bg-accent/50"
|
|
|
|
|
}`}
|
|
|
|
|
onClick={() => updateRoutineView({ groupBy: value, collapsedGroups: [] })}
|
|
|
|
|
>
|
|
|
|
|
<span>{label}</span>
|
|
|
|
|
{routineViewState.groupBy === value ? <Check className="h-3.5 w-3.5" /> : null}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
</div>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
<TabsContent value="runs">
|
|
|
|
|
<IssuesList
|
|
|
|
|
issues={routineExecutionIssues ?? []}
|
|
|
|
|
isLoading={recentRunsLoading}
|
|
|
|
|
error={recentRunsError as Error | null}
|
|
|
|
|
agents={agents}
|
|
|
|
|
projects={projects}
|
|
|
|
|
liveIssueIds={liveIssueIds}
|
|
|
|
|
viewStateKey="paperclip:routine-recent-runs-view"
|
|
|
|
|
issueLinkState={recentRunsIssueLinkState}
|
|
|
|
|
onUpdateIssue={(id, data) => updateIssue.mutate({ id, data })}
|
|
|
|
|
/>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
</Tabs>
|
|
|
|
|
|
2026-03-19 12:07:49 -05:00
|
|
|
<Dialog
|
|
|
|
|
open={composerOpen}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!createRoutine.isPending) {
|
|
|
|
|
setComposerOpen(open);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-03-30 07:07:10 -05:00
|
|
|
<DialogContent
|
|
|
|
|
showCloseButton={false}
|
|
|
|
|
className="flex max-h-[calc(100dvh-2rem)] max-w-3xl flex-col gap-0 overflow-hidden p-0"
|
|
|
|
|
>
|
|
|
|
|
<div className="shrink-0 flex flex-wrap items-center justify-between gap-3 border-b border-border/60 px-5 py-3">
|
2026-03-19 11:36:01 -05:00
|
|
|
<div>
|
|
|
|
|
<p className="text-xs font-medium uppercase tracking-[0.2em] text-muted-foreground">New routine</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
2026-04-09 10:19:52 -05:00
|
|
|
Define the recurring work first. Default project and agent are optional for draft routines.
|
2026-03-19 11:36:01 -05:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setComposerOpen(false);
|
|
|
|
|
setAdvancedOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
disabled={createRoutine.isPending}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-30 07:07:10 -05:00
|
|
|
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
|
|
|
<div className="px-5 pt-5 pb-3">
|
|
|
|
|
<textarea
|
|
|
|
|
ref={titleInputRef}
|
|
|
|
|
className="w-full resize-none overflow-hidden bg-transparent text-xl font-semibold outline-none placeholder:text-muted-foreground/50"
|
|
|
|
|
placeholder="Routine title"
|
|
|
|
|
rows={1}
|
|
|
|
|
value={draft.title}
|
|
|
|
|
onChange={(event) => {
|
|
|
|
|
setDraft((current) => ({ ...current, title: event.target.value }));
|
|
|
|
|
autoResizeTextarea(event.target);
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === "Enter" && !event.metaKey && !event.ctrlKey && !event.nativeEvent.isComposing) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (event.key === "Tab" && !event.shiftKey) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (draft.assigneeAgentId) {
|
|
|
|
|
if (draft.projectId) {
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
} else {
|
|
|
|
|
projectSelectorRef.current?.focus();
|
|
|
|
|
}
|
2026-03-19 11:36:01 -05:00
|
|
|
} else {
|
2026-03-30 07:07:10 -05:00
|
|
|
assigneeSelectorRef.current?.focus();
|
2026-03-19 11:36:01 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-30 07:07:10 -05:00
|
|
|
}}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-03-19 11:36:01 -05:00
|
|
|
|
2026-03-30 07:07:10 -05:00
|
|
|
<div className="px-5 pb-3">
|
|
|
|
|
<div className="overflow-x-auto overscroll-x-contain">
|
|
|
|
|
<div className="inline-flex min-w-full flex-wrap items-center gap-2 text-sm text-muted-foreground sm:min-w-max sm:flex-nowrap">
|
|
|
|
|
<span>For</span>
|
|
|
|
|
<InlineEntitySelector
|
|
|
|
|
ref={assigneeSelectorRef}
|
|
|
|
|
value={draft.assigneeAgentId}
|
|
|
|
|
options={assigneeOptions}
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
recentOptionIds={recentAssigneeIds}
|
2026-03-30 07:07:10 -05:00
|
|
|
placeholder="Assignee"
|
|
|
|
|
noneLabel="No assignee"
|
|
|
|
|
searchPlaceholder="Search assignees..."
|
|
|
|
|
emptyMessage="No assignees found."
|
|
|
|
|
onChange={(assigneeAgentId) => {
|
|
|
|
|
if (assigneeAgentId) trackRecentAssignee(assigneeAgentId);
|
|
|
|
|
setDraft((current) => ({ ...current, assigneeAgentId }));
|
|
|
|
|
}}
|
|
|
|
|
onConfirm={() => {
|
|
|
|
|
if (draft.projectId) {
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
} else {
|
|
|
|
|
projectSelectorRef.current?.focus();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
renderTriggerValue={(option) =>
|
|
|
|
|
option ? (
|
|
|
|
|
currentAssignee ? (
|
|
|
|
|
<>
|
|
|
|
|
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
)
|
|
|
|
|
) : (
|
|
|
|
|
<span className="text-muted-foreground">Assignee</span>
|
|
|
|
|
)
|
2026-03-19 11:36:01 -05:00
|
|
|
}
|
2026-03-30 07:07:10 -05:00
|
|
|
renderOption={(option) => {
|
|
|
|
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
|
|
|
|
const assignee = agentById.get(option.id);
|
|
|
|
|
return (
|
2026-03-19 11:36:01 -05:00
|
|
|
<>
|
2026-03-30 07:07:10 -05:00
|
|
|
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<span>in</span>
|
|
|
|
|
<InlineEntitySelector
|
|
|
|
|
ref={projectSelectorRef}
|
|
|
|
|
value={draft.projectId}
|
|
|
|
|
options={projectOptions}
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
recentOptionIds={recentProjectIds}
|
2026-03-30 07:07:10 -05:00
|
|
|
placeholder="Project"
|
|
|
|
|
noneLabel="No project"
|
|
|
|
|
searchPlaceholder="Search projects..."
|
|
|
|
|
emptyMessage="No projects found."
|
[codex] Polish issue and operator workflow UI (#4090)
## Thinking Path
> - Paperclip operators spend much of their time in issues, inboxes,
selectors, and rich comment threads.
> - Small interaction problems in those surfaces slow down supervision
of AI-agent work.
> - The branch included related operator quality-of-life fixes for issue
layout, inbox actions, recent selectors, mobile inputs, and chat
rendering stability.
> - These changes are UI-focused and can land independently from
workspace navigation and access-profile work.
> - This pull request groups the operator QoL fixes into one standalone
branch.
> - The benefit is a more stable and efficient board workflow for issue
triage and task editing.
## What Changed
- Widened issue detail content and added a desktop inbox archive action.
- Fixed mobile text-field zoom by keeping touch input font sizes at
16px.
- Prioritized recent picker selections for assignees/projects in issue
and routine flows.
- Showed actionable approvals in the Mine inbox model.
- Fixed issue chat renderer state crashes and hardened tests.
## Verification
- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run ui/src/components/IssueChatThread.test.tsx
ui/src/lib/inbox.test.ts ui/src/lib/recent-selections.test.ts`
- Split integration check: merged last after the other
[PAP-1614](/PAP/issues/PAP-1614) branches with no merge conflicts.
- Confirmed this branch does not include `pnpm-lock.yaml`.
## Risks
- Low to medium risk: mostly UI state, layout, and selection-priority
behavior.
- Visual layout and mobile zoom behavior may need browser/device QA
beyond component tests.
- No database migrations are included.
> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.
## Model Used
- OpenAI Codex, GPT-5.4 tool-enabled coding model, agentic
code-editing/runtime with local shell and GitHub CLI access; exact
context window and reasoning mode are not exposed by the Paperclip
harness.
## 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: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-20 06:16:41 -05:00
|
|
|
onChange={(projectId) => {
|
|
|
|
|
if (projectId) trackRecentProject(projectId);
|
|
|
|
|
setDraft((current) => ({ ...current, projectId }));
|
|
|
|
|
}}
|
2026-03-30 07:07:10 -05:00
|
|
|
onConfirm={() => descriptionEditorRef.current?.focus()}
|
|
|
|
|
renderTriggerValue={(option) =>
|
|
|
|
|
option && currentProject ? (
|
|
|
|
|
<>
|
|
|
|
|
<span
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
|
|
|
|
style={{ backgroundColor: currentProject.color ?? "#64748b" }}
|
|
|
|
|
/>
|
2026-03-19 11:36:01 -05:00
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
2026-03-30 07:07:10 -05:00
|
|
|
<span className="text-muted-foreground">Project</span>
|
2026-03-19 11:36:01 -05:00
|
|
|
)
|
2026-03-30 07:07:10 -05:00
|
|
|
}
|
|
|
|
|
renderOption={(option) => {
|
|
|
|
|
if (!option.id) return <span className="truncate">{option.label}</span>;
|
|
|
|
|
const project = projectById.get(option.id);
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<span
|
|
|
|
|
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
|
|
|
|
style={{ backgroundColor: project?.color ?? "#64748b" }}
|
|
|
|
|
/>
|
|
|
|
|
<span className="truncate">{option.label}</span>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-03-19 11:36:01 -05:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-30 07:07:10 -05:00
|
|
|
<div className="border-t border-border/60 px-5 py-4">
|
|
|
|
|
<MarkdownEditor
|
|
|
|
|
ref={descriptionEditorRef}
|
|
|
|
|
value={draft.description}
|
|
|
|
|
onChange={(description) => setDraft((current) => ({ ...current, description }))}
|
|
|
|
|
placeholder="Add instructions..."
|
|
|
|
|
bordered={false}
|
|
|
|
|
contentClassName="min-h-[160px] text-sm text-muted-foreground"
|
|
|
|
|
onSubmit={() => {
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## 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
- [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
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
if (!createRoutine.isPending && draft.title.trim() && draft.projectId && draft.assigneeAgentId) {
|
2026-03-30 07:07:10 -05:00
|
|
|
createRoutine.mutate();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-03-19 11:36:01 -05:00
|
|
|
|
2026-03-30 07:07:10 -05:00
|
|
|
<div className="border-t border-border/60 px-5 py-3">
|
|
|
|
|
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
|
|
|
|
|
<CollapsibleTrigger className="flex w-full items-center justify-between text-left">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium">Advanced delivery settings</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">Keep policy controls secondary to the work definition.</p>
|
2026-03-19 11:36:01 -05:00
|
|
|
</div>
|
2026-03-30 07:07:10 -05:00
|
|
|
{advancedOpen ? <ChevronDown className="h-4 w-4 text-muted-foreground" /> : <ChevronRight className="h-4 w-4 text-muted-foreground" />}
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<CollapsibleContent className="pt-3">
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Concurrency</p>
|
|
|
|
|
<Select
|
|
|
|
|
value={draft.concurrencyPolicy}
|
|
|
|
|
onValueChange={(concurrencyPolicy) => setDraft((current) => ({ ...current, concurrencyPolicy }))}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{concurrencyPolicies.map((value) => (
|
|
|
|
|
<SelectItem key={value} value={value}>{value.replaceAll("_", " ")}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{concurrencyPolicyDescriptions[draft.concurrencyPolicy]}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<p className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">Catch-up</p>
|
|
|
|
|
<Select
|
|
|
|
|
value={draft.catchUpPolicy}
|
|
|
|
|
onValueChange={(catchUpPolicy) => setDraft((current) => ({ ...current, catchUpPolicy }))}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{catchUpPolicies.map((value) => (
|
|
|
|
|
<SelectItem key={value} value={value}>{value.replaceAll("_", " ")}</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{catchUpPolicyDescriptions[draft.catchUpPolicy]}</p>
|
|
|
|
|
</div>
|
2026-03-19 11:36:01 -05:00
|
|
|
</div>
|
2026-03-30 07:07:10 -05:00
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
</div>
|
2026-03-19 08:39:24 -05:00
|
|
|
</div>
|
2026-03-19 11:36:01 -05:00
|
|
|
|
2026-03-30 07:07:10 -05:00
|
|
|
<div className="shrink-0 flex flex-col gap-3 border-t border-border/60 px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
|
2026-03-19 11:36:01 -05:00
|
|
|
<div className="text-sm text-muted-foreground">
|
2026-04-09 10:19:52 -05:00
|
|
|
After creation, Paperclip takes you straight to trigger setup. Draft routines stay paused until you add a default agent.
|
2026-03-19 11:36:01 -05:00
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col gap-2 sm:items-end">
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => createRoutine.mutate()}
|
|
|
|
|
disabled={
|
|
|
|
|
createRoutine.isPending ||
|
2026-04-09 10:19:52 -05:00
|
|
|
!draft.title.trim()
|
2026-03-19 11:36:01 -05:00
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
|
|
|
{createRoutine.isPending ? "Creating..." : "Create routine"}
|
|
|
|
|
</Button>
|
|
|
|
|
{createRoutine.isError ? (
|
|
|
|
|
<p className="text-sm text-destructive">
|
|
|
|
|
{createRoutine.error instanceof Error ? createRoutine.error.message : "Failed to create routine"}
|
|
|
|
|
</p>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2026-03-19 08:39:24 -05:00
|
|
|
</div>
|
2026-03-19 12:07:49 -05:00
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
2026-03-19 08:39:24 -05:00
|
|
|
|
2026-03-19 11:36:01 -05:00
|
|
|
{error ? (
|
2026-03-19 08:39:24 -05:00
|
|
|
<Card>
|
|
|
|
|
<CardContent className="pt-6 text-sm text-destructive">
|
|
|
|
|
{error instanceof Error ? error.message : "Failed to load routines"}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
2026-03-19 11:36:01 -05:00
|
|
|
) : null}
|
2026-03-19 08:39:24 -05:00
|
|
|
|
2026-04-04 13:38:34 -05:00
|
|
|
{activeTab === "routines" ? (
|
|
|
|
|
<div>
|
|
|
|
|
{(routines ?? []).length === 0 ? (
|
|
|
|
|
<div className="py-12">
|
|
|
|
|
<EmptyState
|
|
|
|
|
icon={Repeat}
|
|
|
|
|
message="No routines yet. Use Create routine to define the first recurring workflow."
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="rounded-lg border border-border">
|
|
|
|
|
{routineGroups.map((group) => (
|
|
|
|
|
<Collapsible
|
|
|
|
|
key={group.key}
|
|
|
|
|
open={!routineViewState.collapsedGroups.includes(group.key)}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
updateRoutineView({
|
|
|
|
|
collapsedGroups: open
|
|
|
|
|
? routineViewState.collapsedGroups.filter((item) => item !== group.key)
|
|
|
|
|
: [...routineViewState.collapsedGroups, group.key],
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{group.label ? (
|
|
|
|
|
<div className="flex items-center gap-2 border-b border-border px-3 py-2">
|
|
|
|
|
<CollapsibleTrigger className="flex items-center gap-1.5">
|
|
|
|
|
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90" />
|
|
|
|
|
<span className="text-sm font-semibold uppercase tracking-wide">
|
|
|
|
|
{group.label}
|
|
|
|
|
</span>
|
|
|
|
|
</CollapsibleTrigger>
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
{group.items.length}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
<CollapsibleContent>
|
|
|
|
|
{group.items.map((routine) => (
|
|
|
|
|
<RoutineListRow
|
|
|
|
|
key={routine.id}
|
|
|
|
|
routine={routine}
|
|
|
|
|
projectById={projectById}
|
|
|
|
|
agentById={agentById}
|
|
|
|
|
runningRoutineId={runningRoutineId}
|
|
|
|
|
statusMutationRoutineId={statusMutationRoutineId}
|
2026-04-10 22:26:21 -05:00
|
|
|
href={`/routines/${routine.id}`}
|
2026-04-04 13:38:34 -05:00
|
|
|
onRunNow={handleRunNow}
|
|
|
|
|
onToggleEnabled={handleToggleEnabled}
|
|
|
|
|
onToggleArchived={handleToggleArchived}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</CollapsibleContent>
|
|
|
|
|
</Collapsible>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-04-02 11:38:57 -05:00
|
|
|
|
|
|
|
|
<RoutineRunVariablesDialog
|
|
|
|
|
open={runDialogRoutine !== null}
|
|
|
|
|
onOpenChange={(next) => {
|
|
|
|
|
if (!next) setRunDialogRoutine(null);
|
|
|
|
|
}}
|
|
|
|
|
companyId={selectedCompanyId}
|
[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>
2026-04-15 15:54:05 -05:00
|
|
|
routineName={runDialogRoutine?.title ?? null}
|
2026-04-09 10:19:52 -05:00
|
|
|
agents={agents ?? []}
|
|
|
|
|
projects={projects ?? []}
|
|
|
|
|
defaultProjectId={runDialogRoutine?.projectId ?? null}
|
|
|
|
|
defaultAssigneeAgentId={runDialogRoutine?.assigneeAgentId ?? null}
|
2026-04-02 11:38:57 -05:00
|
|
|
variables={runDialogRoutine?.variables ?? []}
|
|
|
|
|
isPending={runRoutine.isPending}
|
|
|
|
|
onSubmit={(data) => {
|
|
|
|
|
if (!runDialogRoutine) return;
|
|
|
|
|
runRoutine.mutate({ id: runDialogRoutine.id, data });
|
|
|
|
|
}}
|
|
|
|
|
/>
|
2026-03-19 08:39:24 -05:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|