feat(issues): Attention Inbox issue-management control center#183
Conversation
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>
|
Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI. |
📝 WalkthroughWalkthroughThis 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. ChangesAttention Inbox Issue Control Center
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
pr-reviewer could not complete review for #183 in AgentWorkforce/pear. |
There was a problem hiding this comment.
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.
| 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>>() |
There was a problem hiding this comment.
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.
| 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>>() |
| 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 | ||
| }, |
There was a problem hiding this comment.
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
},| 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)) |
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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.
| 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(), |
|
pr-reviewer could not complete review for #183 in AgentWorkforce/pear. |
|
ℹ️ 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. |
|
pr-reviewer could not complete review for #183 in AgentWorkforce/pear. |
|
ℹ️ 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. |
There was a problem hiding this comment.
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
📒 Files selected for processing (11)
docs/specs/2026-06-09-issue-control-center-HANDOFF.mddocs/specs/2026-06-09-issue-control-center.mdsrc/main/integrations.tssrc/renderer/src/App.tsxsrc/renderer/src/components/issues/AttentionInbox.tsxsrc/renderer/src/components/sidebar/ProjectSidebar.tsxsrc/renderer/src/components/terminal/TerminalPane.tsxsrc/renderer/src/lib/ipc-mock.tssrc/renderer/src/lib/issue-navigation.tssrc/renderer/src/stores/issues-store.tssrc/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`) |
There was a problem hiding this comment.
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.
| ``` | ||
| 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 · | ||
| ``` |
There was a problem hiding this comment.
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
| ``` | ||
| 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 | ||
| ``` |
There was a problem hiding this comment.
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
| ## 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. | ||
|
|
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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.
| <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.
| title={needsYouCount > 0 ? `Issues — ${needsYouCount} need you` : 'Issues'} | ||
| aria-label="Issues" | ||
| > |
There was a problem hiding this comment.
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.
| 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.
| const mappedIssue = useMemo( | ||
| () => (agent ? issueForAgent(agent.name) : undefined), | ||
| [agent, issues] |
There was a problem hiding this comment.
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.
| if (targetProjectId && projectStore.projects.some((project) => project.id === targetProjectId)) { | ||
| if (projectStore.activeProjectId !== targetProjectId) { | ||
| await projectStore.setActiveProject(targetProjectId) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
|
ℹ️ 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. |
1 similar comment
|
ℹ️ 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. |
|
pr-reviewer could not complete review for #183 in AgentWorkforce/pear. |
|
ℹ️ 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. |
Summary
Adds a global Issues control-center surface to Pear: an Attention Inbox (not a kanban) —
Needs you/In motion/Settledbands, with status (7-stage) as a derived chip and actor as a label.▸ implementing PEAR-Xchip (agent view → card) + selected-issue persistence.Key internals
issues-store: reads/linear/issues, normalizes real{envelope,payload}records and flat mock fixtures, joins GitHub bysyncedWith, classifies bands from real Linear state/assignee signals (review-stage → needs-you; completed/canceled + Merged/Done → settled; assigned → human actor; codifiedagent/pairing/humanlabels still win).integrations.ts: routes remote integration reads to the account/provider workspace (getAccountWorkspaceId) with a cached handle — fixes provider reads returning empty.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✅tsc -bis red on pre-existing main-process/test drift, unrelated to this change.Follow-ups (not in this PR)
writeMountIPC to make the inert action buttons live.agent/pairing/humanlabels in Linear (operator UI) for authoritative actor tagging.Spec + handoff:
docs/specs/2026-06-09-issue-control-center*.md.🤖 Generated with Claude Code