Skip to content

feat(issues): Attention Inbox issue-management control center#183

Merged
khaliqgant merged 1 commit into
mainfrom
feat/issues-control-center
Jun 9, 2026
Merged

feat(issues): Attention Inbox issue-management control center#183
khaliqgant merged 1 commit into
mainfrom
feat/issues-control-center

Conversation

@khaliqgant

Copy link
Copy Markdown
Member

Summary

Adds a global Issues control-center surface to Pear: an Attention Inbox (not a kanban) — Needs you / In motion / Settled bands, with status (7-stage) as a derived chip and actor as a label.

  • 3-level drill-down: overview card (with a live agent-authored "what's happening" line) → status detail → jump into the live Pear project/agent implementing the issue.
  • Bidirectional nav: forward jump (card → project) + reverse ▸ implementing PEAR-X chip (agent view → card) + selected-issue persistence.
  • Status filter across all bands, derived from the real states present.
  • Linear deep-link on the detail panel; prominent sidebar entry with a needs-you count badge.

Key internals

  • issues-store: reads /linear/issues, normalizes real {envelope,payload} records and flat mock fixtures, joins GitHub by syncedWith, classifies bands from real Linear state/assignee signals (review-stage → needs-you; completed/canceled + Merged/Done → settled; assigned → human actor; codified agent/pairing/human labels still win).
  • integrations.ts: routes remote integration reads to the account/provider workspace (getAccountWorkspaceId) with a cached handle — fixes provider reads returning empty.
  • Web-first: same component runs on mock IPC (VITE_PEAR_MOCK_IPC, npm run build:web) and in Electron against the real mounts, no rewrite.

Testing

  • tsc -p tsconfig.web.json --noEmit ✅ · npm run build:web
  • Verified live: web build renders fixtures; Electron renders the real 67-issue Agent Relay Linear board.
  • Note: tsc -b is red on pre-existing main-process/test drift, unrelated to this change.

Follow-ups (not in this PR)

  • Phase 2: writeMount IPC to make the inert action buttons live.
  • Optional: codify agent/pairing/human labels in Linear (operator UI) for authoritative actor tagging.

Spec + handoff: docs/specs/2026-06-09-issue-control-center*.md.

🤖 Generated with Claude Code

A global "Issues" control-center surface — an Attention Inbox (not a kanban):
Needs you / In motion / Settled bands, 3-level drill-down (card → status detail
→ jump into the live project/agent), bidirectional issue⟷agent navigation,
status filter, agent-chat-on-card, and a Linear deep-link.

- issues-store: reads /linear/issues, normalizes real {envelope,payload} records
  and flat mock fixtures, joins GitHub by syncedWith, classifies bands from real
  Linear state/assignee signals (review-stage → needs-you; completed/canceled +
  Merged/Done → settled; assigned → human actor; codified labels still win)
- integrations.ts: route remote integration reads to the account/provider
  workspace (getAccountWorkspaceId) with a cached handle, fixing empty reads
- ui-store / App / sidebar: 'issues' tab + selected-issue persistence, prominent
  sidebar entry with a needs-you count badge
- web-first via mock IPC (VITE_PEAR_MOCK_IPC); the same component runs in Electron
  against the real Linear/GitHub mounts with no rewrite
- spec + handoff under docs/specs/2026-06-09-issue-control-center*.md

Verified: tsc -p tsconfig.web.json green, build:web green. (tsc -b is red on
pre-existing main-process/test drift unrelated to this change.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@codeant-ai

codeant-ai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI.

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements the "Attention Inbox" issue-control-center feature: a read-only inbox that loads Linear issues via remote integration files, classifies them into three attention bands (Needs you / In motion / Settled), and enables navigation from issues into live agent workspaces. The implementation spans design specs, data stores, main-process remote file handling, UI components, and mock fixtures for development.

Changes

Attention Inbox Issue Control Center

Layer / File(s) Summary
Design specifications
docs/specs/2026-06-09-issue-control-center*.md
Design spec and handoff documentation defining the issue-control-center concept, Linear-as-source-of-truth backing store, locked Attention Inbox UI with three bands, web-first delivery plan, phased roadmap, and known Phase 1 behavior.
Issues store: data loading and classification
src/renderer/src/stores/issues-store.ts
Zustand store that discovers and loads Linear issue JSON files from remote integration paths, parses and enriches with GitHub synced records, classifies into bands (needs-you / in-motion / settled) based on stage/state/labels, and sorts by priority/timestamp. Exports agentForIssue and issueForAgent lookup helpers and subscribe method with debounced refresh on integration events.
Main process: Relayfile remote reader
src/main/integrations.ts
Adds dedicated Relayfile workspace handle for reading integration remote files, cached by account. Implements 401/403 token refresh and retry logic: refreshes token on auth failure, clears cache if refresh fails, and re-creates handle on subsequent retry.
UI store: issues tab and selection state
src/renderer/src/stores/ui-store.ts
Extends UI store to support 'issues' view/tab kind with persistent selectedIssueId in localStorage and transient agentJumpIssueId breadcrumb. Updates tab routing helpers, defaults to issues tab in web-mock builds, and clears breadcrumb on non-issues navigation.
App routing for issues view
src/renderer/src/App.tsx
Wires AttentionInbox component into mainView rendering when activeTab.kind === 'issues'.
AttentionInbox component: three-band inbox UI
src/renderer/src/components/issues/AttentionInbox.tsx
Full React component rendering Attention Inbox with collapsible band sections, stage-based filtering, issue overview cards with expandable agent-chat previews, right-side detail panel showing GitHub links and live-project CTA, and refresh button. Resolves project context with VITE_PEAR_MOCK_IPC fallback.
Issue navigation: jump to live workspace
src/renderer/src/lib/issue-navigation.ts
Implements jumpToIssueWork function that navigates from inbox issue into live agent workspace, branching by assignee/spawn state and environment: returns stub results in web-mock mode, handles unassigned cases, performs real Electron tab/project/agent switching for running agents, and opens spawn dialog for assigned-but-not-spawned cases.
Sidebar and terminal navigation integration
src/renderer/src/components/sidebar/ProjectSidebar.tsx, src/renderer/src/components/terminal/TerminalPane.tsx
Adds Issues navigation entry in sidebar with needs-you counter badge, and adds ActiveAgentIssueChip in terminal header rendering implementing/back breadcrumb derived from agent mapping or jump state for bidirectional navigation.
Mock fixtures and development support
src/renderer/src/lib/ipc-mock.ts
Extends mock with deterministic time utilities, pre-seeded mock agents, comprehensive Linear and GitHub issue/PR fixtures keyed by normalized remote paths. Implements fixture-backed listRemoteDir and readRemoteFile for development and web-first testing.

Sequence Diagram

sequenceDiagram
  participant Inbox as AttentionInbox
  participant Store as IssuesStore
  participant Integrations as Integrations API
  participant Remote as Remote Files
  participant Navigation as jumpToIssueWork

  Inbox->>Store: load(projectId)
  Store->>Integrations: listRemoteDirectory(/linear/issues)
  Integrations->>Remote: read issue JSON files
  Remote-->>Integrations: issue records
  Integrations-->>Store: file list
  
  loop For each issue file
    Store->>Integrations: readRemoteFile(path)
    Integrations->>Remote: fetch JSON preview
    Remote-->>Integrations: parsed issue data
    Integrations-->>Store: issue record
    Store->>Store: classify into band
  end
  
  Store-->>Inbox: normalized issues by band
  Inbox->>Inbox: render three-band layout
  
  Inbox->>Navigation: jumpToIssueWork(issue)
  Navigation->>Navigation: findTrackedAgent(issueAssignee)
  alt Agent running
    Navigation->>Inbox: switch tab, set active agent
    Navigation-->>Inbox: jumped result
  else Agent not spawned
    Navigation->>Inbox: open spawn dialog
    Navigation-->>Inbox: spawn result
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Three bands of issues, sorted with care,
Loading from Linear, enriched GitHub fare,
Click an inbox card, jump to the work,
Breadcrumbs guide you, no focus to shirk!
Phase One's reading—the write comes ahead.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'feat(issues): Attention Inbox issue-management control center' accurately summarizes the primary change: adding an Attention Inbox control center for issue management with status bands and navigation features.
Description check ✅ Passed The pull request description provides comprehensive context about the Attention Inbox feature, including UI structure (three bands), navigation patterns, key internals (issues-store, integrations routing), testing validation, and references to implementation specs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issues-control-center

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #183 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements Phase 1 of the Pear Issue-Management Control Center (Attention Inbox), introducing a web-first, read-only dashboard that displays normalized Linear and GitHub issues categorized by attention bands. Key additions include the AttentionInbox component, a Zustand-based issues-store with real-time subscription support, sidebar navigation integration, and reverse navigation chips in the terminal pane. The main process was also updated to cache remote workspace handles for routing reads. The review feedback highlights several important improvements for the frontend store and UI: implementing a load generation counter and batching remote file reads to prevent race conditions and network congestion, hoisting the real-time subscription logic so the sidebar badge updates when the inbox is unmounted, and using globalThis.crypto.randomUUID() to ensure compatibility across all environments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +61 to +65
let subscription: (() => void) | null = null
let subscriptionProjectId: string | null = null
let subscriptionGeneration = 0
let refreshTimer: ReturnType<typeof setTimeout> | null = null
const loadPromises = new Map<string, Promise<void>>()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

To prevent race conditions where out-of-order async load operations overwrite the store with stale data, we should introduce a loadGeneration counter. This counter will be incremented on every load request and checked before updating the store state.

Suggested change
let subscription: (() => void) | null = null
let subscriptionProjectId: string | null = null
let subscriptionGeneration = 0
let refreshTimer: ReturnType<typeof setTimeout> | null = null
const loadPromises = new Map<string, Promise<void>>()
let subscription: (() => void) | null = null
let subscriptionProjectId: string | null = null
let subscriptionGeneration = 0
let refreshTimer: ReturnType<typeof setTimeout> | null = null
let loadGeneration = 0
const loadPromises = new Map<string, Promise<void>>()

Comment on lines +293 to +334
load: async (projectId, options = {}) => {
const key = `${projectId}:${options.force === true ? 'force' : 'normal'}`
const existing = loadPromises.get(key)
if (existing) return existing

const promise = (async () => {
set({ loading: true, error: null })
try {
const entries = await pear.integrations.listRemoteDir(projectId, '/linear/issues')
const records = await Promise.all(
entries.filter(isIssueFile).map(async (entry) => {
const preview = await pear.integrations.readRemoteFile(projectId, entry.path)
return preview.kind === 'text' ? parseJsonPreview(preview.content, entry.path) : null
})
)
const issues = await Promise.all(
records
.filter((record): record is Record<string, unknown> => !!record)
.map((record) => normalizeIssue(projectId, record))
)

set({
issues: sortIssues(issues),
loading: false,
error: null,
loadedProjectId: projectId,
lastLoadedAt: Date.now()
})
} catch (error) {
set({
loading: false,
error: error instanceof Error ? error.message : String(error),
loadedProjectId: projectId
})
}
})().finally(() => {
loadPromises.delete(key)
})

loadPromises.set(key, promise)
return promise
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

This refactored load function addresses two critical issues:\n1. Concurrency/Rate-limiting: Instead of firing up to 67+ concurrent remote SDK network requests via Promise.all, it batches the readRemoteFile calls in chunks of 5 to prevent rate-limiting and socket exhaustion.\n2. Race Conditions: It checks the loadGeneration counter before calling set(...) to ensure that slower, stale load requests do not overwrite fresher data.

  load: async (projectId, options = {}) => {
    const key = `${projectId}:${options.force === true ? 'force' : 'normal'}`
    const existing = loadPromises.get(key)
    if (existing) return existing

    const generation = ++loadGeneration
    const promise = (async () => {
      set({ loading: true, error: null })
      try {
        const entries = await pear.integrations.listRemoteDir(projectId, '/linear/issues')
        const records: Array<Record<string, unknown> | null> = []
        const filtered = entries.filter(isIssueFile)
        const limit = 5
        for (let i = 0; i < filtered.length; i += limit) {
          const chunk = filtered.slice(i, i + limit)
          const chunkRecords = await Promise.all(
            chunk.map(async (entry) => {
              const preview = await pear.integrations.readRemoteFile(projectId, entry.path)
              return preview.kind === 'text' ? parseJsonPreview(preview.content, entry.path) : null
            })
          )
          records.push(...chunkRecords)
        }

        const issues = await Promise.all(
          records
            .filter((record): record is Record<string, unknown> => !!record)
            .map((record) => normalizeIssue(projectId, record))
        )

        if (generation !== loadGeneration) return
        set({
          issues: sortIssues(issues),
          loading: false,
          error: null,
          loadedProjectId: projectId,
          lastLoadedAt: Date.now()
        })
      } catch (error) {
        if (generation !== loadGeneration) return
        set({
          loading: false,
          error: error instanceof Error ? error.message : String(error),
          loadedProjectId: projectId
        })
      }
    })().finally(() => {
      loadPromises.delete(key)
    })

    loadPromises.set(key, promise)
    return promise
  },

Comment on lines +771 to +775
function IssuesNavEntry({ collapsed = false }: { collapsed?: boolean }): React.ReactNode {
const openTab = useUIStore((s) => s.openTab)
const activeTab = useUIStore((s) => s.tabs.find((tab) => tab.id === s.activeTabId))
const active = activeTab?.kind === 'issues'
const needsYouCount = useIssuesStore((s) => s.issues.reduce((count, issue) => issue.band === 'needs-you' ? count + 1 : count, 0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The needsYouCount badge in the sidebar reads from useIssuesStore, but the real-time event subscription (subscribe) is only active when the AttentionInbox component is mounted. When the user is on other tabs (like "Agents" or "Settings"), AttentionInbox is unmounted, which cleans up the subscription. Consequently, the sidebar badge will not update in real-time when the user is working on other tabs.\n\nTo fix this, consider moving the subscribe and background load logic to a higher level (e.g., in App.tsx or a global layout hook) so that the issues list and the sidebar badge stay updated in the background regardless of the active tab.

)).filter((link): link is IssueGithubLink => !!link)

return {
id: readString(issue.id) || readString(raw.objectId) || readString(issue.identifier) || crypto.randomUUID(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using crypto.randomUUID() directly can cause a ReferenceError in environments or test runners where crypto is not globally bound. Using globalThis.crypto.randomUUID() is safer and universally supported in modern JS environments.

Suggested change
id: readString(issue.id) || readString(raw.objectId) || readString(issue.identifier) || crypto.randomUUID(),
id: readString(issue.id) || readString(raw.objectId) || readString(issue.identifier) || globalThis.crypto.randomUUID(),

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #183 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #183 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #183 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #183 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/specs/2026-06-09-issue-control-center-HANDOFF.md`:
- Line 37: Update the stale branch attribution line "Working tree (UNCOMMITTED —
on branch `feat/mount-optional-dep-no-postinstall`)" to reflect the actual
branch and commit provenance for this PR; locate that exact string in the
handoff and replace the branch name and/or add the current commit SHA (e.g.,
`$(git rev-parse --abbrev-ref HEAD)` and `$(git rev-parse --short HEAD)`) so the
line shows the real branch name and commit id for accurate replay/cherry-pick.

In `@docs/specs/2026-06-09-issue-control-center.md`:
- Around line 100-113: The fenced code block containing the ASCII UI diagram
should include an explicit language tag to satisfy MD040; update the opening
fence from ``` to a tagged fence such as ```text (or ```ascii/```plain) in the
docs/specs/2026-06-09-issue-control-center.md file so the block is annotated and
the linter no longer reports MD040 for that snippet.
- Around line 170-184: The document currently contradicts itself by stating
"Web-first delivery" while the Non-Goals claim "no web surface"; update the spec
so there's a single source of truth: either change the Non-Goals to acknowledge
a web surface (and mention the mechanisms like VITE_PEAR_MOCK_IPC in
vite.web.config.ts, the pear.auto-swap via src/renderer/src/lib/ipc.ts, the mock
implementation in src/renderer/src/lib/ipc-mock.ts, and the build step npm run
build:web → out/web) or remove/modify the Web-first section to match the
Non-Goals and acceptance criteria; ensure references to pear.integrations.* and
the mock/electron swap remain consistent with the chosen stance.
- Around line 66-79: The fenced code block that begins with "INBOX · pear" is
missing a language tag; update the triple-backtick fence to include a language
(e.g., change ``` to ```text) so markdownlint MD040 is satisfied and doc checks
pass, ensuring the block containing "INBOX · pear" is the only fence you modify.

In `@src/renderer/src/components/issues/AttentionInbox.tsx`:
- Line 391: shortRelativeTime(issue.updatedAt) can return "now" for
undefined/invalid dates which currently renders as "now ago"; update the JSX in
AttentionInbox (the <dd> that uses shortRelativeTime(issue.updatedAt)) to handle
that case by either checking issue.updatedAt before appending "ago" or by
inspecting the returned string and rendering "just now" (or no "ago") when
shortRelativeTime(...) === "now"; adjust the output so that only valid relative
strings get "ago" appended and ensure the change references the
shortRelativeTime call inside the AttentionInbox component.

In `@src/renderer/src/components/sidebar/ProjectSidebar.tsx`:
- Around line 787-789: The collapsed button's aria-label currently stays static
("Issues") while the visual badge uses needsYouCount; update the JSX that
renders the Issues button (the element with title={...} and aria-label="Issues"
in ProjectSidebar component) to include the needsYouCount when >0 (for example:
aria-label={`Issues${needsYouCount > 0 ? ` — ${needsYouCount} need you` :
''}`}), so assistive tech receives the same urgent-item information as the
visual badge.

In `@src/renderer/src/components/terminal/TerminalPane.tsx`:
- Around line 399-401: The current mapping uses only agent.name (mappedIssue via
useMemo calling issueForAgent(agent.name)), which can return wrong issues across
projects; change the lookup to pass a stable project identity plus agent name
(e.g., issueForAgent(projectId, agent.name) or a stable agentKey) when computing
mappedIssue, and update the issues-store helper signature (issueForAgent) and
all its callers to accept and use (projectId, agentName) so lookups are scoped
by project. Ensure you update any type signatures and tests that reference
issueForAgent to the new contract.

In `@src/renderer/src/lib/issue-navigation.ts`:
- Around line 80-83: Awaiting projectStore.setActiveProject(...) is unguarded
and may reject, leaving the jump state half-applied; wrap the setActiveProject
call in a try/catch, and on failure handle it before any jump state updates
(e.g., log/report and return early or propagate the error) so the code does not
continue updating jump state when project switching failed; locate the block
that checks projectStore.projects.some(...), the setActiveProject call, and the
subsequent jump state update and ensure failures from setActiveProject are
caught and prevent further jump-state transitions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d9296577-2c0d-480c-be29-a33f2b96ffb8

📥 Commits

Reviewing files that changed from the base of the PR and between 901d508 and 9553083.

📒 Files selected for processing (11)
  • docs/specs/2026-06-09-issue-control-center-HANDOFF.md
  • docs/specs/2026-06-09-issue-control-center.md
  • src/main/integrations.ts
  • src/renderer/src/App.tsx
  • src/renderer/src/components/issues/AttentionInbox.tsx
  • src/renderer/src/components/sidebar/ProjectSidebar.tsx
  • src/renderer/src/components/terminal/TerminalPane.tsx
  • src/renderer/src/lib/ipc-mock.ts
  • src/renderer/src/lib/issue-navigation.ts
  • src/renderer/src/stores/issues-store.ts
  • src/renderer/src/stores/ui-store.ts

and (2) the integration scope mount (`updateScope` adding `/linear/issues`). The web
build hot-reloads, but main-process reads needed the restart. Done; now live.

## Working tree (UNCOMMITTED — on branch `feat/mount-optional-dep-no-postinstall`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update stale branch attribution in the handoff.

This line conflicts with the current PR branch context and can cause confusion when someone tries to replay or cherry-pick the work later. Please update it to match the actual branch/commit provenance for this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/2026-06-09-issue-control-center-HANDOFF.md` at line 37, Update the
stale branch attribution line "Working tree (UNCOMMITTED — on branch
`feat/mount-optional-dep-no-postinstall`)" to reflect the actual branch and
commit provenance for this PR; locate that exact string in the handoff and
replace the branch name and/or add the current commit SHA (e.g., `$(git
rev-parse --abbrev-ref HEAD)` and `$(git rev-parse --short HEAD)`) so the line
shows the real branch name and commit id for accurate replay/cherry-pick.

Comment on lines +66 to +79
```
INBOX · pear 3 need you
──────────────────────────────────────────────────
⚠ NEEDS YOU (only humans can clear these)
PEAR-145 review PR #182 ⑂ ✓ ✗
PEAR-149 "which auth flow?" reply
──────────────────────────────────────────────────
◐ IN MOTION (4) ▾ ← collapsed/ambient
implementer · PEAR-148 · editing ipc.ts · 2m
──────────────────────────────────────────────────
✓ SETTLED today (6) ▾
──────────────────────────────────────────────────
· all clear when empty ·
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to this fenced code block (MD040).

Use a fence like ```text to satisfy markdownlint and keep doc checks clean.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 66-66: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/2026-06-09-issue-control-center.md` around lines 66 - 79, The
fenced code block that begins with "INBOX · pear" is missing a language tag;
update the triple-backtick fence to include a language (e.g., change ``` to
```text) so markdownlint MD040 is satisfied and doc checks pass, ensuring the
block containing "INBOX · pear" is the only fence you modify.

Source: Linters/SAST tools

Comment on lines +100 to +113
```
L1 Overview card L2 Status detail L3 The Project (live)
┌────────────────┐ ▸ ┌──────────────────┐ ▸ ┌────────────────────┐
│ PEAR-148 │ │ PEAR-148 │ │ Pear ▸ project X │
│ writeMount │ │ stage: In Prog. │ │ agent: implementer│
│ ◐ implementer │ │ ◐ editing ipc.ts │ │ ┌──────────────┐ │
│ ┄┄ agent chat ┄│ │ trajectory ↓ │ │ │ live terminal│ │
│ "wiring write- │ │ 🤖 decided … ↗ │ │ │ / agent chat │ │
│ Mount thru │ │ PR #182 · CI ✓ │ │ └──────────────┘ │
│ preload, 1 │ │ [open project ▸]│ │ you are now here │
│ type left" 2m │ └──────────────────┘ └────────────────────┘
└────────────────┘
glance + live narration
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a language tag to this fenced code block (MD040).

Same lint issue here; annotate with ```text (or another explicit language).

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 100-100: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/2026-06-09-issue-control-center.md` around lines 100 - 113, The
fenced code block containing the ASCII UI diagram should include an explicit
language tag to satisfy MD040; update the opening fence from ``` to a tagged
fence such as ```text (or ```ascii/```plain) in the
docs/specs/2026-06-09-issue-control-center.md file so the block is annotated and
the linter no longer reports MD040 for that snippet.

Source: Linters/SAST tools

Comment on lines +170 to +184
## Web-first delivery (LOCKED)

Build the Inbox as a **standalone web view first** so it's viewable in a browser
immediately, then folds into Pear with **zero rewrite**. The seam already exists:

| Mechanism | Where | Effect |
|---|---|---|
| `VITE_PEAR_MOCK_IPC=true` | `vite.web.config.ts` | web build forces mock IPC |
| auto-swap `pearMock` vs `electronPear` | `src/renderer/src/lib/ipc.ts` | same `pear` API, different backend |
| mock impl | `src/renderer/src/lib/ipc-mock.ts` | add sample Linear/GitHub issue fixtures here |
| build | `npm run build:web` → `out/web` | open in any browser |

Component code calls the same `pear.integrations.*` API in both modes. Web =
`pearMock` (fixtures). Electron = real mounts. No component change crossing over.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve the web-surface contradiction in the spec.

The spec mandates browser-first delivery, but the non-goal says there is no web surface. Keep one source of truth here; otherwise implementation and acceptance criteria will diverge.

Also applies to: 233-233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/specs/2026-06-09-issue-control-center.md` around lines 170 - 184, The
document currently contradicts itself by stating "Web-first delivery" while the
Non-Goals claim "no web surface"; update the spec so there's a single source of
truth: either change the Non-Goals to acknowledge a web surface (and mention the
mechanisms like VITE_PEAR_MOCK_IPC in vite.web.config.ts, the pear.auto-swap via
src/renderer/src/lib/ipc.ts, the mock implementation in
src/renderer/src/lib/ipc-mock.ts, and the build step npm run build:web →
out/web) or remove/modify the Web-first section to match the Non-Goals and
acceptance criteria; ensure references to pear.integrations.* and the
mock/electron swap remain consistent with the chosen stance.

<dt className="text-[var(--pear-text-faint)]">Team</dt>
<dd className="text-[var(--pear-text)]">{issue.teamName || 'Unknown'}</dd>
<dt className="text-[var(--pear-text-faint)]">Updated</dt>
<dd className="text-[var(--pear-text)]">{shortRelativeTime(issue.updatedAt)} ago</dd>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Grammatical issue: displays "now ago" when updatedAt is undefined.

shortRelativeTime returns 'now' for undefined/invalid dates, producing "now ago" in the UI.

Proposed fix
-          <dd className="text-[var(--pear-text)]">{shortRelativeTime(issue.updatedAt)} ago</dd>
+          <dd className="text-[var(--pear-text)]">{issue.updatedAt ? `${shortRelativeTime(issue.updatedAt)} ago` : 'Just now'}</dd>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<dd className="text-[var(--pear-text)]">{shortRelativeTime(issue.updatedAt)} ago</dd>
<dd className="text-[var(--pear-text)]">{issue.updatedAt ? `${shortRelativeTime(issue.updatedAt)} ago` : 'Just now'}</dd>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/issues/AttentionInbox.tsx` at line 391,
shortRelativeTime(issue.updatedAt) can return "now" for undefined/invalid dates
which currently renders as "now ago"; update the JSX in AttentionInbox (the <dd>
that uses shortRelativeTime(issue.updatedAt)) to handle that case by either
checking issue.updatedAt before appending "ago" or by inspecting the returned
string and rendering "just now" (or no "ago") when shortRelativeTime(...) ===
"now"; adjust the output so that only valid relative strings get "ago" appended
and ensure the change references the shortRelativeTime call inside the
AttentionInbox component.

Comment on lines +787 to +789
title={needsYouCount > 0 ? `Issues — ${needsYouCount} need you` : 'Issues'}
aria-label="Issues"
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Expose the needs-you count in collapsed button accessibility text.

In collapsed mode, the badge count is visual-only; aria-label stays "Issues" even when urgent items exist.

Suggested fix
-        aria-label="Issues"
+        aria-label={needsYouCount > 0 ? `Issues, ${needsYouCount} need you` : 'Issues'}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
title={needsYouCount > 0 ? `Issues — ${needsYouCount} need you` : 'Issues'}
aria-label="Issues"
>
title={needsYouCount > 0 ? `Issues — ${needsYouCount} need you` : 'Issues'}
aria-label={needsYouCount > 0 ? `Issues, ${needsYouCount} need you` : 'Issues'}
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/sidebar/ProjectSidebar.tsx` around lines 787 -
789, The collapsed button's aria-label currently stays static ("Issues") while
the visual badge uses needsYouCount; update the JSX that renders the Issues
button (the element with title={...} and aria-label="Issues" in ProjectSidebar
component) to include the needsYouCount when >0 (for example:
aria-label={`Issues${needsYouCount > 0 ? ` — ${needsYouCount} need you` :
''}`}), so assistive tech receives the same urgent-item information as the
visual badge.

Comment on lines +399 to +401
const mappedIssue = useMemo(
() => (agent ? issueForAgent(agent.name) : undefined),
[agent, issues]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid name-only issue mapping; scope lookup by project identity.

issueForAgent(agent.name) is ambiguous when the same agent name exists in multiple projects, which can show/link the wrong issue chip.

Suggested direction
-  const mappedIssue = useMemo(
-    () => (agent ? issueForAgent(agent.name) : undefined),
-    [agent, issues]
-  )
+  const mappedIssue = useMemo(
+    () => (agent ? issueForAgent(agent.projectId, agent.name) : undefined),
+    [agent?.projectId, agent?.name, issues]
+  )

You’ll also need to align the issueForAgent helper contract in issues-store to use (projectId, agentName) (or a stable agent key).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/terminal/TerminalPane.tsx` around lines 399 -
401, The current mapping uses only agent.name (mappedIssue via useMemo calling
issueForAgent(agent.name)), which can return wrong issues across projects;
change the lookup to pass a stable project identity plus agent name (e.g.,
issueForAgent(projectId, agent.name) or a stable agentKey) when computing
mappedIssue, and update the issues-store helper signature (issueForAgent) and
all its callers to accept and use (projectId, agentName) so lookups are scoped
by project. Ensure you update any type signatures and tests that reference
issueForAgent to the new contract.

Comment on lines +80 to +83
if (targetProjectId && projectStore.projects.some((project) => project.id === targetProjectId)) {
if (projectStore.activeProjectId !== targetProjectId) {
await projectStore.setActiveProject(targetProjectId)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle project-switch failures before proceeding with jump state updates.

Line 82 awaits setActiveProject(...) without a guard. If it rejects, the click path rejects and the jump flow stops mid-transition.

Suggested fix
     if (targetProjectId && projectStore.projects.some((project) => project.id === targetProjectId)) {
       if (projectStore.activeProjectId !== targetProjectId) {
-        await projectStore.setActiveProject(targetProjectId)
+        try {
+          await projectStore.setActiveProject(targetProjectId)
+        } catch (error) {
+          console.error('[issues] Failed to activate target project during issue jump:', error)
+          return { kind: 'spawn', message: `Could not open ${agentName} for ${issue.identifier}`, agentName }
+        }
       }
       useUIStore.getState().openTab({ kind: 'agents', projectId: targetProjectId })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (targetProjectId && projectStore.projects.some((project) => project.id === targetProjectId)) {
if (projectStore.activeProjectId !== targetProjectId) {
await projectStore.setActiveProject(targetProjectId)
}
if (targetProjectId && projectStore.projects.some((project) => project.id === targetProjectId)) {
if (projectStore.activeProjectId !== targetProjectId) {
try {
await projectStore.setActiveProject(targetProjectId)
} catch (error) {
console.error('[issues] Failed to activate target project during issue jump:', error)
return { kind: 'spawn', message: `Could not open ${agentName} for ${issue.identifier}`, agentName }
}
}
useUIStore.getState().openTab({ kind: 'agents', projectId: targetProjectId })
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/lib/issue-navigation.ts` around lines 80 - 83, Awaiting
projectStore.setActiveProject(...) is unguarded and may reject, leaving the jump
state half-applied; wrap the setActiveProject call in a try/catch, and on
failure handle it before any jump state updates (e.g., log/report and return
early or propagate the error) so the code does not continue updating jump state
when project switching failed; locate the block that checks
projectStore.projects.some(...), the setActiveProject call, and the subsequent
jump state update and ensure failures from setActiveProject are caught and
prevent further jump-state transitions.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #183 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

1 similar comment
@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #183 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #183 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #183 in AgentWorkforce/pear.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@khaliqgant khaliqgant merged commit a1db31d into main Jun 9, 2026
4 checks passed
@khaliqgant khaliqgant deleted the feat/issues-control-center branch June 9, 2026 14:25
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