feat: multi-provider issues workspace, with in-thread hand-off - #6315
feat: multi-provider issues workspace, with in-thread hand-off#6315Bil0000 wants to merge 114 commits into
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:
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 |
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.
| @@ -885,6 +878,19 @@ export const PullRequestSubmitReviewInput = Schema.Struct({ | |||
| }); | |||
| export type PullRequestSubmitReviewInput = typeof PullRequestSubmitReviewInput.Type; | |||
|
|
|||
| export const PullRequestThreadCommentsInput = Schema.Struct({ | |||
| ...PullRequestRef.fields, | |||
| threadId: TrimmedNonEmptyString, | |||
There was a problem hiding this comment.
🔴 Critical src/pullRequest.ts:883
PullRequestThreadCommentsInput allows a threadId from another repository or pull request to be used with any supplied PullRequestRef, so the service can return private comments while presenting them as belonging to the requested PR. getReviewThreadComments resolves the opaque GitHub node ID globally and does not use number or repository; bind the thread to the requested PR before returning its comments, or resolve it through that PR's repository hierarchy.
Also found in 1 other location(s)
apps/server/src/pullRequest/PullRequestService.ts:1138
threadCommentsvalidates only the client-supplied project/repository, then forwards the opaquethreadIddirectly togetReviewThreadComments; the GitHub implementation resolves that ID through a global GraphQLnodequery and does not usenumberorrepository. Supplying a thread ID from another pull request therefore returns that other thread's comments while the response is presented as belonging to the requested PR (and can bypass the service's workspace/PR scoping). Bind the thread to the requested PR before returning its comments, or query it through the repository/PR hierarchy.
🤖 Copy this AI Prompt to have your agent fix this:
In file @packages/contracts/src/pullRequest.ts around line 883:
`PullRequestThreadCommentsInput` allows a `threadId` from another repository or pull request to be used with any supplied `PullRequestRef`, so the service can return private comments while presenting them as belonging to the requested PR. `getReviewThreadComments` resolves the opaque GitHub node ID globally and does not use `number` or `repository`; bind the thread to the requested PR before returning its comments, or resolve it through that PR's repository hierarchy.
Also found in 1 other location(s):
- apps/server/src/pullRequest/PullRequestService.ts:1138 -- `threadComments` validates only the client-supplied project/repository, then forwards the opaque `threadId` directly to `getReviewThreadComments`; the GitHub implementation resolves that ID through a global GraphQL `node` query and does not use `number` or `repository`. Supplying a thread ID from another pull request therefore returns that other thread's comments while the response is presented as belonging to the requested PR (and can bypass the service's workspace/PR scoping). Bind the thread to the requested PR before returning its comments, or query it through the repository/PR hierarchy.
| }): Effect.Effect<A, GitHubPullRequestCliError> => | ||
| github | ||
| }): Effect.Effect<A, GitHubPullRequestCliError> => { | ||
| const decision = githubGraphQlBudget.query(input.host, input.query); |
There was a problem hiding this comment.
🟠 High pullRequest/GitHubPullRequestCli.ts:954
listLinkedIssues and listCitedIssues fail with an invalid GraphQL document instead of returning links. githubGraphQlBudget.query inserts rateLimit before the final }, but these documents end with the LinkedIssue fragment, so the field is added to Issue rather than the root operation; inject the budget selection into the operation's selection set instead.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/pullRequest/GitHubPullRequestCli.ts around line 954:
`listLinkedIssues` and `listCitedIssues` fail with an invalid GraphQL document instead of returning links. `githubGraphQlBudget.query` inserts `rateLimit` before the final `}`, but these documents end with the `LinkedIssue` fragment, so the field is added to `Issue` rather than the root operation; inject the budget selection into the operation's selection set instead.
Adds an Issues workspace: browse, read, act on and hand off issues from every host a repository lives on, without leaving T3 Code.
Companion to the pull requests page (#4849) and built on the same bones — one provider interface, capabilities declared per host, nothing offered that would fail when pressed.
Where it shows up
A full page (sidebar → Issues). Every project in the environment, filtered by state × involvement × project × host × label, searched on the hosts themselves, with the detail beside the list as panel tabs.
A right-panel surface (
Iin the surface chooser). The panel is the browser: the project's issues with their own search and filters, and pressing one turns that same tab into the issue with a way back. Hand-offs from there land in the thread the panel sits beside — reading an issue and acting on it stay one conversation.Beside a pull request. An issue opened from a link or from a change request arrives as its own peer tab.
What you can do
Closes #12.What each host allows
Azure DevOps has work items rather than issues and
az boardsreaches a query, one item and a field write — so it lists, reads, closes and reopens, and declares the rest missing rather than half-implementing it. Anything a host cannot do is not shown, not shown-and-broken.Design
One interface per host.
IssueProviderApidefines the operations; the service knows only the registry. Adding a host is one file.Capabilities are declared, and the server enforces them independently of the UI — an undeclared operation is refused before a host is called, and again against what this viewer may do.
A tracker being off is one repository's problem. Bitbucket switches issues off per repository and answers 404; that becomes an error on that row while every healthy repository still returns issues.
Hosts, not provider kinds. github.com and a GitHub Enterprise install are the same kind and different accounts, so the viewer, de-duplication and every row key are scoped by host.
Degrade, never blank. A malformed row is skipped rather than failing its batch, and one unreachable repository becomes an error entry while the rest render.
Also in here
apps/web/src/components/sourceControl/— 25 files of UI the two features genuinely shared, extracted from both sides: ghosts, empty and unavailable states, the row shell, the filter menu, the entity picker, the timeline rail and comment, the summary layout, the tab strip and its mount hook. The pull-request version was the base wherever the two differed.PullRequestDetail.linkedIssuesis optional on the wire, so a client on an older server reads a pull request unchanged.Testing
Repo typecheck clean. ~1,550 tests across the issue service, the four providers and their decoders, the list and hand-off logic, the schemas, and the web components — including the exact argv and stdin each write produces on each host.
Not exercised end to end: no credentials for any of the four hosts were available while building. Reads and writes are unit-tested at the invocation level against response shapes taken from each host's documented API and from shapes this repo already decodes. This is the part worth a human pass, in this order: GitHub (fullest surface, issue forms), GitLab, Bitbucket (try a repository with the tracker switched off), Azure DevOps (close/reopen on a non-Agile process template).
Notes for reviewers
is:issueon every query);az boards queryruns at the organization and never forwards a project; a GitHub page must never end inside oneupdatedAtinstant or pagination stalls forever; GitLab's assignee candidates had to be seeded from the issue itself or a truncated member list would silently drop assignees.Macroscope - Effect Service Conventionsis red on stale findings — both are fixed at the commit it checked; evidence is in a comment below.Built with Claude Opus 5 in T3 Code.
Note
Add multi-provider issues workspace with in-panel issue/PR cross-linking and thread comment pagination
/issuesroute) supporting GitHub, GitLab, Bitbucket, and Azure DevOps, with list/detail/activity/mutations surfaced via new WebSocket RPCs and a sharedIssueService.nextCommentsCursorinstead of auto-fetching all pages, and clients can load more comments on demand via a newpullRequests.threadCommentsRPC.ListRow,ListEmptyState,EntityPicker,TimelineRail,SourceControlReactionBar,DetailTabStrip, etc.) from pull-request-specific components so both issues and PRs reuse the same building blocks.buildLinkIssuesHandoffthat triggers an agent workflow to link issues by editing a PR description, surfaced as a 'Linked issues' section in the PR summary tab.RIGHT_PANEL_STORAGE_VERSIONbumped from 11 to 13; previously persisted shared issues/pull-request panel states are dropped on upgrade.Macroscope summarized 3edb991.