Skip to content

feat: multi-provider issues workspace, with in-thread hand-off - #6315

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

feat: multi-provider issues workspace, with in-thread hand-off#6315
Bil0000 wants to merge 114 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: 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 (I in 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

  • Read — description and conversation as markdown, a timeline of comments and state changes on one rail.
  • Act — close (with a reason where the host records one), reopen, comment, rename, rewrite the body, labels, assignees.
  • File — the repository's own issue forms: its templates with their descriptions, its contact links, a blank issue where it allows one. Choosing one opens the real questions — checkbox groups, dropdowns, textareas with Write/Preview — and the body filed is byte-for-byte what the host itself would assemble.
  • Hand offSolve, Ask, Explain, Add to composer. From a thread they write into that thread's composer; from the page they start one in the issue's own project.
  • Cross-link — an issue lists the change requests against it, a pull request lists the issues it closes or cites, and either opens the other. Where the host reports no link, Link with agent hands the question over: read the change, read the open issues, record what actually matches by editing the description with Closes #12.

What each host allows

Close reason File Templates Rewrite Labels Assignees Search Linked PRs Timeline
GitHub ✅ forms
GitLab ✅ markdown closing only
Bitbucket
Azure DevOps

Azure DevOps has work items rather than issues and az boards reaches 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. IssueProviderApi defines 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.linkedIssues is optional on the wire, so a client on an older server reads a pull request unchanged.
  • Two bugs fixed on the pull-request side that were found while matching it: a nested scroll folding the panel chrome, and an uncaught clipboard rejection.

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

  • Untrusted input is treated as such: bodies and titles travel over stdin rather than argv, a client-supplied repository is checked against the project's own remote, and every issue's title, URL, body and comments are framed to the agent as untrusted data — an issue on a public tracker is written by strangers.
  • Host quirks that drove real decisions, all commented where they bite: GitHub's search counts pull requests as issues (is:issue on every query); az boards query runs at the organization and never forwards a project; a GitHub page must never end inside one updatedAt instant 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.
  • All 39 review threads are resolved. Two are worth reading rather than trusting: the Azure state-name walk, and why the launcher shortcut fix went into the shared guard.
  • Macroscope - Effect Service Conventions is 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

  • Introduces a full issues workspace (/issues route) supporting GitHub, GitLab, Bitbucket, and Azure DevOps, with list/detail/activity/mutations surfaced via new WebSocket RPCs and a shared IssueService.
  • Adds per-issue and issues-browser tabs to the right panel store, with in-thread hand-off (opening linked issues from a PR and vice versa) and fallback to browser if in-app navigation is unsupported.
  • Implements review thread comment pagination: the server now returns a nextCommentsCursor instead of auto-fetching all pages, and clients can load more comments on demand via a new pullRequests.threadComments RPC.
  • Extracts shared UI primitives (ListRow, ListEmptyState, EntityPicker, TimelineRail, SourceControlReactionBar, DetailTabStrip, etc.) from pull-request-specific components so both issues and PRs reuse the same building blocks.
  • Adds a buildLinkIssuesHandoff that triggers an agent workflow to link issues by editing a PR description, surfaced as a 'Linked issues' section in the PR summary tab.
  • Changes live-refresh cadence from 1 minute to 5 minutes to preserve API quota.
  • Risk: RIGHT_PANEL_STORAGE_VERSION bumped from 11 to 13; previously persisted shared issues/pull-request panel states are dropped on upgrade.

Macroscope summarized 3edb991.

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: bcc61db7-1789-4486-84c7-04b4a410b8a1

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
Comment thread apps/server/src/issue/GitHubIssueCli.ts Outdated
Comment thread apps/web/src/components/issue/IssueSummaryTab.tsx
Comment thread apps/web/src/components/issue/IssueTimelineTab.tsx Outdated
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. and removed vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Aug 13, 2026
@@ -885,6 +878,19 @@ export const PullRequestSubmitReviewInput = Schema.Struct({
});
export type PullRequestSubmitReviewInput = typeof PullRequestSubmitReviewInput.Type;

export const PullRequestThreadCommentsInput = Schema.Struct({
...PullRequestRef.fields,
threadId: TrimmedNonEmptyString,

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.

🔴 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

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.

🤖 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);

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 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.

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