feat: add pluggable issue adapters and workspace - #6315
Conversation
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.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
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
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.
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.
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.
There was a problem hiding this comment.
Two shared-primitive ownership findings in the new code. The earlier duplication findings on this PR (DetailTabStrip, ListRow, SummarySection, list search/filter chrome) are all resolved — these are the two remaining places where new code either re-declares an existing owner or rebuilds a core primitive by hand.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One finding on the new work-item RPC handlers: both new error types wrap a whole multi-step pipeline in a single generic failure with no structural context.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
Reviewed the issues workspace and the shared components/sourceControl extraction. The earlier duplication findings (the timeline conversation group, the segmented toggle in the selection bar, the detail tab strip, the list row frame, the summary section, the list filter/search chrome, the workspace header/container on the issues route) are all resolved — those now render the shared primitives.
Two remaining items, both about the new multi-select feature.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One convention issue in the new WebSocket handlers; everything else in the new issue services (tags, make/layer shape, namespace imports, structured errors, budget-as-service) reads clean.
Posted via Macroscope — Effect Service Conventions
There was a problem hiding this comment.
Two consistency findings on the shared work-item selection feature. The earlier findings (row selection overloading ListRow's selected/aria-current, and the AI-suggestion hover token) are resolved.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Native title tooltips slipped into three new surfaces. The repo standardised on the styled Tooltip (t3code/no-native-title-tooltip, added in #7209), but that rule only inspects intrinsic elements, so title passed to Button, MenuRadioItem, MenuItem and ToggleGroup evades it and still renders a browser tooltip — with different delay, styling and no dark-theme treatment than the styled popups sitting next to it.
Everything else in the shared list/detail chrome looks consistent with the primitives.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One new finding on the latest commit; the three native-tooltip findings from the previous review are still open (see the existing inline comments on IssueListFilters.tsx, IssueDetailPanel.tsx and WorkItemSelectionBar.tsx).
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
Two consistency findings in the new issue UI; everything else in the shared sourceControl/* extraction (list rows, filter menus, tab strips, summary sections, timeline rail, pickers, tooltips) looks aligned with the pull request surface.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One finding: the issue surfaces render the shared HostMarkdown without an environmentId, so file links in an issue body lose the open/reveal actions the identical renderer keeps on a pull request in the same right panel.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One consistency finding on the issue browser panel's failure state. Everything flagged in earlier runs — the native title tooltips, the hand-rolled tab strip and segmented group, the raw search/picker inputs, the aria-current selection overload, the missing environmentId on HostMarkdown, and the duplicated header/container/row/section chrome — reads as addressed at this head.
Posted via Macroscope — UI Consistency
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
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:
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
API and rate-limit safety
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
3e449340f.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 and workspace issue browsing UI
IssueServicewith per-provider adapters for GitHub, GitLab, Bitbucket, Azure DevOps, and Linear, each implementingIssueAdapterwith provider-specific capabilities, viewer permissions, caching, and error normalization/issuesroute with list/filter/search/snapshot,IssueDetailPanelwith summary and timeline tabs,LinearConnectionDialog,IssueCreateDialog, label/assignee pickers, reactions, and work-item multi-selection with AI task generation viaWorkItemSelectionBar"n- Extracts shared source-control components (ListRow,ListEmptyState,ConversationGroup,TimelineComment,HostMarkdown,CommentComposer,EntityPicker,ListFilterMenu,SourceControlReactionBar) from pull-request-specific implementations and refactors PR panels to use themServerSettingsnow includes anissueTrackingsection andPullRequestDetailcarrieslinkedIssues; out-of-tree consumers expecting the prior shapes will need updating.RIGHT_PANEL_STORAGE_VERSIONbumps to 13 with migration that drops legacy:issues-panelstate.ServerSecretStore.makeremoves an extrachmod(0600)call.writeFileStringAtomicallyno longer requires aScope.Macroscope summarized e153385.