Files
PaperClipAI/ui/src/lib/issue-properties-panel-key.ts
T
DottaandPaperclip 37c097a474 [codex] Update watchdog properties pane cache (#8786)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The issue detail surface includes an Issue Properties pane that
shows live metadata for a selected issue.
> - Watchdog configuration can be changed from that pane, but cached
issue detail data also feeds the pane key and rerender behavior.
> - If the cache does not reflect watchdog changes immediately, the
properties pane can keep stale watchdog state until a later refetch
completes.
> - This pull request updates the watchdog save/delete paths to write
the returned watchdog state into cached issue detail data.
> - It also includes watchdog fields in the issue properties panel key
so watchdog configuration changes invalidate the memoized panel state.
> - The benefit is a properties pane that updates consistently after
operators set or remove a watchdog.

## Linked Issues or Issue Description

Refs #8789

This PR fixes stale watchdog state in the Issue Properties pane after
watchdog save/delete mutations.

## What Changed

- Write the saved watchdog summary into cached issue detail data after a
successful watchdog upsert using the exact
`queryKeys.issues.detail(issue.id)` key.
- Clear cached issue detail watchdog data after a successful watchdog
delete using the exact `queryKeys.issues.detail(issue.id)` key.
- Include stable watchdog fields in `buildIssuePropertiesPanelKey` so
watchdog configuration changes produce a new panel key.
- Add focused tests for watchdog cache updates and panel-key
invalidation, including save and delete cache assertions.

## Verification

- `pnpm --filter @paperclipai/ui exec vitest run
src/components/IssueProperties.test.tsx
src/lib/issue-properties-panel-key.test.ts`
- Result: 2 files passed, 40 tests passed.

## Risks

Low risk. The change is scoped to UI cache handling for issue detail
watchdog state and panel-key generation. The main risk is an unexpected
query-cache shape, covered by focused tests around the existing issue
detail query key.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex coding agent running GPT-5-class model with repository tool
use and local command execution. The exact backend model identifier and
context window are not exposed in this runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-06-30 07:54:25 -07:00

111 lines
3.5 KiB
TypeScript

import type { Issue } from "@paperclipai/shared";
type IssuePropertiesPanelKeyIssue = Pick<
Issue,
| "id"
| "status"
| "priority"
| "assigneeAgentId"
| "assigneeUserId"
| "projectId"
| "projectWorkspaceId"
| "parentId"
| "createdByUserId"
| "hiddenAt"
| "labelIds"
| "executionPolicy"
| "executionState"
| "executionWorkspaceId"
| "executionWorkspacePreference"
| "executionWorkspaceSettings"
| "currentExecutionWorkspace"
| "blocks"
| "blockedBy"
| "ancestors"
| "watchdog"
>;
type IssuePropertiesPanelKeyChild = Pick<Issue, "id" | "updatedAt" | "identifier" | "title">;
export function buildIssuePropertiesPanelKey(
issue: IssuePropertiesPanelKeyIssue | null | undefined,
childIssues: readonly IssuePropertiesPanelKeyChild[],
) {
if (!issue) return "";
return JSON.stringify({
id: issue.id,
status: issue.status,
priority: issue.priority,
assigneeAgentId: issue.assigneeAgentId,
assigneeUserId: issue.assigneeUserId,
projectId: issue.projectId,
projectWorkspaceId: issue.projectWorkspaceId,
parentId: issue.parentId,
createdByUserId: issue.createdByUserId,
hiddenAt: issue.hiddenAt,
labelIds: issue.labelIds ?? [],
executionWorkspaceId: issue.executionWorkspaceId,
executionWorkspacePreference: issue.executionWorkspacePreference,
executionWorkspaceSettings: issue.executionWorkspaceSettings ?? null,
currentExecutionWorkspace: issue.currentExecutionWorkspace
? {
id: issue.currentExecutionWorkspace.id,
mode: issue.currentExecutionWorkspace.mode,
status: issue.currentExecutionWorkspace.status,
projectWorkspaceId: issue.currentExecutionWorkspace.projectWorkspaceId,
branchName: issue.currentExecutionWorkspace.branchName,
cwd: issue.currentExecutionWorkspace.cwd,
runtimeServices: (issue.currentExecutionWorkspace.runtimeServices ?? []).map((service) => ({
id: service.id,
status: service.status,
url: service.url,
})),
}
: null,
executionPolicy: issue.executionPolicy ?? null,
executionState: issue.executionState
? {
status: issue.executionState.status,
currentStageType: issue.executionState.currentStageType,
currentParticipant: issue.executionState.currentParticipant,
returnAssignee: issue.executionState.returnAssignee,
}
: null,
blocks: (issue.blocks ?? []).map((relation) => ({
id: relation.id,
identifier: relation.identifier ?? null,
title: relation.title,
status: relation.status,
})),
blockedBy: (issue.blockedBy ?? []).map((relation) => ({
id: relation.id,
identifier: relation.identifier ?? null,
title: relation.title,
status: relation.status,
})),
watchdog: issue.watchdog
? {
id: issue.watchdog.id,
watchdogAgentId: issue.watchdog.watchdogAgentId,
instructions: issue.watchdog.instructions ?? null,
status: issue.watchdog.status,
watchdogIssueId: issue.watchdog.watchdogIssueId ?? null,
}
: null,
parentSummary: issue.ancestors?.[0]
? {
id: issue.ancestors[0].id,
identifier: issue.ancestors[0].identifier ?? null,
title: issue.ancestors[0].title,
}
: null,
childIssues: childIssues.map((child) => ({
id: child.id,
updatedAt: String(child.updatedAt),
identifier: child.identifier ?? null,
title: child.title,
})),
});
}