Skip to content

feat: add pluggable issue adapters and workspace - #6315

Open
Bil0000 wants to merge 179 commits into
pingdotgg:mainfrom
Bil0000:feat/issues-page
Open

feat: add pluggable issue adapters and workspace#6315
Bil0000 wants to merge 179 commits into
pingdotgg:mainfrom
Bil0000:feat/issues-page

Conversation

@Bil0000

@Bil0000 Bil0000 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Adds an Issues workspace backed by provider adapters and one provider-neutral T3 issue model.

Why

Issue tracking should not be tied to source control. A project can use GitHub today and Jira or Linear later without teaching the UI a new data shape.

Architecture

GitHub / GitLab / Bitbucket / Azure DevOps
                    ↓
          IssueProviderApi adapters
                    ↓
       T3 issue contracts + IssueService
                    ↓
             T3 Code clients

Provider-specific API details stay inside adapters. The service handles project lookup, capabilities, pagination, errors, and normalized results. The UI renders only T3 issue contracts.

Current adapters:

  • GitHub
  • GitLab
  • Bitbucket
  • Azure DevOps work items

Jira and Linear are intentionally not included. A future adapter can implement the same interface and be selected by project settings without changing issue rendering.

User-facing changes

  • Full Issues page with host, project, state, involvement, label, sort, and search filters.
  • Right-panel issue browser and detail tabs beside threads and pull requests.
  • Markdown descriptions, activity, comments, reactions, labels, assignees, state actions, templates, and issue creation where the host supports them.
  • Agent hand-offs: Solve, Ask, Explain, Add to composer, and Link with agent.
  • Issue ↔ pull-request links, including cross-repository navigation.
  • Unsupported actions stay hidden and are also rejected by the server.

API and rate-limit safety

  • Reads are paginated, bounded, cached, and concurrency-limited.
  • GitHub issue and pull-request reads share one GraphQL budget, based on fix(pull-requests): protect provider API budgets #6466.
  • Budget pauses return structured retry times instead of issuing more requests.
  • Citation lookups are batched and capped after provider filtering.
  • Bounded linked-issue reads expose truncation instead of silently hiding more results.
  • One failed repository does not blank results from healthy repositories.
  • Provider and viewer state is scoped by host, including GitHub Enterprise.

Compatibility

Existing GitHub, GitLab, Bitbucket, and Azure DevOps source-control behavior remains intact. Pull-request linked-issue fields are optional on the wire for older server/client compatibility.

No issue-tracker plugin system, Jira adapter, Linear adapter, or new project setting is added here. This PR adds the smallest adapter seam needed for those later.

Verification

  • 197 focused linked-issue and adapter tests passed after final review fixes.
  • Server, web, and contracts type checks passed.
  • Full GitHub test, static analysis, smoke, correctness, UI consistency, and Effect checks passed on 3e449340f.
  • All review threads are resolved.
  • Vercel marketing preview remains blocked only by external account authorization.

Host writes are covered at the CLI invocation and response-decoding boundaries. A maintainer credential pass is still useful for GitHub, GitLab, Bitbucket, and Azure DevOps before release.

Built with Claude Opus 5 and GPT-5.6 Codex in T3 Code.

Note

Add pluggable issue adapters, Linear integration, and issues workspace panel

  • Introduces a full issue tracking subsystem with provider adapters for GitHub, GitLab, Bitbucket, Azure DevOps, and Linear, each backed by a CLI/API client and wired through a new IssueProviderRegistry and IssueService with caching and concurrency control
  • Adds an /issues route with a sidebar entry, filter/sort UI, detail panel, and right-panel tab support; the pull requests page gains linked-issue tabs and issue-to-PR cross-linking
  • Exposes ~25 new WebSocket RPC endpoints for issue CRUD, reactions, comments, Linear connection management, and AI work item task generation and match finding
  • Adds Linear connection management UI (LinearConnectionDialog) with per-project team binding, legacy migration, and settings patch support for projectBindings/projectTeams
  • Adds AI-powered work item features: generateWorkItemTask and findWorkItemMatches implemented across all text generation providers (Claude, Codex, Cursor, Grok, OpenCode) with prompt builders and a selection bar to draft agent tasks
  • Adds linked/cited issue resolution for GitHub and GitLab pull requests, parsing references from PR title/body and merging with host-reported links
  • Refactors shared source control UI into reusable components (ListRow, ListEmptyState, EntityPicker, SourceControlReactionBar, ConversationGroup, DetailTabStrip, etc.) consumed by both PR and issue surfaces
  • Risk: RIGHT_PANEL_STORAGE_VERSION bumped to 13, triggering migration of persisted panel state; invalid issue selections and shared issues panel entries are dropped silently

Macroscope summarized 8431d60.

Bil0000 added 13 commits August 12, 2026 11:04
Every wire shape the issue workspace needs: listings with the same
cursor and host bucketing the change-request listings use, a detail
carrying the change requests that reference an issue, activity split
off from the detail, and one input per write.

The actor, label, cursor and project-error shapes both features spell
the same way now live in `sourceControl.ts`, with the pull-request
names kept as aliases so nothing that imports them has to move.
The actor avatar and label, the dotted meta line, the host-markdown
body splitting, the project scope resolver and the composer hand-off
merge rules are all about a host rather than about a change request,
and the issue surfaces need every one of them.

They move to components/sourceControl and keep their pull-request
names as aliases, so no existing import site or test moves with them.
One interface per host, and a service that knows only the registry:
project discovery, per-host viewer resolution, listings merged across
repositories with cursors that neither repeat nor drop rows sharing an
instant, stale-while-revalidate caches, and epoch invalidation.

Every write is refused twice before a host is asked — once against what
the host can do at all, once against what this viewer may ask for — so
a request that never went past the page is refused by the host's own
answer rather than by the client's claim.

A repository whose tracker is switched off is reported as that one
repository failing, not as a dead host: Bitbucket turns issues off per
repository, and one of them must not blank the rest.
Both hosts do everything the port asks for: list, read, comment, file,
rewrite, close and reopen, labels, assignees, candidates and the change
requests that reference an issue.

Two things they do not share are declared rather than papered over.
GitHub records why an issue was closed and GitLab does not, so only
GitHub offers a reason. GitHub's search counts a pull request as an
issue, so every listing and search carries `is:issue` — without it a
change request would arrive as an issue.

Bodies and titles travel over stdin, never argv.
Both hosts reach less than the other two, and say so. Bitbucket has no
labels, no candidate lists and reports no change request against an
issue; a repository with the tracker switched off answers 404, which
becomes that repository's own error rather than the host's.

Azure DevOps has work items instead of issues: `az boards` reaches a
query, one item and a field write, so listing, reading, closing and
reopening are offered and nothing else is. Its query runs at the
organization and never forwards a project, so the project is resolved
from the checkout and named in the WIQL — `@project` would answer for
every project in the organization.
One handler per issue method on the WebSocket group, sharing a single
server-lifetime service so every client reads the same caches and one
client's mutation invalidates them for all of them.

Reads take the read scope and writes the operate scope, with refreshing
counted as reading: a read-only client pressing refresh must not be told
it may not look again.

The environment advertises `issues`, so a client never probes a server
that predates this.
Reads shell out to a host's CLI, so they are held briefly and refreshed
explicitly; writes run serially per environment, because two CLI calls
against one issue are order-sensitive.

The right panel gains an `issue` surface carrying its reference in its
id, so several issues stay open as peer tabs. The issues page's own
shared panel is not persisted, for the same reason the pull requests
page's is not: a restart should open the list, not last session's tabs.
The row, the filters, the ghosts and the empty states, plus the pure
logic the page runs on: involvement grouping, local narrowing while a
host is still answering, relevance ranking and the bounded snapshot a
reload starts from instead of skeletons.

A label wears the colour its host chose, with the ink picked by contrast
rather than fixed — half of GitHub's own palette is pale enough that
white disappears on it.
Summary and timeline beside each other, with close, reopen, comment,
rename, body, labels and assignees offered only where the host and the
viewer's own access agree — nothing renders a control that would fail.

Handing an issue to an agent is the point of the panel. Solve, Ask,
Explain and Add to composer write into a thread's composer draft, into
the thread the panel is open beside where there is one and into a new
one otherwise, and only ever replace their own prior contribution.
Everything the issue carries is framed as untrusted data: a body on a
public tracker is written by strangers.
One page listing issues across every project in the environment, with
the filters in the URL, search asked of the hosts themselves, infinite
scroll from the cursors they hand back, and the detail beside the list
as panel tabs.

An issue link an agent wrote opens in that panel rather than in a
browser, matched to a project by host and repository so a lookalike
domain cannot resolve to one.
An issue opens as a right-panel tab in the chat view, with its live
state on the tab. Solving, asking or attaching from there writes into
that thread's own composer instead of starting a new thread — reading
an issue and acting on it stay one conversation.
A pull request now lists the issues it closes and the ones it only
cites, and an issue lists the change requests against it. Pressing
either opens the other beside it.

GitHub reports both directions; GitLab reports the closing links only,
and the other two report none, so their sections are absent rather than
empty — an empty one would claim a change closes nothing, which a host
with no notion of the link cannot know.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9fb5d1a6-f1a1-43a2-a2bb-46b4d17a5814

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Aug 12, 2026
Comment thread apps/server/src/issue/AzureDevOpsIssueCli.ts
Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/components/issue/IssueAssigneePicker.tsx Outdated
Comment thread apps/server/src/issue/GitLabIssueCli.ts
Comment thread apps/web/src/lib/openIssueLink.ts Outdated
Comment thread apps/server/src/issue/AzureDevOpsIssueCli.ts Outdated
Comment thread apps/web/src/components/issue/IssueCreateDialog.tsx Outdated
Comment thread apps/server/src/pullRequest/gitHubPullRequestJson.ts
Comment thread apps/web/src/components/issue/issueList.logic.ts Outdated
Comment thread apps/web/src/components/issue/IssueDetailPanel.tsx Outdated

@macroscopeapp macroscopeapp Bot left a comment

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.

Reviewed the new Effect service modules under apps/server/src/issue/ (CLI/API wrappers, providers, registry, IssueService), the touched pull-request services, and the contracts/client-runtime additions against the Effect service conventions.

Service shape, layer composition, dependency acquisition (yield* Foo.Foo), namespace imports, Foo["Service"] references, and the runtime boundaries in server.ts/ws.ts all follow the conventions. Two error-modelling findings in the new Azure DevOps issue modules are noted inline.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/issue/AzureDevOpsIssueCli.ts Outdated
Comment thread apps/server/src/issue/AzureDevOpsIssueProvider.ts Outdated
A label was painted in the host's colour at full strength with black or
white ink over it, which reads as a solid block the row has to fight.

It now wears a wash of that colour with an edge a shade stronger and
the name in the colour pulled towards the page's own ink, mixed in CSS
so one set of numbers is right in both themes.
Every event row carried a filled avatar disc, and a marker that size
masks the rail behind it — so the line read as short dashes between
blobs rather than as one thread, which is not how the same rail reads
on a pull request.

Events now wear the issue glyph the way a pull request's lifecycle rows
wear theirs, and a face is left to say what it says there: that a run
of comments has people in it.
Pressing a related pull request on the issues page navigated the whole
page to the pull requests list, and pressing a linked issue did the
same in reverse. Following a link between the two threw away everything
the reader had open to show them one row.

Both pages now open the other kind in their own right panel, beside
what is already there, the way a second issue or a second pull request
already opens. The URL keeps naming only the kind that page reads back,
so a peer tab cannot be reopened as the wrong thing.
A host reports a link only where somebody used a closing keyword or
cross-referenced the issue, so a pull request whose body says "part of
pingdotgg#12" showed no linked issue at all — which is what our own pingdotgg#6315 does.

The title and body are now read for references too, outside code spans
and fences, and each one is resolved against the host before it is
shown: a number in a body is not proof an issue exists. What resolves
is listed as cited, never as closing — only the host can say what
merging will close — and the host's own links always win.

Bounded at ten, and a failed resolve leaves the host's own links
standing rather than failing the read.
Pressing New issue dropped straight into an empty box, which is not
what filing one on a host is like: a repository asks for particular
things, and says so through its templates.

The dialog now offers what this repository offers — its templates with
their descriptions, the contact links it configured, and a blank issue
last where it allows one — and opens the form already filled in from
whichever was chosen. A repository with nothing to offer goes straight
to the blank form, as it does on the host.

Read from the host rather than from the checkout, since a branch's
templates are not the ones a reader is filing against. GitHub and
GitLab report them; Bitbucket and Azure DevOps have none to report and
declare so.
Comment thread apps/web/src/components/issue/IssueCreateDialog.tsx
Comment thread apps/server/src/issue/GitLabIssueCli.ts
Comment thread apps/server/src/issue/GitLabIssueCli.ts
Comment thread apps/web/src/routes/_chat.issues.tsx
Comment thread apps/web/src/routes/_chat.issues.tsx
Comment thread apps/server/src/issue/GitHubIssueCli.ts
The panel already held issue tabs, but the surface chooser offered no
way to open one — an issue could only arrive from a link somebody else
had written.

There is now an Issue card beside Pull request, opening a small picker
of that project's issues with the search the list page uses. What it
opens is the same surface kind as before, so solving, asking or
attaching from it still writes into the thread it sits beside rather
than starting a new one.
A change request with no linked issue said only that it mentions none,
and an issue with no change request said the same — both true, and
neither any use to somebody who knows the link exists and has not been
written down.

Both sections now offer to hand the question over: read the change,
read the open issues, and record what actually matches the way the host
records it, by editing a description to carry `Closes pingdotgg#12`. The task
says to link nothing it is unsure of and that an empty answer is a
valid one, and everything quoted from the host travels as untrusted
data.
A repository that uses issue forms asks particular questions — a
dropdown for the area, boxes to tick before submitting, one box per
part of a report — and we showed one empty body box instead, which is
nothing like filing the issue on the host.

The templates are now read as what they are. A form's fields arrive
typed and render as the controls they describe, with their own
descriptions, placeholders and required marks, and the body sent is
exactly the markdown the host itself would have assembled — headings,
`_No response_` for an empty optional, fenced blocks where a field
declares a language, and every box listed whether ticked or not.

Left out deliberately: the markdown toolbar, since this repo has no
toolbar primitive to reuse and hand-rolling one is a worse trade than
Write and Preview tabs.
Comment thread packages/contracts/src/issue.ts
Comment thread apps/server/src/issue/gitHubIssueJson.ts
Comment thread apps/web/src/components/issue/IssueCreateDialog.tsx
Comment thread apps/web/src/components/issue/IssueCreateDialog.tsx Outdated
Comment thread apps/web/src/components/issue/IssueDetailPanel.tsx Outdated
The Issue surface opened a dialog over the app and put whatever was
picked in a tab of its own, which is neither a panel nor one place to
read from.

The panel is now the browser: the project's issues with their search,
and pressing one turns that same tab into the issue, with a way back to
the list. Its tab says which of the two it is showing. An issue opened
from a link or from a change request still arrives as its own tab, and
hand-offs still land in the thread the panel sits beside.
Comment thread apps/web/src/components/issue/IssuesPanel.tsx
Comment thread apps/web/src/components/issue/IssuesPanel.tsx
Comment thread apps/web/src/components/RightPanelTabs.tsx
Bil0000 added 27 commits August 17, 2026 18:36
# Conflicts:
#	apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
#	apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx
#	apps/web/src/components/pullRequest/PullRequestTimelineTab.tsx
#	apps/web/src/components/pullRequest/pullRequestPresentation.tsx
#	apps/web/src/components/settings/settingsSearch.ts
#	apps/web/src/routeTree.gen.ts
variant="outline"
className="w-full"
disabled={loadingMoreComments}
onClick={onLoadMoreComments}

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.

🟡 Medium issue/IssueSummaryTab.tsx:449

Clicking Load older comments fetches comments that are prepended to detail.comments but remain outside the fixed recentComments slice, so the UI appears unchanged until the user separately clicks Show earlier comments. Expand shown when requesting an older page so the newly loaded comments are rendered immediately.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/issue/IssueSummaryTab.tsx around line 449:

Clicking `Load older comments` fetches comments that are prepended to `detail.comments` but remain outside the fixed `recentComments` slice, so the UI appears unchanged until the user separately clicks `Show earlier comments`. Expand `shown` when requesting an older page so the newly loaded comments are rendered immediately.

onRefresh: () => void;
}) {
// Keyed by the pull request, so opening another one starts at the end of its conversation
// rather than wherever the last one had been read back to.
const [shown, setShown] = useState({ url: detail.url, count: COMMENT_PAGE });
const aiMatches = useWorkItemMatches({

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.

🟡 Medium pullRequest/PullRequestSummaryTab.tsx:342

Switching between environments or providers with the same project, repository, PR number, and updatedAt keeps displaying the previous host's related and duplicate matches, so users can open or hand off the wrong issue. useWorkItemMatches receives environmentId and detail.provider here, but its cache identity excludes them; include both values in that identity or remount/reset the hook when they change.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx around line 342:

Switching between environments or providers with the same project, repository, PR number, and `updatedAt` keeps displaying the previous host's related and duplicate matches, so users can open or hand off the wrong issue. `useWorkItemMatches` receives `environmentId` and `detail.provider` here, but its cache identity excludes them; include both values in that identity or remount/reset the hook when they change.

[issueEnvironmentId, selectEntry, selectingWorkItems, toggleWorkItem],
);

const [creating, setCreating] = useState(false);

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.

🟠 High routes/_chat.issues.tsx:1115

Users cannot open IssueCreateDialog from the Issues workspace, so the issue-creation flow is inaccessible. creating starts as false and is only passed through onOpenChange={setCreating}; no control ever calls setCreating(true). Add a create action wired to setCreating(true).

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/routes/_chat.issues.tsx around line 1115:

Users cannot open `IssueCreateDialog` from the Issues workspace, so the issue-creation flow is inaccessible. `creating` starts as `false` and is only passed through `onOpenChange={setCreating}`; no control ever calls `setCreating(true)`. Add a create action wired to `setCreating(true)`.

const scope =
target.environmentId === undefined ? "" : `${encodeURIComponent(target.environmentId)}:`;
const provider = target.provider === undefined ? "" : `${encodeURIComponent(target.provider)}:`;
return `issue:${scope}${provider}${encodeURIComponent(target.projectId)}:${encodeURIComponent(target.repository)}:${target.number}`;

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.

🟡 Medium src/rightPanelStore.ts:259

issueSurfaceId returns the same ID, issue:github:p:r:1, for targets where environmentId is "github" and where provider is "github". Because upsertSurface deduplicates by id, opening the second target reuses the first tab and displays the wrong issue/server. Encode the optional fields with distinct markers or otherwise preserve their positions so these targets cannot collide.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/rightPanelStore.ts around line 259:

`issueSurfaceId` returns the same ID, `issue:github:p:r:1`, for targets where `environmentId` is `"github"` and where `provider` is `"github"`. Because `upsertSurface` deduplicates by `id`, opening the second target reuses the first tab and displays the wrong issue/server. Encode the optional fields with distinct markers or otherwise preserve their positions so these targets cannot collide.

@@ -584,10 +584,10 @@ const make = Effect.gen(function* () {
applyServerSettingsPatch(current, patch),
);
const next = yield* normalizeServerSettings(nextPersisted);
const materialized = yield* materializeProviderEnvironmentSecrets(next);

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.

🟠 High src/serverSettings.ts:587

A failed updateSettings can still change provider credentials: persistProviderEnvironmentSecrets mutates the secret store before materializeProviderEnvironmentSecrets(next) runs, so a secretStore.get failure exits without committing next while the old cached settings subsequently read the newly written secret. Materialize before persisting secrets, or make persistence and commit transactional with rollback.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/serverSettings.ts around line 587:

A failed `updateSettings` can still change provider credentials: `persistProviderEnvironmentSecrets` mutates the secret store before `materializeProviderEnvironmentSecrets(next)` runs, so a `secretStore.get` failure exits without committing `next` while the old cached settings subsequently read the newly written secret. Materialize before persisting secrets, or make persistence and commit transactional with rollback.

@macroscopeapp macroscopeapp Bot left a comment

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.

Reviewed the new Effect services in apps/server/src/issue and their call sites against the service conventions. Earlier findings (the GraphQL budget module global, the Azure detail/manufactured-cause cases, the rate-limit pass-through) are resolved at this head. Three items remain, all in the new Linear code.

Posted via Macroscope — Effect Service Conventions

),
);
yield* syncLegacyBindings(connection).pipe(
Effect.catchTag("LinearApiError", () => Effect.void),

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.

Effect.catchTag is not used in this codebase's conventions — statically known tagged failures should be recovered with Effect.catchTags, including when only one tag is handled.

Suggested change
Effect.catchTag("LinearApiError", () => Effect.void),
Effect.catchTags({ LinearApiError: () => Effect.void }),

Posted via Macroscope — Effect Service Conventions

Comment on lines +183 to +187
export class LinearApiError extends Data.TaggedError("LinearApiError")<{
readonly reason: "unauthenticated" | "failed";
readonly detail: string;
readonly cause?: unknown;
}> {}

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.

This service failure is a Data.TaggedError whose only payload is a free-text detail, with no message derived from structural attributes — every sibling client added in this PR (BitbucketIssueReadError, AzureDevOpsIssueReadError, GitLabIssueCli's errors) uses Schema.TaggedErrorClass with structural fields and a derived message/detail.

Consider modelling it the same way: Schema.TaggedErrorClass<LinearApiError>()("LinearApiError", { operation: Schema.String, reason: Schema.Literals(["unauthenticated", "failed"]), cause: Schema.optional(Schema.Defect()) }) with get detail()/override get message() computed from operation and reason, so the construction sites pass context rather than a prose sentence.

Posted via Macroscope — Effect Service Conventions

Comment on lines +383 to +386
new LinearApiError({
reason: isAuthError(message) ? "unauthenticated" : "failed",
detail: message,
}),

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.

detail is set to the raw first errors[].message from Linear's GraphQL body, and that string travels on unchanged into IssueProviderError.detail (LinearIssueProvider.fail) and then into IssueOperationError.message, so arbitrary host wire text becomes the caller-visible message. Consider deriving detail from stable attributes and keeping the exact response value only as cause.

Suggested change
new LinearApiError({
reason: isAuthError(message) ? "unauthenticated" : "failed",
detail: message,
}),
new LinearApiError({
reason: isAuthError(message) ? "unauthenticated" : "failed",
detail: `Linear ${operation} was refused.`,
cause: errors,
}),

Posted via Macroscope — Effect Service Conventions

@macroscopeapp macroscopeapp Bot left a comment

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.

Three duplicated-ownership findings in the new shared source-control layer. The five items from the previous run are all addressed (topbar-scroll-fade now matches the pull requests page, ListSearchInput uses InputGroup, EntityPicker uses Input size="compact", the issue header trigger renders Button, and IssueListFilters uses LIST_MENU_TRIGGER_CLASS_NAME).

The remaining findings are the same class of problem: this PR adds a shared component for a treatment, but leaves the pull request original in place, so one treatment now has two owners that will drift.

Posted via Macroscope — UI Consistency

export function SummarySection({

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.

SummarySection reproduces PullRequestSummaryTab's local Section heading (border-t border-border/60 pr-4, the trigger's px-4 py-3 text-sm font-medium, the rotating chevron, the count span, actions only while open), but that Section is retained, so the same section chrome has two owners.

It is also not equivalent: the pull request version pins the heading (sticky top-0 z-10 … bg-background) and anchors scrollTop on collapse via sectionCollapseAnchorScrollTop, so on the issue panel a section collapses out from under the reader and its heading scrolls away. Suggest giving SummarySection the sticky heading plus an optional collapse-anchor hook (a ref/onOpenChange the caller can use) and rendering it from PullRequestSummaryTab, so both panels get the same behaviour from one definition.

Posted via Macroscope — UI Consistency

Comment on lines +1607 to +1612
/**
* The search, folded to an icon until asked for. Opening moves focus into the input — the
* whole point of pressing it is to type. It stays open while it holds a query, so an active
* search is never invisible; empty and blurred, it folds back.
*/
function ExpandableSearch({

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.

ExpandableSearch is a byte-for-byte copy of the one in _chat.pull-requests.tsx apart from the aria-label, and it is behavioural rather than visual: focus-on-open, the focusToken focus/select path, the blur-folds-when-empty rule and the onFocusWithin handover. CompactFilterMenu above (the trigger's inline-flex h-7 … rounded-md px-1.5 … and the min-w-40 popup) and the Mod+F keydown handler are copied from there too.

Since this PR already centralised the list chrome under components/sourceControl/, suggest moving these there as well (e.g. ListTitlebarSearch plus the compact trigger treatment) and keeping only the labels and the provider-specific menu action at each call site — otherwise the two pages' condensed titlebars keyboard-behave differently the first time either copy is touched.

Posted via Macroscope — UI Consistency

Comment on lines +2 to +4
* One row of an issue or pull request list. An issue and a change request are read the same way —
* a glyph, a title, a line of facts under it, and what happened last on the right — so the frame is
* written once here and each list fills the slots with what its own entries carry.

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.

This says the row frame is "written once here", but PullRequestRow still carries its own copy of it — same grid w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 rounded-lg px-3 py-2, same focus-visible:ring-1 focus-visible:ring-ring, same [contain-intrinsic-block-size:54px] [content-visibility:auto], same selected ? "bg-accent" : "hover:bg-accent/60", same title span and trailing column. So the two lists' row geometry, focus ring and hit area now have two independent definitions, and they have already diverged: this meta line adds overflow-hidden and the pull request one does not.

Suggest migrating PullRequestRow onto ListRow, passing its review verdict, checks popover and environment label through meta and its diff stat through trailing; the only thing missing is a slot for the context menu on #number, which is cheaper to add here than to keep a second frame.

Posted via Macroscope — UI Consistency

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant