2026-04-06 10:58:59 -05:00
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent } from "react";
|
2026-03-23 07:48:50 -05:00
|
|
|
import { pickTextColorForPillBg } from "@/lib/color-contrast";
|
2026-03-10 20:59:55 -05:00
|
|
|
import { Link, useLocation, useNavigate, useParams } from "@/lib/router";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { issuesApi } from "../api/issues";
|
2026-04-06 10:36:31 -05:00
|
|
|
import { approvalsApi } from "../api/approvals";
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
import { activityApi } from "../api/activity";
|
2026-02-25 17:40:13 -06:00
|
|
|
import { heartbeatsApi } from "../api/heartbeats";
|
2026-04-02 11:51:40 -05:00
|
|
|
import { instanceSettingsApi } from "../api/instanceSettings";
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
import { agentsApi } from "../api/agents";
|
2026-02-26 16:33:48 -06:00
|
|
|
import { authApi } from "../api/auth";
|
2026-02-20 09:01:28 -06:00
|
|
|
import { projectsApi } from "../api/projects";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { useCompany } from "../context/CompanyContext";
|
2026-04-06 10:58:59 -05:00
|
|
|
import { useDialog } from "../context/DialogContext";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { usePanel } from "../context/PanelContext";
|
2026-03-17 11:12:56 -05:00
|
|
|
import { useToast } from "../context/ToastContext";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { useBreadcrumbs } from "../context/BreadcrumbContext";
|
2026-03-20 06:05:05 -05:00
|
|
|
import { assigneeValueFromSelection, suggestedCommentAssigneeValue } from "../lib/assignees";
|
2026-04-02 11:51:40 -05:00
|
|
|
import { extractIssueTimelineEvents } from "../lib/issue-timeline-events";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { queryKeys } from "../lib/queryKeys";
|
2026-04-02 11:51:40 -05:00
|
|
|
import {
|
2026-04-06 06:32:25 -05:00
|
|
|
hasLegacyIssueDetailQuery,
|
2026-04-02 11:51:40 -05:00
|
|
|
createIssueDetailPath,
|
2026-04-06 06:32:25 -05:00
|
|
|
readIssueDetailLocationState,
|
2026-04-02 11:51:40 -05:00
|
|
|
readIssueDetailBreadcrumb,
|
2026-04-06 06:32:25 -05:00
|
|
|
rememberIssueDetailLocationState,
|
2026-04-02 11:51:40 -05:00
|
|
|
} from "../lib/issueDetailBreadcrumb";
|
2026-04-08 08:27:34 -05:00
|
|
|
import {
|
|
|
|
|
hasBlockingShortcutDialog,
|
2026-04-08 09:24:32 -05:00
|
|
|
resolveIssueDetailGoKeyAction,
|
2026-04-08 08:27:34 -05:00
|
|
|
resolveInboxQuickArchiveKeyAction,
|
|
|
|
|
} from "../lib/keyboardShortcuts";
|
2026-03-28 09:46:34 -05:00
|
|
|
import {
|
2026-04-07 18:11:41 -05:00
|
|
|
applyOptimisticIssueFieldUpdate,
|
|
|
|
|
applyOptimisticIssueFieldUpdateToCollection,
|
2026-03-28 09:46:34 -05:00
|
|
|
applyOptimisticIssueCommentUpdate,
|
|
|
|
|
createOptimisticIssueComment,
|
2026-03-28 11:25:25 -05:00
|
|
|
isQueuedIssueComment,
|
2026-04-07 18:11:41 -05:00
|
|
|
matchesIssueRef,
|
2026-03-28 09:46:34 -05:00
|
|
|
mergeIssueComments,
|
|
|
|
|
upsertIssueComment,
|
|
|
|
|
type IssueCommentReassignment,
|
|
|
|
|
type OptimisticIssueComment,
|
|
|
|
|
} from "../lib/optimistic-issue-comments";
|
2026-03-02 14:20:49 -06:00
|
|
|
import { useProjectOrder } from "../hooks/useProjectOrder";
|
2026-03-14 22:00:12 -05:00
|
|
|
import { relativeTime, cn, formatTokens, visibleRunCostUsd } from "../lib/utils";
|
2026-04-06 10:36:31 -05:00
|
|
|
import { ApprovalCard } from "../components/ApprovalCard";
|
2026-04-06 11:20:46 -05:00
|
|
|
import { InlineEditor } from "../components/InlineEditor";
|
2026-04-08 09:24:32 -05:00
|
|
|
import { IssueChatThread, type IssueChatComposerHandle } from "../components/IssueChatThread";
|
2026-03-13 21:30:48 -05:00
|
|
|
import { IssueDocumentsSection } from "../components/IssueDocumentsSection";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { IssueProperties } from "../components/IssueProperties";
|
2026-03-26 07:56:36 -05:00
|
|
|
import { IssueWorkspaceCard } from "../components/IssueWorkspaceCard";
|
2026-03-02 13:31:58 -06:00
|
|
|
import type { MentionOption } from "../components/MarkdownEditor";
|
2026-04-02 11:51:40 -05:00
|
|
|
import { ImageGalleryModal } from "../components/ImageGalleryModal";
|
2026-03-07 20:07:39 -06:00
|
|
|
import { ScrollToBottom } from "../components/ScrollToBottom";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { StatusIcon } from "../components/StatusIcon";
|
|
|
|
|
import { PriorityIcon } from "../components/PriorityIcon";
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
import { Identity } from "../components/Identity";
|
2026-03-13 23:03:51 -05:00
|
|
|
import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slots";
|
|
|
|
|
import { PluginLauncherOutlet } from "@/plugins/launchers";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
import { Separator } from "@/components/ui/separator";
|
2026-02-19 15:44:05 -06:00
|
|
|
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
2026-02-23 19:49:43 -06:00
|
|
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
|
|
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
2026-02-25 08:39:31 -06:00
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
|
|
import {
|
|
|
|
|
Activity as ActivityIcon,
|
2026-03-17 11:12:56 -05:00
|
|
|
Check,
|
2026-02-25 08:39:31 -06:00
|
|
|
ChevronRight,
|
2026-03-17 11:12:56 -05:00
|
|
|
Copy,
|
2026-02-25 08:39:31 -06:00
|
|
|
EyeOff,
|
|
|
|
|
Hexagon,
|
|
|
|
|
ListTree,
|
|
|
|
|
MessageSquare,
|
|
|
|
|
MoreHorizontal,
|
|
|
|
|
Paperclip,
|
2026-03-20 09:13:30 -05:00
|
|
|
Repeat,
|
2026-02-25 08:39:31 -06:00
|
|
|
SlidersHorizontal,
|
|
|
|
|
Trash2,
|
|
|
|
|
} from "lucide-react";
|
2026-04-04 13:04:34 -05:00
|
|
|
import {
|
|
|
|
|
getClosedIsolatedExecutionWorkspaceMessage,
|
|
|
|
|
isClosedIsolatedExecutionWorkspace,
|
|
|
|
|
type ActivityEvent,
|
|
|
|
|
type Agent,
|
|
|
|
|
type FeedbackVote,
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## 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
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-08 17:54:03 -07:00
|
|
|
type FeedbackVoteValue,
|
2026-04-04 13:04:34 -05:00
|
|
|
type Issue,
|
|
|
|
|
type IssueAttachment,
|
|
|
|
|
type IssueComment,
|
|
|
|
|
} from "@paperclipai/shared";
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
|
2026-03-28 09:46:34 -05:00
|
|
|
type CommentReassignment = IssueCommentReassignment;
|
2026-03-28 11:25:25 -05:00
|
|
|
type IssueDetailComment = (IssueComment | OptimisticIssueComment) & {
|
|
|
|
|
runId?: string | null;
|
|
|
|
|
runAgentId?: string | null;
|
|
|
|
|
interruptedRunId?: string | null;
|
|
|
|
|
queueState?: "queued";
|
|
|
|
|
queueTargetRunId?: string | null;
|
|
|
|
|
};
|
2026-02-26 16:33:48 -06:00
|
|
|
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## 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
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-08 17:54:03 -07:00
|
|
|
const ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS = 3000;
|
|
|
|
|
const IDLE_ISSUE_RUN_POLL_INTERVAL_MS = 30000;
|
|
|
|
|
const ACTIVE_ISSUE_TIMELINE_POLL_INTERVAL_MS = 5000;
|
|
|
|
|
const IDLE_ISSUE_TIMELINE_POLL_INTERVAL_MS = 30000;
|
|
|
|
|
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
const ACTION_LABELS: Record<string, string> = {
|
|
|
|
|
"issue.created": "created the issue",
|
|
|
|
|
"issue.updated": "updated the issue",
|
|
|
|
|
"issue.checked_out": "checked out the issue",
|
|
|
|
|
"issue.released": "released the issue",
|
|
|
|
|
"issue.comment_added": "added a comment",
|
2026-04-02 09:11:49 -05:00
|
|
|
"issue.feedback_vote_saved": "saved feedback on an AI output",
|
2026-02-20 11:29:13 -06:00
|
|
|
"issue.attachment_added": "added an attachment",
|
|
|
|
|
"issue.attachment_removed": "removed an attachment",
|
2026-03-13 21:30:48 -05:00
|
|
|
"issue.document_created": "created a document",
|
|
|
|
|
"issue.document_updated": "updated a document",
|
|
|
|
|
"issue.document_deleted": "deleted a document",
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
"issue.deleted": "deleted the issue",
|
|
|
|
|
"agent.created": "created an agent",
|
|
|
|
|
"agent.updated": "updated the agent",
|
|
|
|
|
"agent.paused": "paused the agent",
|
|
|
|
|
"agent.resumed": "resumed the agent",
|
|
|
|
|
"agent.terminated": "terminated the agent",
|
|
|
|
|
"heartbeat.invoked": "invoked a heartbeat",
|
|
|
|
|
"heartbeat.cancelled": "cancelled a heartbeat",
|
|
|
|
|
"approval.created": "requested approval",
|
|
|
|
|
"approval.approved": "approved",
|
|
|
|
|
"approval.rejected": "rejected",
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-02 09:11:49 -05:00
|
|
|
const FEEDBACK_TERMS_URL = import.meta.env.VITE_FEEDBACK_TERMS_URL?.trim() || "https://paperclip.ing/tos";
|
|
|
|
|
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
function humanizeValue(value: unknown): string {
|
|
|
|
|
if (typeof value !== "string") return String(value ?? "none");
|
|
|
|
|
return value.replace(/_/g, " ");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 10:32:32 -06:00
|
|
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
|
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
|
|
|
return value as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function usageNumber(usage: Record<string, unknown> | null, ...keys: string[]) {
|
|
|
|
|
if (!usage) return 0;
|
|
|
|
|
for (const key of keys) {
|
|
|
|
|
const value = usage[key];
|
|
|
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 15:48:42 -06:00
|
|
|
function truncate(text: string, max: number): string {
|
|
|
|
|
if (text.length <= max) return text;
|
|
|
|
|
return text.slice(0, max - 1) + "\u2026";
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 21:30:48 -05:00
|
|
|
function isMarkdownFile(file: File) {
|
|
|
|
|
const name = file.name.toLowerCase();
|
|
|
|
|
return (
|
|
|
|
|
name.endsWith(".md") ||
|
|
|
|
|
name.endsWith(".markdown") ||
|
|
|
|
|
file.type === "text/markdown"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fileBaseName(filename: string) {
|
|
|
|
|
return filename.replace(/\.[^.]+$/, "");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function slugifyDocumentKey(input: string) {
|
|
|
|
|
const slug = input
|
|
|
|
|
.trim()
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
|
|
|
.replace(/^-+|-+$/g, "");
|
|
|
|
|
return slug || "document";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function titleizeFilename(input: string) {
|
|
|
|
|
return input
|
|
|
|
|
.split(/[-_ ]+/g)
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
|
|
|
.join(" ");
|
|
|
|
|
}
|
|
|
|
|
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
function formatAction(action: string, details?: Record<string, unknown> | null): string {
|
|
|
|
|
if (action === "issue.updated" && details) {
|
|
|
|
|
const previous = (details._previous ?? {}) as Record<string, unknown>;
|
|
|
|
|
const parts: string[] = [];
|
|
|
|
|
|
|
|
|
|
if (details.status !== undefined) {
|
|
|
|
|
const from = previous.status;
|
|
|
|
|
parts.push(
|
|
|
|
|
from
|
|
|
|
|
? `changed the status from ${humanizeValue(from)} to ${humanizeValue(details.status)}`
|
|
|
|
|
: `changed the status to ${humanizeValue(details.status)}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (details.priority !== undefined) {
|
|
|
|
|
const from = previous.priority;
|
|
|
|
|
parts.push(
|
|
|
|
|
from
|
|
|
|
|
? `changed the priority from ${humanizeValue(from)} to ${humanizeValue(details.priority)}`
|
|
|
|
|
: `changed the priority to ${humanizeValue(details.priority)}`
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-26 16:33:48 -06:00
|
|
|
if (details.assigneeAgentId !== undefined || details.assigneeUserId !== undefined) {
|
|
|
|
|
parts.push(
|
|
|
|
|
details.assigneeAgentId || details.assigneeUserId
|
|
|
|
|
? "assigned the issue"
|
|
|
|
|
: "unassigned the issue",
|
|
|
|
|
);
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
}
|
|
|
|
|
if (details.title !== undefined) parts.push("updated the title");
|
|
|
|
|
if (details.description !== undefined) parts.push("updated the description");
|
|
|
|
|
|
|
|
|
|
if (parts.length > 0) return parts.join(", ");
|
|
|
|
|
}
|
2026-03-13 21:30:48 -05:00
|
|
|
if (
|
|
|
|
|
(action === "issue.document_created" || action === "issue.document_updated" || action === "issue.document_deleted") &&
|
|
|
|
|
details
|
|
|
|
|
) {
|
|
|
|
|
const key = typeof details.key === "string" ? details.key : "document";
|
|
|
|
|
const title = typeof details.title === "string" && details.title ? ` (${details.title})` : "";
|
|
|
|
|
return `${ACTION_LABELS[action] ?? action} ${key}${title}`;
|
|
|
|
|
}
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
return ACTION_LABELS[action] ?? action.replace(/[._]/g, " ");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-02 09:11:49 -05:00
|
|
|
function mergeOptimisticFeedbackVote(
|
|
|
|
|
previousVotes: FeedbackVote[] | undefined,
|
|
|
|
|
nextVote: {
|
|
|
|
|
issueId: string;
|
|
|
|
|
targetType: "issue_comment" | "issue_document_revision";
|
|
|
|
|
targetId: string;
|
|
|
|
|
vote: "up" | "down";
|
|
|
|
|
reason?: string;
|
|
|
|
|
},
|
|
|
|
|
currentUserId: string | null,
|
|
|
|
|
): FeedbackVote[] {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const existingVotes = previousVotes ?? [];
|
|
|
|
|
const existingIndex = existingVotes.findIndex(
|
|
|
|
|
(feedbackVote) =>
|
|
|
|
|
feedbackVote.targetType === nextVote.targetType &&
|
|
|
|
|
feedbackVote.targetId === nextVote.targetId &&
|
|
|
|
|
(!currentUserId || feedbackVote.authorUserId === currentUserId),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (existingIndex >= 0) {
|
|
|
|
|
const existingVote = existingVotes[existingIndex]!;
|
|
|
|
|
const updatedVote: FeedbackVote = {
|
|
|
|
|
...existingVote,
|
|
|
|
|
vote: nextVote.vote,
|
|
|
|
|
reason:
|
|
|
|
|
nextVote.reason !== undefined
|
|
|
|
|
? nextVote.reason.trim() || null
|
|
|
|
|
: existingVote.reason,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
};
|
|
|
|
|
const nextVotes = [...existingVotes];
|
|
|
|
|
nextVotes[existingIndex] = updatedVote;
|
|
|
|
|
return nextVotes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
...existingVotes,
|
|
|
|
|
{
|
|
|
|
|
id: `optimistic:${nextVote.targetType}:${nextVote.targetId}`,
|
|
|
|
|
companyId: "",
|
|
|
|
|
issueId: nextVote.issueId,
|
|
|
|
|
targetType: nextVote.targetType,
|
|
|
|
|
targetId: nextVote.targetId,
|
|
|
|
|
authorUserId: currentUserId ?? "current-user",
|
|
|
|
|
vote: nextVote.vote,
|
|
|
|
|
reason: nextVote.reason?.trim() || null,
|
|
|
|
|
sharedWithLabs: false,
|
|
|
|
|
sharedAt: null,
|
|
|
|
|
consentVersion: null,
|
|
|
|
|
redactionSummary: null,
|
|
|
|
|
createdAt: now,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
function ActorIdentity({ evt, agentMap }: { evt: ActivityEvent; agentMap: Map<string, Agent> }) {
|
|
|
|
|
const id = evt.actorId;
|
|
|
|
|
if (evt.actorType === "agent") {
|
|
|
|
|
const agent = agentMap.get(id);
|
|
|
|
|
return <Identity name={agent?.name ?? id.slice(0, 8)} size="sm" />;
|
|
|
|
|
}
|
|
|
|
|
if (evt.actorType === "system") return <Identity name="System" size="sm" />;
|
2026-02-23 19:44:02 -06:00
|
|
|
if (evt.actorType === "user") return <Identity name="Board" size="sm" />;
|
|
|
|
|
return <Identity name={id || "Unknown"} size="sm" />;
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
|
|
|
|
export function IssueDetail() {
|
|
|
|
|
const { issueId } = useParams<{ issueId: string }>();
|
2026-04-02 09:11:49 -05:00
|
|
|
const { selectedCompanyId, selectedCompany } = useCompany();
|
2026-04-06 10:58:59 -05:00
|
|
|
const { openNewIssue } = useDialog();
|
2026-03-03 14:56:32 -06:00
|
|
|
const { openPanel, closePanel, panelVisible, setPanelVisible } = usePanel();
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
const { setBreadcrumbs } = useBreadcrumbs();
|
2026-02-17 12:24:48 -06:00
|
|
|
const queryClient = useQueryClient();
|
2026-02-19 15:44:05 -06:00
|
|
|
const navigate = useNavigate();
|
2026-03-10 20:59:55 -05:00
|
|
|
const location = useLocation();
|
2026-03-17 11:12:56 -05:00
|
|
|
const { pushToast } = useToast();
|
2026-02-19 15:44:05 -06:00
|
|
|
const [moreOpen, setMoreOpen] = useState(false);
|
2026-03-17 11:12:56 -05:00
|
|
|
const [copied, setCopied] = useState(false);
|
2026-02-23 19:49:43 -06:00
|
|
|
const [mobilePropsOpen, setMobilePropsOpen] = useState(false);
|
2026-04-06 07:53:37 -05:00
|
|
|
const [detailTab, setDetailTab] = useState("chat");
|
2026-04-06 10:36:31 -05:00
|
|
|
const [pendingApprovalAction, setPendingApprovalAction] = useState<{
|
|
|
|
|
approvalId: string;
|
|
|
|
|
action: "approve" | "reject";
|
|
|
|
|
} | null>(null);
|
2026-04-06 21:20:18 -05:00
|
|
|
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
|
2026-02-20 10:32:32 -06:00
|
|
|
const [attachmentError, setAttachmentError] = useState<string | null>(null);
|
2026-03-13 21:30:48 -05:00
|
|
|
const [attachmentDragActive, setAttachmentDragActive] = useState(false);
|
2026-04-02 11:51:40 -05:00
|
|
|
const [galleryOpen, setGalleryOpen] = useState(false);
|
|
|
|
|
const [galleryIndex, setGalleryIndex] = useState(0);
|
2026-03-28 09:46:34 -05:00
|
|
|
const [optimisticComments, setOptimisticComments] = useState<OptimisticIssueComment[]>([]);
|
2026-04-08 09:24:32 -05:00
|
|
|
const [pendingCommentComposerFocusKey, setPendingCommentComposerFocusKey] = useState(0);
|
2026-02-20 10:32:32 -06:00
|
|
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
2026-03-06 08:34:19 -06:00
|
|
|
const lastMarkedReadIssueIdRef = useRef<string | null>(null);
|
2026-04-08 09:24:32 -05:00
|
|
|
const commentComposerRef = useRef<IssueChatComposerHandle | null>(null);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const { data: issue, isLoading, error } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.detail(issueId!),
|
|
|
|
|
queryFn: () => issuesApi.get(issueId!),
|
|
|
|
|
enabled: !!issueId,
|
|
|
|
|
});
|
2026-03-13 23:03:51 -05:00
|
|
|
const resolvedCompanyId = issue?.companyId ?? selectedCompanyId;
|
2026-04-04 13:04:34 -05:00
|
|
|
const commentComposerDisabledReason = useMemo(() => {
|
|
|
|
|
if (!issue?.currentExecutionWorkspace || !isClosedIsolatedExecutionWorkspace(issue.currentExecutionWorkspace)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return getClosedIsolatedExecutionWorkspaceMessage(issue.currentExecutionWorkspace);
|
|
|
|
|
}, [issue?.currentExecutionWorkspace]);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const { data: comments } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.comments(issueId!),
|
|
|
|
|
queryFn: () => issuesApi.listComments(issueId!),
|
|
|
|
|
enabled: !!issueId,
|
|
|
|
|
});
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
const { data: activity } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.activity(issueId!),
|
|
|
|
|
queryFn: () => activityApi.forIssue(issueId!),
|
|
|
|
|
enabled: !!issueId,
|
|
|
|
|
});
|
|
|
|
|
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
const { data: linkedApprovals } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.approvals(issueId!),
|
|
|
|
|
queryFn: () => issuesApi.listApprovals(issueId!),
|
|
|
|
|
enabled: !!issueId,
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-20 10:32:32 -06:00
|
|
|
const { data: attachments } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.attachments(issueId!),
|
|
|
|
|
queryFn: () => issuesApi.listAttachments(issueId!),
|
|
|
|
|
enabled: !!issueId,
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-25 17:40:13 -06:00
|
|
|
const { data: liveRuns } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.liveRuns(issueId!),
|
|
|
|
|
queryFn: () => heartbeatsApi.liveRunsForIssue(issueId!),
|
2026-03-03 11:06:41 -06:00
|
|
|
enabled: !!issueId,
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## 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
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-08 17:54:03 -07:00
|
|
|
refetchInterval: (query) => {
|
|
|
|
|
const data = query.state.data as Array<unknown> | undefined;
|
|
|
|
|
return data && data.length > 0
|
|
|
|
|
? ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS
|
|
|
|
|
: IDLE_ISSUE_RUN_POLL_INTERVAL_MS;
|
|
|
|
|
},
|
2026-03-03 11:06:41 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { data: activeRun } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.activeRun(issueId!),
|
|
|
|
|
queryFn: () => heartbeatsApi.activeRunForIssue(issueId!),
|
|
|
|
|
enabled: !!issueId,
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## 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
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-08 17:54:03 -07:00
|
|
|
refetchInterval: (query) =>
|
|
|
|
|
query.state.data
|
|
|
|
|
? ACTIVE_ISSUE_RUN_POLL_INTERVAL_MS
|
|
|
|
|
: IDLE_ISSUE_RUN_POLL_INTERVAL_MS,
|
2026-02-25 17:40:13 -06:00
|
|
|
});
|
|
|
|
|
|
2026-03-03 11:06:41 -06:00
|
|
|
const hasLiveRuns = (liveRuns ?? []).length > 0 || !!activeRun;
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## 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
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-08 17:54:03 -07:00
|
|
|
const { data: linkedRuns } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.runs(issueId!),
|
|
|
|
|
queryFn: () => activityApi.runsForIssue(issueId!),
|
|
|
|
|
enabled: !!issueId,
|
|
|
|
|
refetchInterval: hasLiveRuns
|
|
|
|
|
? ACTIVE_ISSUE_TIMELINE_POLL_INTERVAL_MS
|
|
|
|
|
: IDLE_ISSUE_TIMELINE_POLL_INTERVAL_MS,
|
|
|
|
|
});
|
2026-03-28 11:25:25 -05:00
|
|
|
const runningIssueRun = useMemo(
|
|
|
|
|
() => (
|
|
|
|
|
activeRun?.status === "running"
|
|
|
|
|
? activeRun
|
|
|
|
|
: (liveRuns ?? []).find((run) => run.status === "running") ?? null
|
|
|
|
|
),
|
|
|
|
|
[activeRun, liveRuns],
|
|
|
|
|
);
|
2026-04-06 06:32:25 -05:00
|
|
|
const resolvedIssueDetailState = useMemo(
|
|
|
|
|
() => readIssueDetailLocationState(issueId, location.state, location.search),
|
|
|
|
|
[issueId, location.state, location.search],
|
|
|
|
|
);
|
2026-03-10 20:59:55 -05:00
|
|
|
const sourceBreadcrumb = useMemo(
|
2026-04-06 06:32:25 -05:00
|
|
|
() => readIssueDetailBreadcrumb(issueId, location.state, location.search) ?? { label: "Issues", href: "/issues" },
|
|
|
|
|
[issueId, location.state, location.search],
|
2026-03-10 20:59:55 -05:00
|
|
|
);
|
2026-02-25 17:40:13 -06:00
|
|
|
|
2026-03-03 12:19:26 -06:00
|
|
|
// Filter out runs already shown by the live widget to avoid duplication
|
|
|
|
|
const timelineRuns = useMemo(() => {
|
|
|
|
|
const liveIds = new Set<string>();
|
|
|
|
|
for (const r of liveRuns ?? []) liveIds.add(r.id);
|
|
|
|
|
if (activeRun) liveIds.add(activeRun.id);
|
|
|
|
|
if (liveIds.size === 0) return linkedRuns ?? [];
|
|
|
|
|
return (linkedRuns ?? []).filter((r) => !liveIds.has(r.runId));
|
|
|
|
|
}, [linkedRuns, liveRuns, activeRun]);
|
|
|
|
|
|
2026-02-20 14:48:30 -06:00
|
|
|
const { data: allIssues } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => issuesApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId,
|
|
|
|
|
});
|
|
|
|
|
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
const { data: agents } = useQuery({
|
|
|
|
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId,
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-26 16:33:48 -06:00
|
|
|
const { data: session } = useQuery({
|
|
|
|
|
queryKey: queryKeys.auth.session,
|
|
|
|
|
queryFn: () => authApi.getSession(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-20 09:01:28 -06:00
|
|
|
const { data: projects } = useQuery({
|
|
|
|
|
queryKey: queryKeys.projects.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => projectsApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId,
|
|
|
|
|
});
|
2026-03-02 14:20:49 -06:00
|
|
|
const currentUserId = session?.user?.id ?? session?.session?.userId ?? null;
|
2026-04-02 09:11:49 -05:00
|
|
|
const { data: feedbackVotes } = useQuery({
|
|
|
|
|
queryKey: queryKeys.issues.feedbackVotes(issueId!),
|
|
|
|
|
queryFn: () => issuesApi.listFeedbackVotes(issueId!),
|
|
|
|
|
enabled: !!issueId && !!currentUserId,
|
|
|
|
|
});
|
|
|
|
|
const { data: instanceGeneralSettings } = useQuery({
|
|
|
|
|
queryKey: queryKeys.instance.generalSettings,
|
|
|
|
|
queryFn: () => instanceSettingsApi.getGeneral(),
|
|
|
|
|
enabled: !!issueId,
|
|
|
|
|
retry: false,
|
|
|
|
|
});
|
2026-04-02 11:51:40 -05:00
|
|
|
const keyboardShortcutsEnabled = instanceGeneralSettings?.keyboardShortcuts === true;
|
2026-04-02 09:11:49 -05:00
|
|
|
const feedbackDataSharingPreference = instanceGeneralSettings?.feedbackDataSharingPreference ?? "prompt";
|
2026-03-02 14:20:49 -06:00
|
|
|
const { orderedProjects } = useProjectOrder({
|
|
|
|
|
projects: projects ?? [],
|
|
|
|
|
companyId: selectedCompanyId,
|
|
|
|
|
userId: currentUserId,
|
|
|
|
|
});
|
2026-03-13 23:03:51 -05:00
|
|
|
const { slots: issuePluginDetailSlots } = usePluginSlots({
|
|
|
|
|
slotTypes: ["detailTab"],
|
|
|
|
|
entityType: "issue",
|
|
|
|
|
companyId: resolvedCompanyId,
|
|
|
|
|
enabled: !!resolvedCompanyId,
|
|
|
|
|
});
|
|
|
|
|
const issuePluginTabItems = useMemo(
|
|
|
|
|
() => issuePluginDetailSlots.map((slot) => ({
|
|
|
|
|
value: `plugin:${slot.pluginKey}:${slot.id}`,
|
|
|
|
|
label: slot.displayName,
|
|
|
|
|
slot,
|
|
|
|
|
})),
|
|
|
|
|
[issuePluginDetailSlots],
|
|
|
|
|
);
|
|
|
|
|
const activePluginTab = issuePluginTabItems.find((item) => item.value === detailTab) ?? null;
|
2026-02-20 09:01:28 -06:00
|
|
|
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
const agentMap = useMemo(() => {
|
|
|
|
|
const map = new Map<string, Agent>();
|
|
|
|
|
for (const a of agents ?? []) map.set(a.id, a);
|
|
|
|
|
return map;
|
|
|
|
|
}, [agents]);
|
|
|
|
|
|
2026-03-02 13:31:58 -06:00
|
|
|
const mentionOptions = useMemo<MentionOption[]>(() => {
|
|
|
|
|
const options: MentionOption[] = [];
|
|
|
|
|
const activeAgents = [...(agents ?? [])]
|
|
|
|
|
.filter((agent) => agent.status !== "terminated")
|
|
|
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
for (const agent of activeAgents) {
|
|
|
|
|
options.push({
|
|
|
|
|
id: `agent:${agent.id}`,
|
|
|
|
|
name: agent.name,
|
|
|
|
|
kind: "agent",
|
2026-03-21 14:48:10 -05:00
|
|
|
agentId: agent.id,
|
|
|
|
|
agentIcon: agent.icon,
|
2026-03-02 13:31:58 -06:00
|
|
|
});
|
|
|
|
|
}
|
2026-03-02 14:20:49 -06:00
|
|
|
for (const project of orderedProjects) {
|
2026-03-02 13:31:58 -06:00
|
|
|
options.push({
|
|
|
|
|
id: `project:${project.id}`,
|
|
|
|
|
name: project.name,
|
|
|
|
|
kind: "project",
|
|
|
|
|
projectId: project.id,
|
|
|
|
|
projectColor: project.color,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return options;
|
2026-03-02 14:20:49 -06:00
|
|
|
}, [agents, orderedProjects]);
|
2026-03-02 13:31:58 -06:00
|
|
|
|
2026-02-20 14:48:30 -06:00
|
|
|
const childIssues = useMemo(() => {
|
2026-02-20 16:04:05 -06:00
|
|
|
if (!allIssues || !issue) return [];
|
2026-02-20 14:48:30 -06:00
|
|
|
return allIssues
|
2026-02-20 16:04:05 -06:00
|
|
|
.filter((i) => i.parentId === issue.id)
|
2026-02-20 14:48:30 -06:00
|
|
|
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
2026-02-20 16:04:05 -06:00
|
|
|
}, [allIssues, issue]);
|
2026-04-06 11:20:46 -05:00
|
|
|
const childIssuesPanelKey = useMemo(
|
|
|
|
|
() => childIssues.map((child) => `${child.id}:${String(child.updatedAt)}`).join("|"),
|
|
|
|
|
[childIssues],
|
|
|
|
|
);
|
|
|
|
|
const issuePanelKey = issue
|
|
|
|
|
? `${issue.id}:${String(issue.updatedAt)}:${childIssuesPanelKey}`
|
|
|
|
|
: "";
|
2026-04-06 10:58:59 -05:00
|
|
|
const openNewSubIssue = useCallback(() => {
|
|
|
|
|
if (!issue) return;
|
|
|
|
|
openNewIssue({
|
|
|
|
|
parentId: issue.id,
|
|
|
|
|
parentIdentifier: issue.identifier ?? undefined,
|
|
|
|
|
parentTitle: issue.title,
|
|
|
|
|
projectId: issue.projectId ?? undefined,
|
2026-04-06 11:34:11 -05:00
|
|
|
projectWorkspaceId: issue.projectWorkspaceId ?? undefined,
|
2026-04-06 10:58:59 -05:00
|
|
|
goalId: issue.goalId ?? undefined,
|
2026-04-06 11:34:11 -05:00
|
|
|
executionWorkspaceId: issue.executionWorkspaceId ?? undefined,
|
|
|
|
|
executionWorkspaceMode: issue.executionWorkspaceId ? "reuse_existing" : issue.executionWorkspacePreference ?? undefined,
|
|
|
|
|
parentExecutionWorkspaceLabel:
|
|
|
|
|
issue.currentExecutionWorkspace?.name
|
|
|
|
|
?? issue.currentExecutionWorkspace?.branchName
|
|
|
|
|
?? issue.currentExecutionWorkspace?.cwd
|
|
|
|
|
?? issue.executionWorkspaceId
|
|
|
|
|
?? undefined,
|
2026-04-06 10:58:59 -05:00
|
|
|
});
|
2026-04-06 11:34:11 -05:00
|
|
|
}, [
|
|
|
|
|
issue?.currentExecutionWorkspace?.branchName,
|
|
|
|
|
issue?.currentExecutionWorkspace?.cwd,
|
|
|
|
|
issue?.currentExecutionWorkspace?.name,
|
|
|
|
|
issue?.executionWorkspaceId,
|
|
|
|
|
issue?.executionWorkspacePreference,
|
|
|
|
|
issue?.goalId,
|
|
|
|
|
issue?.id,
|
|
|
|
|
issue?.identifier,
|
|
|
|
|
issue?.projectId,
|
|
|
|
|
issue?.projectWorkspaceId,
|
|
|
|
|
issue?.title,
|
|
|
|
|
openNewIssue,
|
|
|
|
|
]);
|
2026-02-20 14:48:30 -06:00
|
|
|
|
2026-02-26 16:33:48 -06:00
|
|
|
const commentReassignOptions = useMemo(() => {
|
2026-03-02 16:56:05 -06:00
|
|
|
const options: Array<{ id: string; label: string; searchText?: string }> = [];
|
2026-02-26 16:33:48 -06:00
|
|
|
const activeAgents = [...(agents ?? [])]
|
|
|
|
|
.filter((agent) => agent.status !== "terminated")
|
|
|
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
for (const agent of activeAgents) {
|
2026-03-02 16:56:05 -06:00
|
|
|
options.push({ id: `agent:${agent.id}`, label: agent.name });
|
2026-02-26 16:33:48 -06:00
|
|
|
}
|
2026-03-02 16:56:05 -06:00
|
|
|
if (currentUserId) {
|
2026-03-12 16:12:38 -05:00
|
|
|
options.push({ id: `user:${currentUserId}`, label: "Me" });
|
2026-02-26 16:33:48 -06:00
|
|
|
}
|
|
|
|
|
return options;
|
2026-03-02 16:56:05 -06:00
|
|
|
}, [agents, currentUserId]);
|
|
|
|
|
|
2026-03-20 06:05:05 -05:00
|
|
|
const actualAssigneeValue = useMemo(
|
|
|
|
|
() => assigneeValueFromSelection(issue ?? {}),
|
|
|
|
|
[issue],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const suggestedAssigneeValue = useMemo(
|
2026-03-28 09:46:34 -05:00
|
|
|
() =>
|
|
|
|
|
suggestedCommentAssigneeValue(
|
|
|
|
|
issue ?? {},
|
|
|
|
|
mergeIssueComments(comments ?? [], optimisticComments),
|
|
|
|
|
currentUserId,
|
|
|
|
|
),
|
|
|
|
|
[issue, comments, optimisticComments, currentUserId],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const threadComments = useMemo(
|
|
|
|
|
() => mergeIssueComments(comments ?? [], optimisticComments),
|
|
|
|
|
[comments, optimisticComments],
|
2026-03-20 06:05:05 -05:00
|
|
|
);
|
2026-02-26 16:33:48 -06:00
|
|
|
|
2026-03-28 11:25:25 -05:00
|
|
|
const commentsWithRunMeta = useMemo<IssueDetailComment[]>(() => {
|
|
|
|
|
const activeRunStartedAt = runningIssueRun?.startedAt ?? runningIssueRun?.createdAt ?? null;
|
|
|
|
|
const runMetaByCommentId = new Map<string, { runId: string; runAgentId: string | null; interruptedRunId: string | null }>();
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
const agentIdByRunId = new Map<string, string>();
|
|
|
|
|
for (const run of linkedRuns ?? []) {
|
|
|
|
|
agentIdByRunId.set(run.runId, run.agentId);
|
|
|
|
|
}
|
|
|
|
|
for (const evt of activity ?? []) {
|
|
|
|
|
if (evt.action !== "issue.comment_added" || !evt.runId) continue;
|
|
|
|
|
const details = evt.details ?? {};
|
|
|
|
|
const commentId = typeof details["commentId"] === "string" ? details["commentId"] : null;
|
|
|
|
|
if (!commentId || runMetaByCommentId.has(commentId)) continue;
|
2026-03-28 11:25:25 -05:00
|
|
|
const interruptedRunId =
|
|
|
|
|
typeof details["interruptedRunId"] === "string" ? details["interruptedRunId"] : null;
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
runMetaByCommentId.set(commentId, {
|
|
|
|
|
runId: evt.runId,
|
|
|
|
|
runAgentId: evt.agentId ?? agentIdByRunId.get(evt.runId) ?? null,
|
2026-03-28 11:25:25 -05:00
|
|
|
interruptedRunId,
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
});
|
|
|
|
|
}
|
2026-03-28 09:46:34 -05:00
|
|
|
return threadComments.map((comment) => {
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
const meta = runMetaByCommentId.get(comment.id);
|
2026-03-28 11:25:25 -05:00
|
|
|
const nextComment: IssueDetailComment = meta ? { ...comment, ...meta } : { ...comment };
|
|
|
|
|
if (
|
|
|
|
|
isQueuedIssueComment({
|
|
|
|
|
comment: nextComment,
|
|
|
|
|
activeRunStartedAt,
|
2026-04-06 08:27:53 -05:00
|
|
|
activeRunAgentId: runningIssueRun?.agentId ?? null,
|
2026-03-28 11:25:25 -05:00
|
|
|
runId: meta?.runId ?? nextComment.runId ?? null,
|
|
|
|
|
interruptedRunId: meta?.interruptedRunId ?? nextComment.interruptedRunId ?? null,
|
|
|
|
|
})
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
...nextComment,
|
|
|
|
|
queueState: "queued" as const,
|
|
|
|
|
queueTargetRunId: runningIssueRun?.id ?? nextComment.queueTargetRunId ?? null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return nextComment;
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
});
|
2026-03-28 11:25:25 -05:00
|
|
|
}, [activity, threadComments, linkedRuns, runningIssueRun]);
|
|
|
|
|
|
2026-04-02 11:51:40 -05:00
|
|
|
const timelineEvents = useMemo(
|
|
|
|
|
() => extractIssueTimelineEvents(activity),
|
|
|
|
|
[activity],
|
|
|
|
|
);
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
|
2026-02-20 10:32:32 -06:00
|
|
|
const issueCostSummary = useMemo(() => {
|
|
|
|
|
let input = 0;
|
|
|
|
|
let output = 0;
|
|
|
|
|
let cached = 0;
|
|
|
|
|
let cost = 0;
|
|
|
|
|
let hasCost = false;
|
|
|
|
|
let hasTokens = false;
|
|
|
|
|
|
|
|
|
|
for (const run of linkedRuns ?? []) {
|
|
|
|
|
const usage = asRecord(run.usageJson);
|
|
|
|
|
const result = asRecord(run.resultJson);
|
|
|
|
|
const runInput = usageNumber(usage, "inputTokens", "input_tokens");
|
|
|
|
|
const runOutput = usageNumber(usage, "outputTokens", "output_tokens");
|
|
|
|
|
const runCached = usageNumber(
|
|
|
|
|
usage,
|
|
|
|
|
"cachedInputTokens",
|
|
|
|
|
"cached_input_tokens",
|
|
|
|
|
"cache_read_input_tokens",
|
|
|
|
|
);
|
2026-03-14 22:00:12 -05:00
|
|
|
const runCost = visibleRunCostUsd(usage, result);
|
2026-02-20 10:32:32 -06:00
|
|
|
if (runCost > 0) hasCost = true;
|
|
|
|
|
if (runInput + runOutput + runCached > 0) hasTokens = true;
|
|
|
|
|
input += runInput;
|
|
|
|
|
output += runOutput;
|
|
|
|
|
cached += runCached;
|
|
|
|
|
cost += runCost;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
input,
|
|
|
|
|
output,
|
|
|
|
|
cached,
|
|
|
|
|
cost,
|
|
|
|
|
totalTokens: input + output,
|
|
|
|
|
hasCost,
|
|
|
|
|
hasTokens,
|
|
|
|
|
};
|
|
|
|
|
}, [linkedRuns]);
|
|
|
|
|
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
const invalidateIssue = () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(issueId!) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.runs(issueId!) });
|
UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements
Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:03:08 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.approvals(issueId!) });
|
2026-04-02 09:11:49 -05:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.feedbackVotes(issueId!) });
|
2026-02-20 10:32:32 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(issueId!) });
|
2026-03-13 21:30:48 -05:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.documents(issueId!) });
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(issueId!) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(issueId!) });
|
|
|
|
|
if (selectedCompanyId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
|
2026-03-26 08:19:16 -05:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listMineByMe(selectedCompanyId) });
|
2026-03-06 08:34:19 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) });
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-07 18:11:41 -05:00
|
|
|
const invalidateIssueCollections = () => {
|
|
|
|
|
if (selectedCompanyId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listMineByMe(selectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const applyOptimisticIssueCacheUpdate = useCallback((refs: Iterable<string>, data: Record<string, unknown>) => {
|
|
|
|
|
queryClient.setQueriesData<Issue>(
|
|
|
|
|
{ queryKey: ["issues", "detail"] },
|
|
|
|
|
(cached) => (cached && matchesIssueRef(cached, refs) ? applyOptimisticIssueFieldUpdate(cached, data) : cached),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!selectedCompanyId) return;
|
|
|
|
|
queryClient.setQueryData<Issue[] | undefined>(
|
|
|
|
|
queryKeys.issues.list(selectedCompanyId),
|
|
|
|
|
(cached) => applyOptimisticIssueFieldUpdateToCollection(cached, refs, data),
|
|
|
|
|
);
|
|
|
|
|
}, [queryClient, selectedCompanyId]);
|
|
|
|
|
|
|
|
|
|
const mergeIssueResponseIntoCaches = useCallback((refs: Iterable<string>, nextIssue: Issue) => {
|
|
|
|
|
queryClient.setQueriesData<Issue>(
|
|
|
|
|
{ queryKey: ["issues", "detail"] },
|
|
|
|
|
(cached) => (cached && matchesIssueRef(cached, refs) ? { ...cached, ...nextIssue } : cached),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!selectedCompanyId) return;
|
|
|
|
|
queryClient.setQueryData<Issue[] | undefined>(
|
|
|
|
|
queryKeys.issues.list(selectedCompanyId),
|
|
|
|
|
(cached) => cached?.map((item) => (matchesIssueRef(item, refs) ? { ...item, ...nextIssue } : item)),
|
|
|
|
|
);
|
|
|
|
|
}, [queryClient, selectedCompanyId]);
|
|
|
|
|
|
2026-03-06 08:34:19 -06:00
|
|
|
const markIssueRead = useMutation({
|
|
|
|
|
mutationFn: (id: string) => issuesApi.markRead(id),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
if (selectedCompanyId) {
|
2026-03-26 08:19:16 -05:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listMineByMe(selectedCompanyId) });
|
2026-03-06 08:34:19 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listTouchedByMe(selectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.listUnreadTouchedByMe(selectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.sidebarBadges(selectedCompanyId) });
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const updateIssue = useMutation({
|
|
|
|
|
mutationFn: (data: Record<string, unknown>) => issuesApi.update(issueId!, data),
|
2026-04-07 18:11:41 -05:00
|
|
|
onMutate: async (data) => {
|
|
|
|
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
|
|
|
|
if (selectedCompanyId) {
|
|
|
|
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const previousIssue = queryClient.getQueryData<Issue>(queryKeys.issues.detail(issueId!));
|
|
|
|
|
const issueRefs = new Set<string>([issueId!]);
|
|
|
|
|
if (previousIssue?.id) issueRefs.add(previousIssue.id);
|
|
|
|
|
if (previousIssue?.identifier) issueRefs.add(previousIssue.identifier);
|
|
|
|
|
|
|
|
|
|
const previousDetailQueries = queryClient
|
|
|
|
|
.getQueriesData<Issue>({ queryKey: ["issues", "detail"] })
|
|
|
|
|
.filter(([, cachedIssue]) => cachedIssue && matchesIssueRef(cachedIssue, issueRefs));
|
|
|
|
|
const previousList = selectedCompanyId
|
|
|
|
|
? queryClient.getQueryData<Issue[]>(queryKeys.issues.list(selectedCompanyId))
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
applyOptimisticIssueCacheUpdate(issueRefs, data);
|
|
|
|
|
|
|
|
|
|
return { previousDetailQueries, previousList, selectedCompanyId };
|
|
|
|
|
},
|
|
|
|
|
onSuccess: ({ comment: _comment, ...nextIssue }) => {
|
|
|
|
|
const issueRefs = new Set<string>([issueId!, nextIssue.id]);
|
|
|
|
|
if (nextIssue.identifier) issueRefs.add(nextIssue.identifier);
|
|
|
|
|
mergeIssueResponseIntoCaches(issueRefs, nextIssue);
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(issueId!) });
|
|
|
|
|
invalidateIssueCollections();
|
|
|
|
|
},
|
|
|
|
|
onError: (err, _variables, context) => {
|
|
|
|
|
for (const [queryKey, previousIssue] of context?.previousDetailQueries ?? []) {
|
|
|
|
|
queryClient.setQueryData(queryKey, previousIssue);
|
|
|
|
|
}
|
|
|
|
|
if (context?.selectedCompanyId) {
|
|
|
|
|
queryClient.setQueryData(queryKeys.issues.list(context.selectedCompanyId), context.previousList);
|
|
|
|
|
}
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Issue update failed",
|
|
|
|
|
body: err instanceof Error ? err.message : "Unable to save issue changes",
|
|
|
|
|
tone: "error",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onSettled: () => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
|
|
|
|
if (selectedCompanyId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(selectedCompanyId) });
|
|
|
|
|
}
|
2026-02-20 13:47:13 -06:00
|
|
|
},
|
2026-02-17 12:24:48 -06:00
|
|
|
});
|
2026-04-06 11:20:46 -05:00
|
|
|
const handleIssuePropertiesUpdate = useCallback((data: Record<string, unknown>) => {
|
|
|
|
|
updateIssue.mutate(data);
|
|
|
|
|
}, [updateIssue.mutate]);
|
2026-02-17 12:24:48 -06:00
|
|
|
|
2026-04-06 10:36:31 -05:00
|
|
|
const approvalDecision = useMutation({
|
|
|
|
|
mutationFn: async ({ approvalId, action }: { approvalId: string; action: "approve" | "reject" }) => {
|
|
|
|
|
if (action === "approve") {
|
|
|
|
|
return approvalsApi.approve(approvalId);
|
|
|
|
|
}
|
|
|
|
|
return approvalsApi.reject(approvalId);
|
|
|
|
|
},
|
|
|
|
|
onMutate: ({ approvalId, action }) => {
|
|
|
|
|
setPendingApprovalAction({ approvalId, action });
|
|
|
|
|
},
|
|
|
|
|
onSuccess: (_approval, variables) => {
|
|
|
|
|
invalidateIssue();
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.detail(variables.approvalId) });
|
|
|
|
|
if (resolvedCompanyId) {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.approvals.list(resolvedCompanyId) });
|
|
|
|
|
}
|
|
|
|
|
pushToast({
|
|
|
|
|
title: variables.action === "approve" ? "Approval approved" : "Approval rejected",
|
|
|
|
|
tone: "success",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onError: (err, variables) => {
|
|
|
|
|
pushToast({
|
|
|
|
|
title: variables.action === "approve" ? "Approval failed" : "Rejection failed",
|
|
|
|
|
body: err instanceof Error ? err.message : "Unable to update approval",
|
|
|
|
|
tone: "error",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onSettled: () => {
|
|
|
|
|
setPendingApprovalAction(null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const addComment = useMutation({
|
2026-03-28 10:34:36 -05:00
|
|
|
mutationFn: ({ body, reopen, interrupt }: { body: string; reopen?: boolean; interrupt?: boolean }) =>
|
|
|
|
|
issuesApi.addComment(issueId!, body, reopen, interrupt),
|
2026-03-28 11:25:25 -05:00
|
|
|
onMutate: async ({ body, reopen, interrupt }) => {
|
2026-03-28 09:46:34 -05:00
|
|
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
|
|
|
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
|
|
|
|
|
|
|
|
|
const previousIssue = queryClient.getQueryData<Issue>(queryKeys.issues.detail(issueId!));
|
2026-03-28 11:25:25 -05:00
|
|
|
const queuedComment = !interrupt && runningIssueRun;
|
2026-03-28 09:46:34 -05:00
|
|
|
const optimisticComment = issue
|
|
|
|
|
? createOptimisticIssueComment({
|
|
|
|
|
companyId: issue.companyId,
|
|
|
|
|
issueId: issue.id,
|
|
|
|
|
body,
|
|
|
|
|
authorUserId: currentUserId,
|
2026-03-28 11:25:25 -05:00
|
|
|
clientStatus: queuedComment ? "queued" : "pending",
|
|
|
|
|
queueTargetRunId: queuedComment ? runningIssueRun.id : null,
|
2026-03-28 09:46:34 -05:00
|
|
|
})
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if (optimisticComment) {
|
|
|
|
|
setOptimisticComments((current) => [...current, optimisticComment]);
|
|
|
|
|
}
|
|
|
|
|
if (previousIssue) {
|
|
|
|
|
queryClient.setQueryData(
|
|
|
|
|
queryKeys.issues.detail(issueId!),
|
|
|
|
|
applyOptimisticIssueCommentUpdate(previousIssue, { reopen }),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
optimisticCommentId: optimisticComment?.clientId ?? null,
|
|
|
|
|
previousIssue,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
onSuccess: (comment, _variables, context) => {
|
|
|
|
|
if (context?.optimisticCommentId) {
|
|
|
|
|
setOptimisticComments((current) =>
|
|
|
|
|
current.filter((entry) => entry.clientId !== context.optimisticCommentId),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
queryClient.setQueryData<IssueComment[]>(
|
|
|
|
|
queryKeys.issues.comments(issueId!),
|
|
|
|
|
(current) => upsertIssueComment(current, comment),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
onError: (err, _variables, context) => {
|
|
|
|
|
if (context?.optimisticCommentId) {
|
|
|
|
|
setOptimisticComments((current) =>
|
|
|
|
|
current.filter((entry) => entry.clientId !== context.optimisticCommentId),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (context?.previousIssue) {
|
|
|
|
|
queryClient.setQueryData(queryKeys.issues.detail(issueId!), context.previousIssue);
|
|
|
|
|
}
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Comment failed",
|
|
|
|
|
body: err instanceof Error ? err.message : "Unable to post comment",
|
|
|
|
|
tone: "error",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onSettled: () => {
|
UI: Identity component, LiveRunWidget, issue identifiers, and UX improvements
Add Identity component (avatar + name) used across agent/issue displays. Add
LiveRunWidget for real-time streaming of active heartbeat runs on issue detail
pages via WebSocket. Display issue identifiers (PAP-42) instead of UUID
fragments throughout Issues, Inbox, CommandPalette, and detail pages.
Enhance CommentThread with re-open checkbox, Cmd+Enter submit, sorted display,
and run linking. Improve Activity page with richer formatting and filtering.
Update Dashboard with live metrics. Add reports-to agent link in AgentProperties.
Various small fixes: StatusIcon centering, CopyText ref init, agent detail
run-issue cross-links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:10:07 -06:00
|
|
|
invalidateIssue();
|
2026-02-17 12:24:48 -06:00
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
|
|
|
|
},
|
|
|
|
|
});
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-26 16:33:48 -06:00
|
|
|
const addCommentAndReassign = useMutation({
|
|
|
|
|
mutationFn: ({
|
|
|
|
|
body,
|
|
|
|
|
reopen,
|
2026-03-28 10:34:36 -05:00
|
|
|
interrupt,
|
2026-02-26 16:33:48 -06:00
|
|
|
reassignment,
|
|
|
|
|
}: {
|
|
|
|
|
body: string;
|
|
|
|
|
reopen?: boolean;
|
2026-03-28 10:34:36 -05:00
|
|
|
interrupt?: boolean;
|
2026-02-26 16:33:48 -06:00
|
|
|
reassignment: CommentReassignment;
|
|
|
|
|
}) =>
|
|
|
|
|
issuesApi.update(issueId!, {
|
|
|
|
|
comment: body,
|
|
|
|
|
assigneeAgentId: reassignment.assigneeAgentId,
|
|
|
|
|
assigneeUserId: reassignment.assigneeUserId,
|
|
|
|
|
...(reopen ? { status: "todo" } : {}),
|
2026-03-28 10:34:36 -05:00
|
|
|
...(interrupt ? { interrupt } : {}),
|
2026-02-26 16:33:48 -06:00
|
|
|
}),
|
2026-03-28 11:25:25 -05:00
|
|
|
onMutate: async ({ body, reopen, reassignment, interrupt }) => {
|
2026-03-28 09:46:34 -05:00
|
|
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
|
|
|
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.detail(issueId!) });
|
|
|
|
|
|
|
|
|
|
const previousIssue = queryClient.getQueryData<Issue>(queryKeys.issues.detail(issueId!));
|
2026-03-28 11:25:25 -05:00
|
|
|
const queuedComment = !interrupt && runningIssueRun;
|
2026-03-28 09:46:34 -05:00
|
|
|
const optimisticComment = issue
|
|
|
|
|
? createOptimisticIssueComment({
|
|
|
|
|
companyId: issue.companyId,
|
|
|
|
|
issueId: issue.id,
|
|
|
|
|
body,
|
|
|
|
|
authorUserId: currentUserId,
|
2026-03-28 11:25:25 -05:00
|
|
|
clientStatus: queuedComment ? "queued" : "pending",
|
|
|
|
|
queueTargetRunId: queuedComment ? runningIssueRun.id : null,
|
2026-03-28 09:46:34 -05:00
|
|
|
})
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if (optimisticComment) {
|
|
|
|
|
setOptimisticComments((current) => [...current, optimisticComment]);
|
|
|
|
|
}
|
|
|
|
|
if (previousIssue) {
|
|
|
|
|
queryClient.setQueryData(
|
|
|
|
|
queryKeys.issues.detail(issueId!),
|
|
|
|
|
applyOptimisticIssueCommentUpdate(previousIssue, { reopen, reassignment }),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
optimisticCommentId: optimisticComment?.clientId ?? null,
|
|
|
|
|
previousIssue,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
onSuccess: (result, _variables, context) => {
|
|
|
|
|
if (context?.optimisticCommentId) {
|
|
|
|
|
setOptimisticComments((current) =>
|
|
|
|
|
current.filter((entry) => entry.clientId !== context.optimisticCommentId),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { comment, ...nextIssue } = result;
|
|
|
|
|
queryClient.setQueryData(queryKeys.issues.detail(issueId!), nextIssue);
|
|
|
|
|
if (comment) {
|
|
|
|
|
queryClient.setQueryData<IssueComment[]>(
|
|
|
|
|
queryKeys.issues.comments(issueId!),
|
|
|
|
|
(current) => upsertIssueComment(current, comment),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onError: (err, _variables, context) => {
|
|
|
|
|
if (context?.optimisticCommentId) {
|
|
|
|
|
setOptimisticComments((current) =>
|
|
|
|
|
current.filter((entry) => entry.clientId !== context.optimisticCommentId),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (context?.previousIssue) {
|
|
|
|
|
queryClient.setQueryData(queryKeys.issues.detail(issueId!), context.previousIssue);
|
|
|
|
|
}
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Comment failed",
|
|
|
|
|
body: err instanceof Error ? err.message : "Unable to post comment",
|
|
|
|
|
tone: "error",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onSettled: () => {
|
2026-02-26 16:33:48 -06:00
|
|
|
invalidateIssue();
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.comments(issueId!) });
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-28 11:25:25 -05:00
|
|
|
const interruptQueuedComment = useMutation({
|
|
|
|
|
mutationFn: (runId: string) => heartbeatsApi.cancel(runId),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
invalidateIssue();
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Interrupt requested",
|
|
|
|
|
body: "The active run is stopping so queued comments can continue next.",
|
|
|
|
|
tone: "success",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Interrupt failed",
|
|
|
|
|
body: err instanceof Error ? err.message : "Unable to interrupt the active run",
|
|
|
|
|
tone: "error",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-02 09:11:49 -05:00
|
|
|
const feedbackVoteMutation = useMutation({
|
|
|
|
|
mutationFn: (variables: {
|
|
|
|
|
targetType: "issue_comment" | "issue_document_revision";
|
|
|
|
|
targetId: string;
|
|
|
|
|
vote: "up" | "down";
|
|
|
|
|
reason?: string;
|
|
|
|
|
allowSharing?: boolean;
|
|
|
|
|
sharingPreferenceAtSubmit: "allowed" | "not_allowed" | "prompt";
|
|
|
|
|
}) =>
|
|
|
|
|
issuesApi.upsertFeedbackVote(issueId!, {
|
|
|
|
|
targetType: variables.targetType,
|
|
|
|
|
targetId: variables.targetId,
|
|
|
|
|
vote: variables.vote,
|
|
|
|
|
...(variables.reason ? { reason: variables.reason } : {}),
|
|
|
|
|
...(variables.allowSharing ? { allowSharing: true } : {}),
|
|
|
|
|
}),
|
|
|
|
|
onMutate: async (variables) => {
|
|
|
|
|
await queryClient.cancelQueries({ queryKey: queryKeys.issues.feedbackVotes(issueId!) });
|
|
|
|
|
const previousVotes = queryClient.getQueryData<FeedbackVote[]>(
|
|
|
|
|
queryKeys.issues.feedbackVotes(issueId!),
|
|
|
|
|
);
|
|
|
|
|
queryClient.setQueryData<FeedbackVote[]>(
|
|
|
|
|
queryKeys.issues.feedbackVotes(issueId!),
|
|
|
|
|
mergeOptimisticFeedbackVote(
|
|
|
|
|
previousVotes,
|
|
|
|
|
{
|
|
|
|
|
issueId: issueId!,
|
|
|
|
|
targetType: variables.targetType,
|
|
|
|
|
targetId: variables.targetId,
|
|
|
|
|
vote: variables.vote,
|
|
|
|
|
reason: variables.reason,
|
|
|
|
|
},
|
|
|
|
|
currentUserId,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
return { previousVotes };
|
|
|
|
|
},
|
|
|
|
|
onSuccess: (_savedVote, variables) => {
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.feedbackVotes(issueId!) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.instance.generalSettings });
|
|
|
|
|
pushToast({
|
|
|
|
|
title:
|
|
|
|
|
variables.sharingPreferenceAtSubmit === "prompt"
|
|
|
|
|
? variables.allowSharing
|
|
|
|
|
? "Feedback saved. Future votes will share"
|
|
|
|
|
: "Feedback saved. Future votes will stay local"
|
|
|
|
|
: variables.allowSharing
|
|
|
|
|
? "Feedback saved and sharing enabled"
|
|
|
|
|
: "Feedback saved",
|
|
|
|
|
tone: "success",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onError: (err, _variables, context) => {
|
|
|
|
|
if (context?.previousVotes) {
|
|
|
|
|
queryClient.setQueryData(queryKeys.issues.feedbackVotes(issueId!), context.previousVotes);
|
|
|
|
|
}
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Failed to save feedback",
|
|
|
|
|
body: err instanceof Error ? err.message : "Unknown error",
|
|
|
|
|
tone: "error",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-20 10:32:32 -06:00
|
|
|
const uploadAttachment = useMutation({
|
|
|
|
|
mutationFn: async (file: File) => {
|
|
|
|
|
if (!selectedCompanyId) throw new Error("No company selected");
|
|
|
|
|
return issuesApi.uploadAttachment(selectedCompanyId, issueId!, file);
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setAttachmentError(null);
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(issueId!) });
|
|
|
|
|
invalidateIssue();
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
setAttachmentError(err instanceof Error ? err.message : "Upload failed");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-13 21:30:48 -05:00
|
|
|
const importMarkdownDocument = useMutation({
|
|
|
|
|
mutationFn: async (file: File) => {
|
|
|
|
|
const baseName = fileBaseName(file.name);
|
|
|
|
|
const key = slugifyDocumentKey(baseName);
|
|
|
|
|
const existing = (issue?.documentSummaries ?? []).find((doc) => doc.key === key) ?? null;
|
|
|
|
|
const body = await file.text();
|
|
|
|
|
const inferredTitle = titleizeFilename(baseName);
|
|
|
|
|
const nextTitle = existing?.title ?? inferredTitle ?? null;
|
|
|
|
|
return issuesApi.upsertDocument(issueId!, key, {
|
|
|
|
|
title: key === "plan" ? null : nextTitle,
|
|
|
|
|
format: "markdown",
|
|
|
|
|
body,
|
|
|
|
|
baseRevisionId: existing?.latestRevisionId ?? null,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setAttachmentError(null);
|
|
|
|
|
invalidateIssue();
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
setAttachmentError(err instanceof Error ? err.message : "Document import failed");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-20 10:32:32 -06:00
|
|
|
const deleteAttachment = useMutation({
|
|
|
|
|
mutationFn: (attachmentId: string) => issuesApi.deleteAttachment(attachmentId),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
setAttachmentError(null);
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.issues.attachments(issueId!) });
|
|
|
|
|
invalidateIssue();
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
setAttachmentError(err instanceof Error ? err.message : "Delete failed");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-02 11:51:40 -05:00
|
|
|
const archiveFromInbox = useMutation({
|
|
|
|
|
mutationFn: (id: string) => issuesApi.archiveFromInbox(id),
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
invalidateIssue();
|
|
|
|
|
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox", { replace: true });
|
|
|
|
|
pushToast({ title: "Issue archived from inbox", tone: "success" });
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
pushToast({
|
|
|
|
|
title: "Archive failed",
|
|
|
|
|
body: err instanceof Error ? err.message : "Unable to archive this issue from the inbox",
|
|
|
|
|
tone: "error",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## 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
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-08 17:54:03 -07:00
|
|
|
const handleInterruptQueued = useCallback(
|
|
|
|
|
async (runId: string) => {
|
|
|
|
|
await interruptQueuedComment.mutateAsync(runId);
|
|
|
|
|
},
|
|
|
|
|
[interruptQueuedComment.mutateAsync],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleCommentImageUpload = useCallback(
|
|
|
|
|
async (file: File) => {
|
|
|
|
|
const attachment = await uploadAttachment.mutateAsync(file);
|
|
|
|
|
return attachment.contentPath;
|
|
|
|
|
},
|
|
|
|
|
[uploadAttachment.mutateAsync],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleCommentAttachImage = useCallback(
|
|
|
|
|
async (file: File) => {
|
|
|
|
|
await uploadAttachment.mutateAsync(file);
|
|
|
|
|
},
|
|
|
|
|
[uploadAttachment.mutateAsync],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleCommentAdd = useCallback(
|
|
|
|
|
async (body: string, reopen?: boolean, reassignment?: CommentReassignment) => {
|
|
|
|
|
if (reassignment) {
|
|
|
|
|
await addCommentAndReassign.mutateAsync({ body, reopen, reassignment });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
await addComment.mutateAsync({ body, reopen });
|
|
|
|
|
},
|
|
|
|
|
[addComment.mutateAsync, addCommentAndReassign.mutateAsync],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleCommentVote = useCallback(
|
|
|
|
|
async (commentId: string, vote: FeedbackVoteValue, options?: { reason?: string; allowSharing?: boolean }) => {
|
|
|
|
|
await feedbackVoteMutation.mutateAsync({
|
|
|
|
|
targetType: "issue_comment",
|
|
|
|
|
targetId: commentId,
|
|
|
|
|
vote,
|
|
|
|
|
reason: options?.reason,
|
|
|
|
|
allowSharing: options?.allowSharing,
|
|
|
|
|
sharingPreferenceAtSubmit: feedbackDataSharingPreference,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[feedbackVoteMutation.mutateAsync, feedbackDataSharingPreference],
|
|
|
|
|
);
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
useEffect(() => {
|
2026-03-05 12:20:43 -06:00
|
|
|
const titleLabel = issue?.title ?? issueId ?? "Issue";
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
setBreadcrumbs([
|
2026-03-10 20:59:55 -05:00
|
|
|
sourceBreadcrumb,
|
2026-03-05 12:20:43 -06:00
|
|
|
{ label: hasLiveRuns ? `🔵 ${titleLabel}` : titleLabel },
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
]);
|
2026-03-10 20:59:55 -05:00
|
|
|
}, [setBreadcrumbs, sourceBreadcrumb, issue, issueId, hasLiveRuns]);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-02-20 16:04:05 -06:00
|
|
|
// Redirect to identifier-based URL if navigated via UUID
|
|
|
|
|
useEffect(() => {
|
2026-04-06 06:32:25 -05:00
|
|
|
const nextState = resolvedIssueDetailState ?? location.state;
|
2026-02-20 16:04:05 -06:00
|
|
|
if (issue?.identifier && issueId !== issue.identifier) {
|
2026-04-06 06:32:25 -05:00
|
|
|
rememberIssueDetailLocationState(issue.identifier, nextState, location.search);
|
|
|
|
|
navigate(createIssueDetailPath(issue.identifier), {
|
|
|
|
|
replace: true,
|
|
|
|
|
state: nextState,
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (issueId && hasLegacyIssueDetailQuery(location.search)) {
|
|
|
|
|
rememberIssueDetailLocationState(issueId, nextState, location.search);
|
|
|
|
|
navigate(createIssueDetailPath(issueId), {
|
2026-03-27 08:02:26 -05:00
|
|
|
replace: true,
|
2026-04-06 06:32:25 -05:00
|
|
|
state: nextState,
|
2026-03-27 08:02:26 -05:00
|
|
|
});
|
2026-02-20 16:04:05 -06:00
|
|
|
}
|
2026-04-06 06:32:25 -05:00
|
|
|
}, [issue, issueId, navigate, location.state, location.search, resolvedIssueDetailState]);
|
2026-02-20 16:04:05 -06:00
|
|
|
|
2026-03-06 08:34:19 -06:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!issue?.id) return;
|
|
|
|
|
if (lastMarkedReadIssueIdRef.current === issue.id) return;
|
|
|
|
|
lastMarkedReadIssueIdRef.current = issue.id;
|
|
|
|
|
markIssueRead.mutate(issue.id);
|
|
|
|
|
}, [issue?.id]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
useEffect(() => {
|
2026-04-06 11:20:46 -05:00
|
|
|
if (!issue) {
|
|
|
|
|
closePanel();
|
|
|
|
|
return;
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
}
|
2026-04-06 11:20:46 -05:00
|
|
|
openPanel(
|
|
|
|
|
<IssueProperties
|
|
|
|
|
issue={issue}
|
|
|
|
|
childIssues={childIssues}
|
|
|
|
|
onAddSubIssue={openNewSubIssue}
|
|
|
|
|
onUpdate={handleIssuePropertiesUpdate}
|
|
|
|
|
/>
|
|
|
|
|
);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
return () => closePanel();
|
2026-04-06 11:20:46 -05:00
|
|
|
}, [closePanel, handleIssuePropertiesUpdate, issuePanelKey, openNewSubIssue, openPanel]);
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
|
2026-04-08 08:27:34 -05:00
|
|
|
const goToInboxShortcutArmedRef = useRef(false);
|
|
|
|
|
const goToInboxShortcutTimeoutRef = useRef<number | null>(null);
|
2026-04-02 11:51:40 -05:00
|
|
|
const canQuickArchiveFromInbox =
|
|
|
|
|
keyboardShortcutsEnabled &&
|
2026-04-08 08:56:07 -05:00
|
|
|
!issue?.hiddenAt;
|
2026-04-02 11:51:40 -05:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-04-08 08:56:07 -05:00
|
|
|
if (!issue?.id || !canQuickArchiveFromInbox) return;
|
2026-04-02 11:51:40 -05:00
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
const action = resolveInboxQuickArchiveKeyAction({
|
2026-04-08 08:56:07 -05:00
|
|
|
armed: canQuickArchiveFromInbox,
|
2026-04-02 11:51:40 -05:00
|
|
|
defaultPrevented: event.defaultPrevented,
|
|
|
|
|
key: event.key,
|
|
|
|
|
metaKey: event.metaKey,
|
|
|
|
|
ctrlKey: event.ctrlKey,
|
|
|
|
|
altKey: event.altKey,
|
|
|
|
|
target: event.target,
|
|
|
|
|
hasOpenDialog: hasBlockingShortcutDialog(document),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (action !== "archive") return;
|
|
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (!archiveFromInbox.isPending) {
|
|
|
|
|
archiveFromInbox.mutate(issue.id);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener("keydown", handleKeyDown, true);
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("keydown", handleKeyDown, true);
|
|
|
|
|
};
|
|
|
|
|
}, [archiveFromInbox, canQuickArchiveFromInbox, issue?.id]);
|
|
|
|
|
|
2026-04-08 08:27:34 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (!keyboardShortcutsEnabled) {
|
|
|
|
|
goToInboxShortcutArmedRef.current = false;
|
|
|
|
|
if (goToInboxShortcutTimeoutRef.current !== null) {
|
|
|
|
|
window.clearTimeout(goToInboxShortcutTimeoutRef.current);
|
|
|
|
|
goToInboxShortcutTimeoutRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clearArmTimeout = () => {
|
|
|
|
|
if (goToInboxShortcutTimeoutRef.current !== null) {
|
|
|
|
|
window.clearTimeout(goToInboxShortcutTimeoutRef.current);
|
|
|
|
|
goToInboxShortcutTimeoutRef.current = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const disarm = () => {
|
|
|
|
|
goToInboxShortcutArmedRef.current = false;
|
|
|
|
|
clearArmTimeout();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const arm = () => {
|
|
|
|
|
goToInboxShortcutArmedRef.current = true;
|
|
|
|
|
clearArmTimeout();
|
|
|
|
|
goToInboxShortcutTimeoutRef.current = window.setTimeout(() => {
|
|
|
|
|
goToInboxShortcutArmedRef.current = false;
|
|
|
|
|
goToInboxShortcutTimeoutRef.current = null;
|
|
|
|
|
}, 1200);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePointerDown = () => {
|
|
|
|
|
disarm();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleFocusIn = (event: FocusEvent) => {
|
|
|
|
|
if (event.target instanceof HTMLElement && event.target !== document.body) {
|
|
|
|
|
disarm();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
2026-04-08 09:24:32 -05:00
|
|
|
const action = resolveIssueDetailGoKeyAction({
|
2026-04-08 08:27:34 -05:00
|
|
|
armed: goToInboxShortcutArmedRef.current,
|
|
|
|
|
defaultPrevented: event.defaultPrevented,
|
|
|
|
|
key: event.key,
|
|
|
|
|
metaKey: event.metaKey,
|
|
|
|
|
ctrlKey: event.ctrlKey,
|
|
|
|
|
altKey: event.altKey,
|
|
|
|
|
target: event.target,
|
|
|
|
|
hasOpenDialog: hasBlockingShortcutDialog(document),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (action === "ignore") return;
|
|
|
|
|
if (action === "arm") {
|
|
|
|
|
arm();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disarm();
|
2026-04-08 09:24:32 -05:00
|
|
|
if (action === "navigate_inbox") {
|
|
|
|
|
event.preventDefault();
|
2026-04-08 11:12:01 -05:00
|
|
|
event.stopPropagation();
|
2026-04-08 09:24:32 -05:00
|
|
|
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (action === "focus_comment") {
|
|
|
|
|
event.preventDefault();
|
2026-04-08 11:12:01 -05:00
|
|
|
event.stopPropagation();
|
2026-04-08 09:24:32 -05:00
|
|
|
setDetailTab("chat");
|
|
|
|
|
setPendingCommentComposerFocusKey((current) => current + 1);
|
|
|
|
|
}
|
2026-04-08 08:27:34 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.addEventListener("pointerdown", handlePointerDown, true);
|
|
|
|
|
document.addEventListener("focusin", handleFocusIn, true);
|
|
|
|
|
document.addEventListener("keydown", handleKeyDown, true);
|
|
|
|
|
return () => {
|
|
|
|
|
disarm();
|
|
|
|
|
document.removeEventListener("pointerdown", handlePointerDown, true);
|
|
|
|
|
document.removeEventListener("focusin", handleFocusIn, true);
|
|
|
|
|
document.removeEventListener("keydown", handleKeyDown, true);
|
|
|
|
|
};
|
|
|
|
|
}, [keyboardShortcutsEnabled, navigate, sourceBreadcrumb.href]);
|
|
|
|
|
|
2026-04-08 09:24:32 -05:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (pendingCommentComposerFocusKey === 0) return;
|
|
|
|
|
if (detailTab !== "chat") return;
|
|
|
|
|
commentComposerRef.current?.focus();
|
|
|
|
|
}, [detailTab, pendingCommentComposerFocusKey]);
|
|
|
|
|
|
2026-04-08 07:20:24 -05:00
|
|
|
const isImageAttachment = (attachment: IssueAttachment) => attachment.contentType.startsWith("image/");
|
|
|
|
|
const attachmentList = attachments ?? [];
|
|
|
|
|
const imageAttachments = attachmentList.filter(isImageAttachment);
|
|
|
|
|
const nonImageAttachments = attachmentList.filter((a) => !isImageAttachment(a));
|
|
|
|
|
|
|
|
|
|
const handleChatImageClick = useCallback(
|
|
|
|
|
(src: string) => {
|
|
|
|
|
// Try exact contentPath match first
|
|
|
|
|
let idx = imageAttachments.findIndex((a) => a.contentPath === src);
|
|
|
|
|
if (idx < 0) {
|
|
|
|
|
// Try matching by asset ID extracted from /api/assets/{assetId}/content URLs
|
|
|
|
|
const assetMatch = src.match(/\/api\/assets\/([^/]+)\/content/);
|
|
|
|
|
if (assetMatch) {
|
|
|
|
|
idx = imageAttachments.findIndex((a) => a.assetId === assetMatch[1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (idx >= 0) {
|
|
|
|
|
setGalleryIndex(idx);
|
|
|
|
|
setGalleryOpen(true);
|
|
|
|
|
} else {
|
|
|
|
|
// Image not in attachment list — open in new tab
|
|
|
|
|
window.open(src, "_blank");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[imageAttachments],
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-17 11:12:56 -05:00
|
|
|
const copyIssueToClipboard = async () => {
|
|
|
|
|
if (!issue) return;
|
2026-03-17 11:18:55 -05:00
|
|
|
const decodeEntities = (text: string) => {
|
|
|
|
|
const el = document.createElement("textarea");
|
|
|
|
|
el.innerHTML = text;
|
|
|
|
|
return el.value;
|
|
|
|
|
};
|
|
|
|
|
const title = decodeEntities(issue.title);
|
|
|
|
|
const body = decodeEntities(issue.description ?? "");
|
|
|
|
|
const md = `# ${issue.identifier}: ${title}\n\n${body}`.trimEnd();
|
2026-03-17 11:12:56 -05:00
|
|
|
await navigator.clipboard.writeText(md);
|
|
|
|
|
setCopied(true);
|
|
|
|
|
pushToast({ title: "Copied to clipboard", tone: "success" });
|
|
|
|
|
setTimeout(() => setCopied(false), 2000);
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
if (isLoading) return <p className="text-sm text-muted-foreground">Loading...</p>;
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
if (error) return <p className="text-sm text-destructive">{error.message}</p>;
|
|
|
|
|
if (!issue) return null;
|
|
|
|
|
|
2026-02-17 20:16:57 -06:00
|
|
|
// Ancestors are returned oldest-first from the server (root at end, immediate parent at start)
|
|
|
|
|
const ancestors = issue.ancestors ?? [];
|
2026-02-20 10:32:32 -06:00
|
|
|
const handleFilePicked = async (evt: ChangeEvent<HTMLInputElement>) => {
|
2026-03-13 21:30:48 -05:00
|
|
|
const files = evt.target.files;
|
|
|
|
|
if (!files || files.length === 0) return;
|
|
|
|
|
for (const file of Array.from(files)) {
|
|
|
|
|
if (isMarkdownFile(file)) {
|
|
|
|
|
await importMarkdownDocument.mutateAsync(file);
|
|
|
|
|
} else {
|
|
|
|
|
await uploadAttachment.mutateAsync(file);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-20 10:32:32 -06:00
|
|
|
if (fileInputRef.current) {
|
|
|
|
|
fileInputRef.current.value = "";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-13 21:30:48 -05:00
|
|
|
const handleAttachmentDrop = async (evt: DragEvent<HTMLDivElement>) => {
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
setAttachmentDragActive(false);
|
|
|
|
|
const files = evt.dataTransfer.files;
|
|
|
|
|
if (!files || files.length === 0) return;
|
|
|
|
|
for (const file of Array.from(files)) {
|
|
|
|
|
if (isMarkdownFile(file)) {
|
|
|
|
|
await importMarkdownDocument.mutateAsync(file);
|
|
|
|
|
} else {
|
|
|
|
|
await uploadAttachment.mutateAsync(file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-14 06:02:46 -05:00
|
|
|
const hasAttachments = attachmentList.length > 0;
|
|
|
|
|
const attachmentUploadButton = (
|
2026-03-14 06:13:07 -05:00
|
|
|
<>
|
2026-03-14 06:02:46 -05:00
|
|
|
<input
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
type="file"
|
|
|
|
|
className="hidden"
|
|
|
|
|
onChange={handleFilePicked}
|
|
|
|
|
multiple
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => fileInputRef.current?.click()}
|
|
|
|
|
disabled={uploadAttachment.isPending || importMarkdownDocument.isPending}
|
|
|
|
|
className={cn(
|
2026-03-14 06:13:07 -05:00
|
|
|
"shadow-none",
|
|
|
|
|
attachmentDragActive && "border-primary bg-primary/5",
|
2026-03-14 06:02:46 -05:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Paperclip className="h-3.5 w-3.5 mr-1.5" />
|
2026-03-22 11:02:04 -05:00
|
|
|
{uploadAttachment.isPending || importMarkdownDocument.isPending ? "Uploading..." : (
|
|
|
|
|
<>
|
|
|
|
|
<span className="hidden sm:inline">Upload attachment</span>
|
|
|
|
|
<span className="sm:hidden">Upload</span>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
2026-03-14 06:02:46 -05:00
|
|
|
</Button>
|
2026-03-14 06:13:07 -05:00
|
|
|
</>
|
2026-03-14 06:02:46 -05:00
|
|
|
);
|
2026-02-20 10:32:32 -06:00
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
return (
|
|
|
|
|
<div className="max-w-2xl space-y-6">
|
2026-02-17 20:16:57 -06:00
|
|
|
{/* Parent chain breadcrumb */}
|
|
|
|
|
{ancestors.length > 0 && (
|
|
|
|
|
<nav className="flex items-center gap-1 text-xs text-muted-foreground flex-wrap">
|
|
|
|
|
{[...ancestors].reverse().map((ancestor, i) => (
|
|
|
|
|
<span key={ancestor.id} className="flex items-center gap-1">
|
|
|
|
|
{i > 0 && <ChevronRight className="h-3 w-3 shrink-0" />}
|
|
|
|
|
<Link
|
2026-04-06 06:32:25 -05:00
|
|
|
to={createIssueDetailPath(ancestor.identifier ?? ancestor.id)}
|
|
|
|
|
state={resolvedIssueDetailState ?? location.state}
|
|
|
|
|
onClickCapture={() =>
|
|
|
|
|
rememberIssueDetailLocationState(
|
|
|
|
|
ancestor.identifier ?? ancestor.id,
|
|
|
|
|
resolvedIssueDetailState ?? location.state,
|
|
|
|
|
location.search,
|
|
|
|
|
)}
|
2026-02-17 20:16:57 -06:00
|
|
|
className="hover:text-foreground transition-colors truncate max-w-[200px]"
|
|
|
|
|
title={ancestor.title}
|
|
|
|
|
>
|
|
|
|
|
{ancestor.title}
|
|
|
|
|
</Link>
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
<ChevronRight className="h-3 w-3 shrink-0" />
|
|
|
|
|
<span className="text-foreground/60 truncate max-w-[200px]">{issue.title}</span>
|
|
|
|
|
</nav>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-20 07:11:06 -06:00
|
|
|
{issue.hiddenAt && (
|
|
|
|
|
<div className="flex items-center gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
|
|
|
|
<EyeOff className="h-4 w-4 shrink-0" />
|
|
|
|
|
This issue is hidden
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
<div className="space-y-3">
|
2026-02-25 17:40:13 -06:00
|
|
|
<div className="flex items-center gap-2 min-w-0 flex-wrap">
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
<StatusIcon
|
|
|
|
|
status={issue.status}
|
2026-02-17 12:24:48 -06:00
|
|
|
onChange={(status) => updateIssue.mutate({ status })}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
/>
|
|
|
|
|
<PriorityIcon
|
|
|
|
|
priority={issue.priority}
|
2026-02-17 12:24:48 -06:00
|
|
|
onChange={(priority) => updateIssue.mutate({ priority })}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
/>
|
2026-02-25 17:40:13 -06:00
|
|
|
<span className="text-sm font-mono text-muted-foreground shrink-0">{issue.identifier ?? issue.id.slice(0, 8)}</span>
|
|
|
|
|
|
|
|
|
|
{hasLiveRuns && (
|
2026-02-26 16:33:48 -06:00
|
|
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/30 px-2 py-0.5 text-[10px] font-medium text-cyan-600 dark:text-cyan-400 shrink-0">
|
2026-02-25 17:40:13 -06:00
|
|
|
<span className="relative flex h-1.5 w-1.5">
|
ui: apply interface polish from design article review
- Add global font smoothing (antialiased) to body
- Add tabular-nums to all numeric displays: MetricCard values, Costs page,
AgentDetail token/cost grids and tables, IssueDetail cost summary,
Companies page budget display
- Replace markdown image hard border with subtle inset box-shadow overlay
- Replace all animate-ping status dots with calmer animate-pulse across
AgentDetail, IssueDetail, Agents, sidebar, kanban, issues list, and
active agents panel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 08:20:24 -05:00
|
|
|
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
|
2026-02-25 17:40:13 -06:00
|
|
|
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-cyan-400" />
|
|
|
|
|
</span>
|
|
|
|
|
Live
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-02-19 15:44:05 -06:00
|
|
|
|
2026-03-20 09:13:30 -05:00
|
|
|
{issue.originKind === "routine_execution" && issue.originId && (
|
|
|
|
|
<Link
|
|
|
|
|
to={`/routines/${issue.originId}`}
|
|
|
|
|
className="inline-flex items-center gap-1 rounded-full bg-violet-500/10 border border-violet-500/30 px-2 py-0.5 text-[10px] font-medium text-violet-600 dark:text-violet-400 shrink-0 hover:bg-violet-500/20 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<Repeat className="h-3 w-3" />
|
|
|
|
|
Routine
|
|
|
|
|
</Link>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-23 14:41:21 -06:00
|
|
|
{issue.projectId ? (
|
|
|
|
|
<Link
|
|
|
|
|
to={`/projects/${issue.projectId}`}
|
2026-02-25 17:40:13 -06:00
|
|
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors rounded px-1 -mx-1 py-0.5 min-w-0"
|
2026-02-23 14:41:21 -06:00
|
|
|
>
|
|
|
|
|
<Hexagon className="h-3 w-3 shrink-0" />
|
2026-02-25 17:40:13 -06:00
|
|
|
<span className="truncate">{(projects ?? []).find((p) => p.id === issue.projectId)?.name ?? issue.projectId.slice(0, 8)}</span>
|
2026-02-23 14:41:21 -06:00
|
|
|
</Link>
|
|
|
|
|
) : (
|
|
|
|
|
<span className="inline-flex items-center gap-1 text-xs text-muted-foreground opacity-50 px-1 -mx-1 py-0.5">
|
|
|
|
|
<Hexagon className="h-3 w-3 shrink-0" />
|
|
|
|
|
No project
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-02-20 09:01:28 -06:00
|
|
|
|
2026-02-23 19:49:43 -06:00
|
|
|
{(issue.labels ?? []).length > 0 && (
|
|
|
|
|
<div className="hidden sm:flex items-center gap-1">
|
|
|
|
|
{(issue.labels ?? []).slice(0, 4).map((label) => (
|
|
|
|
|
<span
|
|
|
|
|
key={label.id}
|
|
|
|
|
className="inline-flex items-center rounded-full border px-2 py-0.5 text-[10px] font-medium"
|
|
|
|
|
style={{
|
|
|
|
|
borderColor: label.color,
|
2026-03-23 07:48:50 -05:00
|
|
|
color: pickTextColorForPillBg(label.color, 0.12),
|
2026-02-23 19:49:43 -06:00
|
|
|
backgroundColor: `${label.color}1f`,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{label.name}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
{(issue.labels ?? []).length > 4 && (
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">+{(issue.labels ?? []).length - 4}</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-17 11:12:56 -05:00
|
|
|
<div className="ml-auto flex items-center gap-0.5 md:hidden shrink-0">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
onClick={copyIssueToClipboard}
|
|
|
|
|
title="Copy issue as markdown"
|
|
|
|
|
>
|
|
|
|
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
onClick={() => setMobilePropsOpen(true)}
|
|
|
|
|
title="Properties"
|
|
|
|
|
>
|
|
|
|
|
<SlidersHorizontal className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-02-23 19:49:43 -06:00
|
|
|
|
2026-03-03 15:06:42 -06:00
|
|
|
<div className="hidden md:flex items-center md:ml-auto shrink-0">
|
2026-03-17 11:12:56 -05:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
onClick={copyIssueToClipboard}
|
|
|
|
|
title="Copy issue as markdown"
|
|
|
|
|
>
|
|
|
|
|
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
|
|
|
|
</Button>
|
2026-03-03 14:56:32 -06:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
2026-03-03 15:06:42 -06:00
|
|
|
className={cn(
|
|
|
|
|
"shrink-0 transition-opacity duration-200",
|
|
|
|
|
panelVisible ? "opacity-0 pointer-events-none w-0 overflow-hidden" : "opacity-100",
|
|
|
|
|
)}
|
2026-03-03 14:56:32 -06:00
|
|
|
onClick={() => setPanelVisible(true)}
|
|
|
|
|
title="Show properties"
|
|
|
|
|
>
|
|
|
|
|
<SlidersHorizontal className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
|
2026-03-03 15:06:42 -06:00
|
|
|
<Popover open={moreOpen} onOpenChange={setMoreOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon-xs" className="shrink-0">
|
|
|
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
2026-02-19 15:44:05 -06:00
|
|
|
<PopoverContent className="w-44 p-1" align="end">
|
|
|
|
|
<button
|
|
|
|
|
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-destructive"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
updateIssue.mutate(
|
|
|
|
|
{ hiddenAt: new Date().toISOString() },
|
2026-02-20 07:11:06 -06:00
|
|
|
{ onSuccess: () => navigate("/issues/all") },
|
2026-02-19 15:44:05 -06:00
|
|
|
);
|
|
|
|
|
setMoreOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<EyeOff className="h-3 w-3" />
|
|
|
|
|
Hide this Issue
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverContent>
|
2026-03-03 15:06:42 -06:00
|
|
|
</Popover>
|
|
|
|
|
</div>
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<InlineEditor
|
|
|
|
|
value={issue.title}
|
2026-03-13 21:30:48 -05:00
|
|
|
onSave={(title) => updateIssue.mutateAsync({ title })}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
as="h2"
|
|
|
|
|
className="text-xl font-bold"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<InlineEditor
|
|
|
|
|
value={issue.description ?? ""}
|
2026-03-13 21:30:48 -05:00
|
|
|
onSave={(description) => updateIssue.mutateAsync({ description })}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
as="p"
|
2026-03-10 20:55:41 -05:00
|
|
|
className="text-[15px] leading-7 text-foreground"
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
placeholder="Add a description..."
|
|
|
|
|
multiline
|
2026-03-02 13:31:58 -06:00
|
|
|
mentions={mentionOptions}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
imageUploadHandler={async (file) => {
|
|
|
|
|
const attachment = await uploadAttachment.mutateAsync(file);
|
|
|
|
|
return attachment.contentPath;
|
|
|
|
|
}}
|
2026-04-05 06:39:20 -05:00
|
|
|
onDropFile={async (file) => {
|
|
|
|
|
await uploadAttachment.mutateAsync(file);
|
|
|
|
|
}}
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-13 23:03:51 -05:00
|
|
|
<PluginSlotOutlet
|
|
|
|
|
slotTypes={["toolbarButton", "contextMenuItem"]}
|
|
|
|
|
entityType="issue"
|
|
|
|
|
context={{
|
|
|
|
|
companyId: issue.companyId,
|
|
|
|
|
projectId: issue.projectId ?? null,
|
|
|
|
|
entityId: issue.id,
|
|
|
|
|
entityType: "issue",
|
|
|
|
|
}}
|
|
|
|
|
className="flex flex-wrap gap-2"
|
|
|
|
|
itemClassName="inline-flex"
|
|
|
|
|
missingBehavior="placeholder"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<PluginLauncherOutlet
|
|
|
|
|
placementZones={["toolbarButton"]}
|
|
|
|
|
entityType="issue"
|
|
|
|
|
context={{
|
|
|
|
|
companyId: issue.companyId,
|
|
|
|
|
projectId: issue.projectId ?? null,
|
|
|
|
|
entityId: issue.id,
|
|
|
|
|
entityType: "issue",
|
|
|
|
|
}}
|
|
|
|
|
className="flex flex-wrap gap-2"
|
|
|
|
|
itemClassName="inline-flex"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<PluginSlotOutlet
|
|
|
|
|
slotTypes={["taskDetailView"]}
|
|
|
|
|
entityType="issue"
|
|
|
|
|
context={{
|
|
|
|
|
companyId: issue.companyId,
|
|
|
|
|
projectId: issue.projectId ?? null,
|
|
|
|
|
entityId: issue.id,
|
|
|
|
|
entityType: "issue",
|
|
|
|
|
}}
|
|
|
|
|
className="space-y-3"
|
|
|
|
|
itemClassName="rounded-lg border border-border p-3"
|
|
|
|
|
missingBehavior="placeholder"
|
|
|
|
|
/>
|
|
|
|
|
|
2026-04-06 11:59:37 -05:00
|
|
|
{childIssues.length > 0 && (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
<h3 className="text-sm font-medium text-muted-foreground">Sub-issues</h3>
|
|
|
|
|
<Button variant="outline" size="sm" onClick={openNewSubIssue} className="shadow-none">
|
|
|
|
|
<ListTree className="h-3.5 w-3.5 mr-1.5" />
|
|
|
|
|
<span className="hidden sm:inline">Add sub-issue</span>
|
|
|
|
|
<span className="sm:hidden">Sub-issue</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="border border-border rounded-lg divide-y divide-border">
|
|
|
|
|
{childIssues.map((child) => (
|
|
|
|
|
<Link
|
|
|
|
|
key={child.id}
|
|
|
|
|
to={createIssueDetailPath(child.identifier ?? child.id)}
|
|
|
|
|
state={resolvedIssueDetailState ?? location.state}
|
|
|
|
|
onClickCapture={() =>
|
|
|
|
|
rememberIssueDetailLocationState(
|
|
|
|
|
child.identifier ?? child.id,
|
|
|
|
|
resolvedIssueDetailState ?? location.state,
|
|
|
|
|
location.search,
|
|
|
|
|
)}
|
|
|
|
|
className="flex items-center justify-between px-3 py-2 text-sm hover:bg-accent/20 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
|
|
|
<StatusIcon status={child.status} />
|
|
|
|
|
<PriorityIcon priority={child.priority} />
|
|
|
|
|
<span className="font-mono text-muted-foreground shrink-0">
|
|
|
|
|
{child.identifier ?? child.id.slice(0, 8)}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="truncate">{child.title}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{child.assigneeAgentId && (() => {
|
|
|
|
|
const name = agentMap.get(child.assigneeAgentId)?.name;
|
|
|
|
|
return name
|
|
|
|
|
? <Identity name={name} size="sm" />
|
|
|
|
|
: <span className="text-muted-foreground font-mono">{child.assigneeAgentId.slice(0, 8)}</span>;
|
|
|
|
|
})()}
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-03-13 21:30:48 -05:00
|
|
|
<IssueDocumentsSection
|
|
|
|
|
issue={issue}
|
|
|
|
|
canDeleteDocuments={Boolean(session?.user?.id)}
|
2026-04-02 09:11:49 -05:00
|
|
|
feedbackVotes={feedbackVotes}
|
|
|
|
|
feedbackDataSharingPreference={feedbackDataSharingPreference}
|
|
|
|
|
feedbackTermsUrl={FEEDBACK_TERMS_URL}
|
2026-03-13 21:30:48 -05:00
|
|
|
mentions={mentionOptions}
|
|
|
|
|
imageUploadHandler={async (file) => {
|
|
|
|
|
const attachment = await uploadAttachment.mutateAsync(file);
|
|
|
|
|
return attachment.contentPath;
|
|
|
|
|
}}
|
2026-04-02 09:11:49 -05:00
|
|
|
onVote={async (revisionId, vote, options) => {
|
|
|
|
|
await feedbackVoteMutation.mutateAsync({
|
|
|
|
|
targetType: "issue_document_revision",
|
|
|
|
|
targetId: revisionId,
|
|
|
|
|
vote,
|
|
|
|
|
reason: options?.reason,
|
|
|
|
|
allowSharing: options?.allowSharing,
|
|
|
|
|
sharingPreferenceAtSubmit: feedbackDataSharingPreference,
|
|
|
|
|
});
|
|
|
|
|
}}
|
2026-04-06 11:59:37 -05:00
|
|
|
extraActions={
|
|
|
|
|
<>
|
|
|
|
|
{!hasAttachments && attachmentUploadButton}
|
|
|
|
|
{childIssues.length === 0 && (
|
|
|
|
|
<Button variant="outline" size="sm" onClick={openNewSubIssue} className="shadow-none">
|
|
|
|
|
<ListTree className="h-3.5 w-3.5 mr-1.5" />
|
|
|
|
|
<span className="hidden sm:inline">Add sub-issue</span>
|
|
|
|
|
<span className="sm:hidden">Sub-issue</span>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
}
|
2026-03-13 21:30:48 -05:00
|
|
|
/>
|
|
|
|
|
|
2026-03-14 06:02:46 -05:00
|
|
|
{hasAttachments ? (
|
|
|
|
|
<div
|
2026-03-13 21:30:48 -05:00
|
|
|
className={cn(
|
|
|
|
|
"space-y-3 rounded-lg transition-colors",
|
|
|
|
|
)}
|
|
|
|
|
onDragEnter={(evt) => {
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
setAttachmentDragActive(true);
|
|
|
|
|
}}
|
|
|
|
|
onDragOver={(evt) => {
|
|
|
|
|
evt.preventDefault();
|
|
|
|
|
setAttachmentDragActive(true);
|
|
|
|
|
}}
|
|
|
|
|
onDragLeave={(evt) => {
|
|
|
|
|
if (evt.currentTarget.contains(evt.relatedTarget as Node | null)) return;
|
|
|
|
|
setAttachmentDragActive(false);
|
|
|
|
|
}}
|
|
|
|
|
onDrop={(evt) => void handleAttachmentDrop(evt)}
|
|
|
|
|
>
|
2026-02-20 10:32:32 -06:00
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
<h3 className="text-sm font-medium text-muted-foreground">Attachments</h3>
|
2026-03-14 06:02:46 -05:00
|
|
|
{attachmentUploadButton}
|
2026-02-20 10:32:32 -06:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{attachmentError && (
|
|
|
|
|
<p className="text-xs text-destructive">{attachmentError}</p>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-04-06 11:56:20 -05:00
|
|
|
{imageAttachments.length > 0 && (
|
|
|
|
|
<div className="grid grid-cols-4 gap-2">
|
|
|
|
|
{imageAttachments.map((attachment) => (
|
|
|
|
|
<div
|
|
|
|
|
key={attachment.id}
|
|
|
|
|
className="group relative aspect-square rounded-lg overflow-hidden border border-border bg-accent/10 cursor-pointer"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const idx = imageAttachments.findIndex((a) => a.id === attachment.id);
|
|
|
|
|
setGalleryIndex(idx >= 0 ? idx : 0);
|
|
|
|
|
setGalleryOpen(true);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<img
|
|
|
|
|
src={attachment.contentPath}
|
|
|
|
|
alt={attachment.originalFilename ?? "attachment"}
|
|
|
|
|
className="h-full w-full object-cover"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-colors" />
|
|
|
|
|
{confirmDeleteId === attachment.id ? (
|
|
|
|
|
<div
|
|
|
|
|
className="absolute inset-0 flex flex-col items-center justify-center gap-1.5 bg-black/60"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
<p className="text-xs text-white font-medium">Delete?</p>
|
|
|
|
|
<div className="flex gap-1.5">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="rounded bg-destructive px-2 py-0.5 text-xs text-white hover:bg-destructive/80"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
deleteAttachment.mutate(attachment.id);
|
|
|
|
|
setConfirmDeleteId(null);
|
|
|
|
|
}}
|
|
|
|
|
disabled={deleteAttachment.isPending}
|
|
|
|
|
>
|
|
|
|
|
Yes
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="rounded bg-muted px-2 py-0.5 text-xs hover:bg-muted/80"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setConfirmDeleteId(null);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
No
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="absolute top-1.5 right-1.5 rounded-md bg-black/50 p-1 text-white opacity-0 group-hover:opacity-100 transition-opacity hover:bg-destructive"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setConfirmDeleteId(attachment.id);
|
|
|
|
|
}}
|
|
|
|
|
title="Delete attachment"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-02-20 10:32:32 -06:00
|
|
|
</div>
|
2026-04-06 11:56:20 -05:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{nonImageAttachments.length > 0 && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{nonImageAttachments.map((attachment) => (
|
|
|
|
|
<div key={attachment.id} className="border border-border rounded-md p-2">
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
<a
|
|
|
|
|
href={attachment.contentPath}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
className="text-xs hover:underline truncate"
|
|
|
|
|
title={attachment.originalFilename ?? attachment.id}
|
|
|
|
|
>
|
|
|
|
|
{attachment.originalFilename ?? attachment.id}
|
|
|
|
|
</a>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
className="text-muted-foreground hover:text-destructive"
|
|
|
|
|
onClick={() => deleteAttachment.mutate(attachment.id)}
|
|
|
|
|
disabled={deleteAttachment.isPending}
|
|
|
|
|
title="Delete attachment"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-[11px] text-muted-foreground">
|
|
|
|
|
{attachment.contentType} · {(attachment.byteSize / 1024).toFixed(1)} KB
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-14 06:02:46 -05:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
2026-02-20 10:32:32 -06:00
|
|
|
|
2026-04-02 11:51:40 -05:00
|
|
|
<ImageGalleryModal
|
|
|
|
|
images={imageAttachments}
|
|
|
|
|
initialIndex={galleryIndex}
|
|
|
|
|
open={galleryOpen}
|
|
|
|
|
onOpenChange={setGalleryOpen}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-03-26 07:56:36 -05:00
|
|
|
<IssueWorkspaceCard
|
|
|
|
|
issue={issue}
|
|
|
|
|
project={orderedProjects.find((p) => p.id === issue.projectId) ?? null}
|
|
|
|
|
onUpdate={(data) => updateIssue.mutate(data)}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-02-20 10:32:32 -06:00
|
|
|
<Separator />
|
|
|
|
|
|
2026-02-25 08:39:31 -06:00
|
|
|
<Tabs value={detailTab} onValueChange={setDetailTab} className="space-y-3">
|
|
|
|
|
<TabsList variant="line" className="w-full justify-start gap-1">
|
2026-04-06 07:53:37 -05:00
|
|
|
<TabsTrigger value="chat" className="gap-1.5">
|
2026-02-25 08:39:31 -06:00
|
|
|
<MessageSquare className="h-3.5 w-3.5" />
|
2026-04-06 07:53:37 -05:00
|
|
|
Chat
|
2026-02-25 08:39:31 -06:00
|
|
|
</TabsTrigger>
|
|
|
|
|
<TabsTrigger value="activity" className="gap-1.5">
|
|
|
|
|
<ActivityIcon className="h-3.5 w-3.5" />
|
|
|
|
|
Activity
|
|
|
|
|
</TabsTrigger>
|
2026-03-13 23:03:51 -05:00
|
|
|
{issuePluginTabItems.map((item) => (
|
|
|
|
|
<TabsTrigger key={item.value} value={item.value}>
|
|
|
|
|
{item.label}
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
))}
|
2026-02-25 08:39:31 -06:00
|
|
|
</TabsList>
|
|
|
|
|
|
2026-04-06 07:53:37 -05:00
|
|
|
<TabsContent value="chat">
|
|
|
|
|
<IssueChatThread
|
2026-04-08 09:24:32 -05:00
|
|
|
composerRef={commentComposerRef}
|
2026-04-06 07:53:37 -05:00
|
|
|
comments={commentsWithRunMeta}
|
2026-04-02 09:11:49 -05:00
|
|
|
feedbackVotes={feedbackVotes}
|
|
|
|
|
feedbackDataSharingPreference={feedbackDataSharingPreference}
|
|
|
|
|
feedbackTermsUrl={FEEDBACK_TERMS_URL}
|
2026-03-03 12:19:26 -06:00
|
|
|
linkedRuns={timelineRuns}
|
2026-04-02 11:51:40 -05:00
|
|
|
timelineEvents={timelineEvents}
|
2026-04-06 07:53:37 -05:00
|
|
|
liveRuns={liveRuns}
|
|
|
|
|
activeRun={activeRun}
|
2026-03-13 23:03:51 -05:00
|
|
|
companyId={issue.companyId}
|
|
|
|
|
projectId={issue.projectId}
|
2026-02-25 08:39:31 -06:00
|
|
|
issueStatus={issue.status}
|
|
|
|
|
agentMap={agentMap}
|
2026-04-02 11:51:40 -05:00
|
|
|
currentUserId={currentUserId}
|
2026-02-25 08:39:31 -06:00
|
|
|
draftKey={`paperclip:issue-comment-draft:${issue.id}`}
|
2026-03-02 16:56:05 -06:00
|
|
|
enableReassign
|
2026-02-26 16:33:48 -06:00
|
|
|
reassignOptions={commentReassignOptions}
|
2026-03-20 06:05:05 -05:00
|
|
|
currentAssigneeValue={actualAssigneeValue}
|
|
|
|
|
suggestedAssigneeValue={suggestedAssigneeValue}
|
2026-03-02 13:31:58 -06:00
|
|
|
mentions={mentionOptions}
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## 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
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-08 17:54:03 -07:00
|
|
|
onInterruptQueued={handleInterruptQueued}
|
2026-04-06 11:00:12 -05:00
|
|
|
interruptingQueuedRunId={interruptQueuedComment.isPending ? interruptQueuedComment.variables ?? null : null}
|
Fix typing lag in long comment threads (PAPA-63) (#3163)
## Thinking Path
> - Paperclip orchestrates AI agents for zero-human companies
> - The issue detail page displays comment threads with rich timeline
rendering
> - Long threads (100+ items) cause severe typing lag in the comment
composer because every keystroke re-renders the entire timeline
> - CDP tracing confirmed 110ms avg key→paint latency and 60 long tasks
blocking the main thread for 3.7s total
> - This pull request memoizes the timeline, stabilizes callback props,
debounces editor observers, and reduces idle polling frequency
> - The benefit is responsive typing (21ms avg, 5.3× faster) even on
threads with 100+ timeline items
## What Changed
- **CommentThread.tsx**: Memoize `TimelineList` with `useMemo` so typing
state changes don't re-render 143 timeline items; extract
`handleFeedbackVote` to `useCallback`; added missing deps
(`pendingApprovalAction`, `onApproveApproval`, `onRejectApproval`) to
useMemo array
- **IssueDetail.tsx**: Extract inline callbacks (`handleCommentAdd`,
`handleCommentVote`, `handleCommentImageUpload`,
`handleCommentAttachImage`, `handleInterruptQueued`) to `useCallback`
with `.mutateAsync` deps (not full mutation objects) for stable
references; add conditional polling intervals (3s active / 30s idle) for
`liveRuns`, `activeRun`, `linkedRuns`, and timeline queries
- **MarkdownEditor.tsx**: Debounce `MutationObserver` and
`selectionchange` handlers via `requestAnimationFrame` coalescing
- **LiveRunWidget.tsx**: Accept optional `liveRunsData` and
`activeRunData` props to reuse parent-fetched data instead of duplicate
polling
## Verification
- Navigated to [IP address]:3105/PAPA/issues/PAPA-32 (thread with 100+
items)
- Typed in comment composer — lag eliminated, characters appear
instantly
- CDP trace test script (`test-typing-lag.mjs`) confirmed: avg 21ms
key→paint (was 110ms), 5 long tasks (was 60), 0.5s blocking (was 3.7s)
- Ran `pnpm test:run` locally — all tests pass
## Risks
- Low risk. All changes are additive memoization and callback
stabilization — no behavioral changes. Polling intervals are only
reduced for idle state; active runs still poll at 3–5s.
## Model Used
- Claude Opus 4.6 (`claude-opus-4-6`) via Claude Code CLI, with tool use
and extended context
## 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
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-08 17:54:03 -07:00
|
|
|
composerDisabledReason={commentComposerDisabledReason}
|
|
|
|
|
onVote={handleCommentVote}
|
|
|
|
|
onAdd={handleCommentAdd}
|
|
|
|
|
imageUploadHandler={handleCommentImageUpload}
|
|
|
|
|
onAttachImage={handleCommentAttachImage}
|
2026-04-06 07:53:37 -05:00
|
|
|
onCancelRun={runningIssueRun
|
|
|
|
|
? async () => {
|
|
|
|
|
await interruptQueuedComment.mutateAsync(runningIssueRun.id);
|
|
|
|
|
}
|
|
|
|
|
: undefined}
|
2026-04-08 07:09:01 -05:00
|
|
|
onImageClick={handleChatImageClick}
|
2026-02-25 08:39:31 -06:00
|
|
|
/>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
<TabsContent value="activity">
|
2026-04-06 11:20:46 -05:00
|
|
|
{linkedApprovals && linkedApprovals.length > 0 && (
|
|
|
|
|
<div className="mb-3 space-y-3">
|
|
|
|
|
{linkedApprovals.map((approval) => (
|
|
|
|
|
<ApprovalCard
|
|
|
|
|
key={approval.id}
|
|
|
|
|
approval={approval}
|
|
|
|
|
requesterAgent={approval.requestedByAgentId ? agentMap.get(approval.requestedByAgentId) ?? null : null}
|
|
|
|
|
onApprove={() => approvalDecision.mutate({ approvalId: approval.id, action: "approve" })}
|
|
|
|
|
onReject={() => approvalDecision.mutate({ approvalId: approval.id, action: "reject" })}
|
|
|
|
|
detailLink={`/approvals/${approval.id}`}
|
|
|
|
|
isPending={pendingApprovalAction?.approvalId === approval.id}
|
|
|
|
|
pendingAction={
|
|
|
|
|
pendingApprovalAction?.approvalId === approval.id
|
|
|
|
|
? pendingApprovalAction.action
|
|
|
|
|
: null
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-03-19 08:53:28 -05:00
|
|
|
{linkedRuns && linkedRuns.length > 0 && (
|
|
|
|
|
<div className="mb-3 px-3 py-2 rounded-lg border border-border">
|
|
|
|
|
<div className="text-sm font-medium text-muted-foreground mb-1">Cost Summary</div>
|
|
|
|
|
{!issueCostSummary.hasCost && !issueCostSummary.hasTokens ? (
|
|
|
|
|
<div className="text-xs text-muted-foreground">No cost data yet.</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex flex-wrap gap-3 text-xs text-muted-foreground tabular-nums">
|
|
|
|
|
{issueCostSummary.hasCost && (
|
|
|
|
|
<span className="font-medium text-foreground">
|
|
|
|
|
${issueCostSummary.cost.toFixed(4)}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{issueCostSummary.hasTokens && (
|
|
|
|
|
<span>
|
|
|
|
|
Tokens {formatTokens(issueCostSummary.totalTokens)}
|
|
|
|
|
{issueCostSummary.cached > 0
|
|
|
|
|
? ` (in ${formatTokens(issueCostSummary.input)}, out ${formatTokens(issueCostSummary.output)}, cached ${formatTokens(issueCostSummary.cached)})`
|
|
|
|
|
: ` (in ${formatTokens(issueCostSummary.input)}, out ${formatTokens(issueCostSummary.output)})`}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-25 08:39:31 -06:00
|
|
|
{!activity || activity.length === 0 ? (
|
|
|
|
|
<p className="text-xs text-muted-foreground">No activity yet.</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
{activity.slice(0, 20).map((evt) => (
|
|
|
|
|
<div key={evt.id} className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
|
|
|
<ActorIdentity evt={evt} agentMap={agentMap} />
|
|
|
|
|
<span>{formatAction(evt.action, evt.details)}</span>
|
|
|
|
|
<span className="ml-auto shrink-0">{relativeTime(evt.createdAt)}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</TabsContent>
|
2026-03-13 23:03:51 -05:00
|
|
|
|
|
|
|
|
{activePluginTab && (
|
|
|
|
|
<TabsContent value={activePluginTab.value}>
|
|
|
|
|
<PluginSlotMount
|
|
|
|
|
slot={activePluginTab.slot}
|
|
|
|
|
context={{
|
|
|
|
|
companyId: issue.companyId,
|
|
|
|
|
projectId: issue.projectId ?? null,
|
|
|
|
|
entityId: issue.id,
|
|
|
|
|
entityType: "issue",
|
|
|
|
|
}}
|
|
|
|
|
missingBehavior="placeholder"
|
|
|
|
|
/>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
)}
|
2026-02-25 08:39:31 -06:00
|
|
|
</Tabs>
|
2026-02-20 14:48:30 -06:00
|
|
|
|
2026-02-23 19:49:43 -06:00
|
|
|
|
|
|
|
|
{/* Mobile properties drawer */}
|
|
|
|
|
<Sheet open={mobilePropsOpen} onOpenChange={setMobilePropsOpen}>
|
2026-02-25 21:36:06 -06:00
|
|
|
<SheetContent side="bottom" className="max-h-[85dvh] pb-[env(safe-area-inset-bottom)]">
|
2026-02-23 19:49:43 -06:00
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle className="text-sm">Properties</SheetTitle>
|
|
|
|
|
</SheetHeader>
|
|
|
|
|
<ScrollArea className="flex-1 overflow-y-auto">
|
|
|
|
|
<div className="px-4 pb-4">
|
2026-04-06 10:58:59 -05:00
|
|
|
<IssueProperties
|
|
|
|
|
issue={issue}
|
|
|
|
|
childIssues={childIssues}
|
|
|
|
|
onAddSubIssue={openNewSubIssue}
|
|
|
|
|
onUpdate={(data) => updateIssue.mutate(data)}
|
|
|
|
|
inline
|
|
|
|
|
/>
|
2026-02-23 19:49:43 -06:00
|
|
|
</div>
|
|
|
|
|
</ScrollArea>
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
2026-03-07 20:07:39 -06:00
|
|
|
<ScrollToBottom />
|
Add detail pages, property panels, and restyle list pages
New pages: AgentDetail, GoalDetail, IssueDetail, ProjectDetail, Inbox,
MyIssues. New feature components: AgentProperties, GoalProperties,
IssueProperties, ProjectProperties, GoalTree, NewIssueDialog. Add
heartbeats API client. Restyle all list pages (Agents, Issues, Goals,
Projects, Dashboard, Costs, Activity, Org) with EntityRow, FilterBar,
and improved layouts. Add routing for detail views.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 09:57:06 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|