Skip to content

feat(code): add Board view to inbox alongside the existing list#2349

Open
fercgomes wants to merge 4 commits into
mainfrom
posthog-code/inbox-view-as-board
Open

feat(code): add Board view to inbox alongside the existing list#2349
fercgomes wants to merge 4 commits into
mainfrom
posthog-code/inbox-view-as-board

Conversation

@fercgomes
Copy link
Copy Markdown
Contributor

@fercgomes fercgomes commented May 25, 2026

Summary

  • Adds a List/Board toggle to the inbox toolbar so users can switch between the current flat list and a kanban-style board grouped by status.
  • New ReportBoardPane renders horizontal lanes for ready, pending_input, in_progress, candidate, potential, and failed, reusing the existing ReportCardContent, click/cmd/shift selection, multi-select checkboxes, and motion fade-in animations from the list rows.
  • Choice persists via inboxSignalsFilterStore.viewMode ("list" | "board") so it survives reloads. Infinite-scroll is preserved by anchoring the load-more sentinel to the tallest lane.
Screenshot 2026-05-25 at 14 19 23

Why

The inbox has been getting noisy with many items at once. Switching to a status-grouped board makes it much easier to scan what's actually actionable (Ready, Needs input) vs. what is still being processed.

Notes

  • Lane width is fixed at 280px and the board scrolls horizontally inside the existing left pane. Drag the pane's resize handle to see more lanes at once.
  • Lane headers are intentionally non-sticky to avoid layering conflicts with the existing sticky toolbar above.
  • Empty lanes show a small "No reports" hint so the board still feels populated when most reports are in one status.

Test plan

  • Open the Inbox tab — default view is List (existing behavior).
  • Click the kanban icon in the toolbar — Board view appears, lanes are grouped by status with counts.
  • Click a card in any lane — detail pane opens; cmd-click and shift-click still extend selection across lanes.
  • Toggle back to List — same selection persists, no flicker.
  • Reload the app — selected view mode is restored.
  • In Board view: scroll the longest lane to the bottom — load-more triggers further pages.
  • In Board view: trigger an empty filter combination — see "No reports match current filters" empty state.
  • In Board view: simulate an error (e.g. offline) — Retry button appears.

fercgomes added 4 commits May 25, 2026 06:51
Adds a "View as" toggle to the inbox toolbar (List / Board). Board view
groups inbox reports into a horizontal kanban by status (Ready, Needs
input, Researching, Queued, Gathering, Failed), each lane sharing the
existing `ReportCardContent` so badges, summaries, and selection match
the list view. The choice is persisted via `inboxSignalsFilterStore.viewMode`.

Infinite-scroll loading is preserved by parking the load-more sentinel
at the bottom of the lane with the most reports.

Generated-By: PostHog Code
Task-Id: b4476476-5cb0-4ab1-8062-555eba7bfbda
Adds a "View as" toggle to the inbox toolbar with a list (default) and board option. The board groups reports into kanban columns by status (Ready, Needs input, Researching, Failed, Queued, Gathering), respecting active status filters. Selecting a card opens the standard detail pane in a right-side panel. The view mode is persisted in the filter store.

Generated-By: PostHog Code
Task-Id: bb7bd45f-d16b-4cca-a8bd-00856f6c5004
- Each board column now has its own vertical overflow, so it scrolls independently instead of the whole board overflowing the page.
- Replaces the nested Radix ScrollArea (which doesn't propagate height through a horizontally-scrolling parent) with plain CSS overflow + a min-h-0 flex chain.
- Detail pane in board mode is now 560px by default (was 480px, which squeezed the title in the header) and resizable from its left edge, persisted to its own store so it doesn't fight the list-mode sidebar width.
- Infinite-scroll trigger moves to the bottom of the longest column so it actually enters the viewport when scrolling.

Generated-By: PostHog Code
Task-Id: bb7bd45f-d16b-4cca-a8bd-00856f6c5004
Grouping by `status` is almost useless because nearly all reports sit in `ready` (the pipeline statuses cover the minority of items still being researched), so the board collapses into a single column.

Adds a `Group by` menu next to the View toggle (only visible in board mode) with three options — Actionability (default), Priority, Status — and an `inboxBoardGrouping` helper that maps a report to a column id for each option. Actionability shows Actionable / Needs input / Not actionable / In pipeline; Priority shows P0…P4 plus Unprioritized; Status keeps the previous behaviour.

Persisted in the existing inbox filter store as `boardGroupBy`.

Generated-By: PostHog Code
Task-Id: bb7bd45f-d16b-4cca-a8bd-00856f6c5004
@fercgomes fercgomes marked this pull request as ready for review May 25, 2026 17:18
@fercgomes fercgomes requested a review from a team May 25, 2026 17:18
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 25, 2026

Comments Outside Diff (1)

  1. apps/code/src/renderer/features/inbox/components/board/InboxBoardView.tsx, line 611-612 (link)

    P2 Column count only reflects locally-loaded data

    The badge shows reports.length, which is the count of items from pages already fetched for that column — not the server-side total. When there are more pages to load, a user in the "ready" lane could see "4" even though 40 items exist in that status. Because the load-more sentinel is anchored to the single tallest column, items in shorter columns never get their "true" count shown and users have no signal that more exist elsewhere. Consider appending a + suffix (e.g. 4+) when hasNextPage is true, or keeping the column count blank/hidden until all pages are loaded.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/code/src/renderer/features/inbox/components/board/InboxBoardView.tsx
    Line: 611-612
    
    Comment:
    **Column count only reflects locally-loaded data**
    
    The badge shows `reports.length`, which is the count of items from pages already fetched for that column — not the server-side total. When there are more pages to load, a user in the "ready" lane could see "4" even though 40 items exist in that status. Because the load-more sentinel is anchored to the single tallest column, items in shorter columns never get their "true" count shown and users have no signal that more exist elsewhere. Consider appending a `+` suffix (e.g. `4+`) when `hasNextPage` is true, or keeping the column count blank/hidden until all pages are loaded.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/code/src/renderer/features/inbox/components/board/InboxBoardView.tsx:611-612
**Column count only reflects locally-loaded data**

The badge shows `reports.length`, which is the count of items from pages already fetched for that column — not the server-side total. When there are more pages to load, a user in the "ready" lane could see "4" even though 40 items exist in that status. Because the load-more sentinel is anchored to the single tallest column, items in shorter columns never get their "true" count shown and users have no signal that more exist elsewhere. Consider appending a `+` suffix (e.g. `4+`) when `hasNextPage` is true, or keeping the column count blank/hidden until all pages are loaded.

### Issue 2 of 2
apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx:463-498
**Board detail pane resize duplicates the existing sidebar resize logic**

The board detail pane's `handleBoardDetailResizeMouseDown` + its `useEffect` for `mousemove`/`mouseup` are structurally identical to the existing sidebar resize that handles `handleResizeMouseDown`. Both set `body.style.cursor/userSelect` on drag start, compute width from the same pattern, and clean up on mouseup. This is an OnceAndOnlyOnce violation — consider extracting a shared `useResizeHandle(isResizing, setIsResizing, setWidth, containerRef, options)` hook that both handles can reuse.

```suggestion
  // TODO: extract a shared useResizeHandle hook to cover both this and the
  // sidebar resize below — both use identical mousemove/mouseup logic.
  const handleBoardDetailResizeMouseDown = useCallback(
    (e: React.MouseEvent) => {
      e.preventDefault();
      setBoardDetailIsResizing(true);
      document.body.style.cursor = "col-resize";
      document.body.style.userSelect = "none";
    },
    [setBoardDetailIsResizing],
  );

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      if (!boardDetailIsResizing || !boardContainerRef.current) return;
      const rect = boardContainerRef.current.getBoundingClientRect();
      const containerWidth = rect.width;
      const maxWidth = Math.max(420, containerWidth * 0.7);
      const newWidth = Math.max(
        420,
        Math.min(maxWidth, rect.right - e.clientX),
      );
      setBoardDetailWidth(newWidth);
    };
    const handleMouseUp = () => {
      if (boardDetailIsResizing) {
        setBoardDetailIsResizing(false);
        document.body.style.cursor = "";
        document.body.style.userSelect = "";
      }
    };
    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);
    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
  }, [boardDetailIsResizing, setBoardDetailWidth, setBoardDetailIsResizing]);
```

Reviews (1): Last reviewed commit: "feat(code): group inbox board by actiona..." | Re-trigger Greptile

Comment on lines +463 to +498
const handleBoardDetailResizeMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
setBoardDetailIsResizing(true);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
},
[setBoardDetailIsResizing],
);

useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!boardDetailIsResizing || !boardContainerRef.current) return;
const rect = boardContainerRef.current.getBoundingClientRect();
const containerWidth = rect.width;
const maxWidth = Math.max(420, containerWidth * 0.7);
const newWidth = Math.max(
420,
Math.min(maxWidth, rect.right - e.clientX),
);
setBoardDetailWidth(newWidth);
};
const handleMouseUp = () => {
if (boardDetailIsResizing) {
setBoardDetailIsResizing(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [boardDetailIsResizing, setBoardDetailWidth, setBoardDetailIsResizing]);
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 Board detail pane resize duplicates the existing sidebar resize logic

The board detail pane's handleBoardDetailResizeMouseDown + its useEffect for mousemove/mouseup are structurally identical to the existing sidebar resize that handles handleResizeMouseDown. Both set body.style.cursor/userSelect on drag start, compute width from the same pattern, and clean up on mouseup. This is an OnceAndOnlyOnce violation — consider extracting a shared useResizeHandle(isResizing, setIsResizing, setWidth, containerRef, options) hook that both handles can reuse.

Suggested change
const handleBoardDetailResizeMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
setBoardDetailIsResizing(true);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
},
[setBoardDetailIsResizing],
);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!boardDetailIsResizing || !boardContainerRef.current) return;
const rect = boardContainerRef.current.getBoundingClientRect();
const containerWidth = rect.width;
const maxWidth = Math.max(420, containerWidth * 0.7);
const newWidth = Math.max(
420,
Math.min(maxWidth, rect.right - e.clientX),
);
setBoardDetailWidth(newWidth);
};
const handleMouseUp = () => {
if (boardDetailIsResizing) {
setBoardDetailIsResizing(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [boardDetailIsResizing, setBoardDetailWidth, setBoardDetailIsResizing]);
// TODO: extract a shared useResizeHandle hook to cover both this and the
// sidebar resize below — both use identical mousemove/mouseup logic.
const handleBoardDetailResizeMouseDown = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
setBoardDetailIsResizing(true);
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
},
[setBoardDetailIsResizing],
);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
if (!boardDetailIsResizing || !boardContainerRef.current) return;
const rect = boardContainerRef.current.getBoundingClientRect();
const containerWidth = rect.width;
const maxWidth = Math.max(420, containerWidth * 0.7);
const newWidth = Math.max(
420,
Math.min(maxWidth, rect.right - e.clientX),
);
setBoardDetailWidth(newWidth);
};
const handleMouseUp = () => {
if (boardDetailIsResizing) {
setBoardDetailIsResizing(false);
document.body.style.cursor = "";
document.body.style.userSelect = "";
}
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [boardDetailIsResizing, setBoardDetailWidth, setBoardDetailIsResizing]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx
Line: 463-498

Comment:
**Board detail pane resize duplicates the existing sidebar resize logic**

The board detail pane's `handleBoardDetailResizeMouseDown` + its `useEffect` for `mousemove`/`mouseup` are structurally identical to the existing sidebar resize that handles `handleResizeMouseDown`. Both set `body.style.cursor/userSelect` on drag start, compute width from the same pattern, and clean up on mouseup. This is an OnceAndOnlyOnce violation — consider extracting a shared `useResizeHandle(isResizing, setIsResizing, setWidth, containerRef, options)` hook that both handles can reuse.

```suggestion
  // TODO: extract a shared useResizeHandle hook to cover both this and the
  // sidebar resize below — both use identical mousemove/mouseup logic.
  const handleBoardDetailResizeMouseDown = useCallback(
    (e: React.MouseEvent) => {
      e.preventDefault();
      setBoardDetailIsResizing(true);
      document.body.style.cursor = "col-resize";
      document.body.style.userSelect = "none";
    },
    [setBoardDetailIsResizing],
  );

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      if (!boardDetailIsResizing || !boardContainerRef.current) return;
      const rect = boardContainerRef.current.getBoundingClientRect();
      const containerWidth = rect.width;
      const maxWidth = Math.max(420, containerWidth * 0.7);
      const newWidth = Math.max(
        420,
        Math.min(maxWidth, rect.right - e.clientX),
      );
      setBoardDetailWidth(newWidth);
    };
    const handleMouseUp = () => {
      if (boardDetailIsResizing) {
        setBoardDetailIsResizing(false);
        document.body.style.cursor = "";
        document.body.style.userSelect = "";
      }
    };
    document.addEventListener("mousemove", handleMouseMove);
    document.addEventListener("mouseup", handleMouseUp);
    return () => {
      document.removeEventListener("mousemove", handleMouseMove);
      document.removeEventListener("mouseup", handleMouseUp);
    };
  }, [boardDetailIsResizing, setBoardDetailWidth, setBoardDetailIsResizing]);
```

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!

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