Skip to content

fix(home): run quick actions inline with optimistic task insert#2601

Open
k11kirky wants to merge 2 commits into
mainfrom
posthog-code/home-quick-action-inline
Open

fix(home): run quick actions inline with optimistic task insert#2601
k11kirky wants to merge 2 commits into
mainfrom
posthog-code/home-quick-action-inline

Conversation

@k11kirky

Copy link
Copy Markdown
Contributor

Problem

On the Home tab, clicking a quick action ("Fix CI", "Address comments", "Create PR", "Review with agent") navigated away — to the pending route and then the task-detail view — and gave no in-flight feedback on the button. The desired behaviour is to kick off the cloud task in place, show it immediately in the workstream's task list, and disable the button while it starts.

Changes

  • Stay on Home. useRunWorkstreamAction no longer navigates to the pending or task-detail routes. It starts the cloud task, optimistically splices the new run into the workstream's task list (insertOptimisticTask), disables the trigger with a spinner while in flight (isPending threaded through useWorkstreamPresentation to the row / card / detail-panel buttons and overflow items), and fires a server snapshot refresh so the next poll reconciles. The offline / signed-out fallback still routes to the new-task screen.
  • Quick-action tags. The action label flows end-to-end (TaskCreationInput.homeQuickActionLabel → saga → api-client home_quick_action body) so the server records which quick action started a run. The snapshot's HomeWorkstreamTask gains quickAction, rendered as a per-task chip in the detail panel and a glanceable "✨ Fix CI, …" indicator on the workstream row. The optimistic row is tagged immediately too.

Server half (grouping + home_quick_action persistence + quickAction in the snapshot) is in PostHog/posthog.

How did you test this?

I'm an agent (Claude Code). Automated checks run in the sandbox:

  • vitest run src/features/home — 36 passing, including new optimisticTask coverage (splice into the right workstream, no cross-workstream bleed, no duplicates, quick-action tagging).
  • tsc --noEmit clean across @posthog/ui, @posthog/core, @posthog/shared, @posthog/api-client.
  • biome check clean on all touched files.

I did not run the app manually.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Quick actions on a workstream row/card/detail panel no longer navigate away.
The action starts the cloud task in place, optimistically splices the new run
into the workstream's task list (tagged with the action label), disables the
trigger with a spinner while in flight, and nudges a server snapshot refresh
so the next poll reconciles.

Threads the quick-action label end-to-end (TaskCreationInput -> saga ->
api-client body -> home_quick_action) and renders it: a per-task chip in the
detail panel and a glanceable indicator on the row showing which quick
actions have run against the workstream.

Generated-By: PostHog Code
Task-Id: 8dc7acb2-b80c-402c-be8c-5a82ea04a68f
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

React Doctor could not complete this scan.

No React dependency found in /tmp/react-doctor-baseline-Mj48gV/package.json. Add "react" to dependencies (or peerDependencies) and re-run.

Reviewed by React Doctor for commit f51118a.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
packages/ui/src/features/home/hooks/useRunWorkstreamAction.ts:75-107
**Per-instance `inFlightRef` doesn't guard across simultaneous row + detail panel mounts**

`HomeWorkstreamRow` (via `useWorkstreamPresentation`) and `HomeWorkstreamDetailPanel` both mount their own `useRunWorkstreamAction()` instance, each with an independent `inFlightRef` and `isPending` state. When the detail panel is open alongside the row, clicking the quick action in the row sets only the row's `inFlightRef.current = true` and `isPending = true`; the detail panel's guard is still `false`. A user can then immediately click the panel's action button and start a second task for the same workstream.

Before this PR, navigation away unmounted both surfaces, so races were impossible. The new stay-on-Home design keeps both mounted, making this a real double-submit path. Sharing state through a context or lifting `inFlightRef` to a workstream-keyed store (e.g. a `Map<workstreamId, boolean>` ref at a common ancestor) would close the gap.

### Issue 2 of 3
packages/ui/src/features/home/hooks/useRunWorkstreamAction.ts:197-199
`refreshHome` is the entire `useMutation` result object, which gets a new reference whenever mutation state transitions (idle → pending → settled). Including it in the `useCallback` dep array means `run` is recreated on every transition, which defeats memoisation and propagates new references through every `useWorkstreamPresentation` caller. `mutateAsync` is a stable reference in react-query v5 and is the correct dep here.

```suggestion
      queryClient,
      refreshHome.mutateAsync,
      getUserIntegrationIdForRepo,
```

### Issue 3 of 3
packages/ui/src/features/home/utils/optimisticTask.test.ts:40-71
**Prefer parameterised tests for `workstreamTaskFromTask`**

The four `workstreamTaskFromTask` cases are each a distinct input-to-output mapping and are a natural fit for `it.each`. Per the project's convention, parameterised tests are preferred; this block could be expressed as a single `it.each` table covering id, title, status, and quickAction across the four variants.

Reviews (1): Last reviewed commit: "fix(home): run quick actions inline with..." | Re-trigger Greptile

Comment on lines +75 to +107
@@ -87,14 +104,7 @@ export function useRunWorkstreamAction() {

if (inFlightRef.current) return;
inFlightRef.current = true;

const pendingTaskKey =
globalThis.crypto?.randomUUID?.() ?? `pending-${Date.now()}`;
pendingTaskPromptStoreApi.set(pendingTaskKey, {
promptText,
attachments: [],
});
navigateToTaskPending(pendingTaskKey);
setIsPending(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Per-instance inFlightRef doesn't guard across simultaneous row + detail panel mounts

HomeWorkstreamRow (via useWorkstreamPresentation) and HomeWorkstreamDetailPanel both mount their own useRunWorkstreamAction() instance, each with an independent inFlightRef and isPending state. When the detail panel is open alongside the row, clicking the quick action in the row sets only the row's inFlightRef.current = true and isPending = true; the detail panel's guard is still false. A user can then immediately click the panel's action button and start a second task for the same workstream.

Before this PR, navigation away unmounted both surfaces, so races were impossible. The new stay-on-Home design keeps both mounted, making this a real double-submit path. Sharing state through a context or lifting inFlightRef to a workstream-keyed store (e.g. a Map<workstreamId, boolean> ref at a common ancestor) would close the gap.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/home/hooks/useRunWorkstreamAction.ts
Line: 75-107

Comment:
**Per-instance `inFlightRef` doesn't guard across simultaneous row + detail panel mounts**

`HomeWorkstreamRow` (via `useWorkstreamPresentation`) and `HomeWorkstreamDetailPanel` both mount their own `useRunWorkstreamAction()` instance, each with an independent `inFlightRef` and `isPending` state. When the detail panel is open alongside the row, clicking the quick action in the row sets only the row's `inFlightRef.current = true` and `isPending = true`; the detail panel's guard is still `false`. A user can then immediately click the panel's action button and start a second task for the same workstream.

Before this PR, navigation away unmounted both surfaces, so races were impossible. The new stay-on-Home design keeps both mounted, making this a real double-submit path. Sharing state through a context or lifting `inFlightRef` to a workstream-keyed store (e.g. a `Map<workstreamId, boolean>` ref at a common ancestor) would close the gap.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +197 to 199
queryClient,
refreshHome,
getUserIntegrationIdForRepo,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 refreshHome is the entire useMutation result object, which gets a new reference whenever mutation state transitions (idle → pending → settled). Including it in the useCallback dep array means run is recreated on every transition, which defeats memoisation and propagates new references through every useWorkstreamPresentation caller. mutateAsync is a stable reference in react-query v5 and is the correct dep here.

Suggested change
queryClient,
refreshHome,
getUserIntegrationIdForRepo,
queryClient,
refreshHome.mutateAsync,
getUserIntegrationIdForRepo,
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/home/hooks/useRunWorkstreamAction.ts
Line: 197-199

Comment:
`refreshHome` is the entire `useMutation` result object, which gets a new reference whenever mutation state transitions (idle → pending → settled). Including it in the `useCallback` dep array means `run` is recreated on every transition, which defeats memoisation and propagates new references through every `useWorkstreamPresentation` caller. `mutateAsync` is a stable reference in react-query v5 and is the correct dep here.

```suggestion
      queryClient,
      refreshHome.mutateAsync,
      getUserIntegrationIdForRepo,
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +40 to +71
describe("workstreamTaskFromTask", () => {
it("maps a created task to a provisional queued workstream task", () => {
const wsTask = workstreamTaskFromTask(makeTask());
expect(wsTask).toEqual({
id: "task_1",
title: "Fix CI",
status: "queued",
isGenerating: false,
needsPermission: false,
quickAction: null,
});
});

it("records the quick action label when provided", () => {
expect(workstreamTaskFromTask(makeTask(), "Fix CI").quickAction).toBe(
"Fix CI",
);
});

it("prefers the latest run status when present", () => {
const wsTask = workstreamTaskFromTask(
makeTask({ latest_run: { status: "in_progress" } as Task["latest_run"] }),
);
expect(wsTask.status).toBe("in_progress");
});

it("falls back to a placeholder title", () => {
expect(workstreamTaskFromTask(makeTask({ title: "" })).title).toBe(
"New task",
);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Prefer parameterised tests for workstreamTaskFromTask

The four workstreamTaskFromTask cases are each a distinct input-to-output mapping and are a natural fit for it.each. Per the project's convention, parameterised tests are preferred; this block could be expressed as a single it.each table covering id, title, status, and quickAction across the four variants.

Context Used: Do not attempt to comment on incorrect alphabetica... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/home/utils/optimisticTask.test.ts
Line: 40-71

Comment:
**Prefer parameterised tests for `workstreamTaskFromTask`**

The four `workstreamTaskFromTask` cases are each a distinct input-to-output mapping and are a natural fit for `it.each`. Per the project's convention, parameterised tests are preferred; this block could be expressed as a single `it.each` table covering id, title, status, and quickAction across the four variants.

**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Addresses Greptile review on PR #2601.

- Move the quick-action in-flight guard from a per-hook ref/state into a shared,
  workstream-keyed Zustand store. The list/board row and the open detail panel
  mount independent useRunWorkstreamAction hooks, so the previous per-instance
  guard let both start a task for the same workstream. The store keys by
  workstream id, so the same workstream can't double-submit while distinct
  workstreams still run concurrently.
- Depend on refreshHome.mutateAsync (stable in react-query v5) instead of the
  whole mutation object, so run isn't recreated on every mutation transition.
- Parameterise the workstreamTaskFromTask tests with it.each per repo convention.

Generated-By: PostHog Code
Task-Id: 8dc7acb2-b80c-402c-be8c-5a82ea04a68f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant