-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(web-shell): derive session issue bindings from the closing references of bound PRs #10425
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wenshao
wants to merge
10
commits into
main
Choose a base branch
from
feat/webshell-session-issue-binding
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,752
−169
Open
Changes from 5 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
15f97a6
feat(web-shell): derive session issue bindings from the closing refer…
wenshao 8729d2b
Merge branch 'main' into feat/webshell-session-issue-binding
wenshao 350c3ac
fix(serve): converge foreign merged bindings and gate every snapshot …
wenshao e696fb8
Merge branch 'main' into feat/webshell-session-issue-binding
wenshao 75c5f88
fix(serve): retire unresolvable bindings from the issue sweep and rej…
wenshao 4e18594
fix(serve): reject partial GraphQL errors and match issue lookups by …
wenshao 94e5f92
Merge branch 'main' into feat/webshell-session-issue-binding
wenshao 4b1fc62
fix(core): treat a repository GitHub no longer serves as structurally…
wenshao fd1effb
Merge remote-tracking branch 'origin/main' into feat/webshell-session…
wenshao 29f6a70
fix(serve): keep GH_HOST mismatches transient and match every GitHub …
wenshao File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
84 changes: 84 additions & 0 deletions
84
.qwen/e2e-tests/2026-08-27-webshell-session-issue-binding.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| # E2E Test Plan: Session Issue Binding Derived From Bound PRs | ||
|
|
||
| ## Scope | ||
|
|
||
| The daemon's PR refresh sweep snapshots the issues each bound PR closes | ||
| (GitHub `closingIssuesReferences`, with state) into the existing `.pr.json` | ||
| sidecar; the Web Shell tooltip lists them and sidebar search matches their | ||
| numbers. No new write path, sidecar, or env switch. | ||
|
|
||
| ## Baseline dry-run | ||
|
|
||
| ```bash | ||
| qwen --version | ||
| ``` | ||
|
|
||
| With the released CLI, a session bound to a PR whose body says `Fixes #N` | ||
| shows only the PR in the session tooltip; searching `N` in the sidebar does | ||
| not find the session; the sidecar entry carries no `issues` field. | ||
|
|
||
| ## Group A: core contract | ||
|
|
||
| ```bash | ||
| cd packages/core | ||
| npx vitest run src/services/session-pr-service.test.ts src/utils/github-pr-issues.test.ts | ||
| ``` | ||
|
|
||
| Expected: an `issues` list round-trips; a non-http(s) issue url, an unknown | ||
| issue state, or more than 10 issues voids the sidecar; a same-PR re-bind | ||
| keeps the snapshot while a foreign-repo re-bind drops it; | ||
| `updateSessionPrStates` writes `issues` with or without a `state` and skips | ||
| the write when nothing changed. The GraphQL wrapper aliases one | ||
| `pullRequest(number:)` per PR, maps OPEN / CLOSED+COMPLETED / | ||
| CLOSED+NOT_PLANNED / CLOSED+DUPLICATE to open / completed / not_planned / | ||
| not_planned, keeps resolved aliases when gh exits non-zero over a NOT_FOUND | ||
| number, chunks at 100, and maps a missing binary to `cli_unavailable`. | ||
|
|
||
| ## Group B: daemon sweep and listing | ||
|
|
||
| ```bash | ||
| cd packages/cli | ||
| npx vitest run src/serve/server/session-pr-refresh.test.ts | ||
| npx vitest run src/serve/server.test.ts -t sidecar | ||
| cd ../acp-bridge && npx vitest run src/bridge.test.ts -t "SessionPrs|re-bind|client-supplied" | ||
| cd ../sdk-typescript && npx vitest run test/unit/sessionPr.test.ts | ||
| ``` | ||
|
|
||
| Expected: an open binding gets its state and issues in one write with | ||
| `createdAt` untouched; merged bindings without a snapshot get one by-number | ||
| lookup and no list query, and neither query once snapshotted; a foreign-repo | ||
| binding never receives this repository's issues; a failed issue lookup still | ||
| refreshes states; the retired-generation guard covers the new lookup. The | ||
| session list prefers sidecar `issues` over the live entry; the bridge echoes | ||
| seeded issues and keeps them across a state-only re-bind; the SDK guard | ||
| accepts the three issue states and rejects `javascript:` issue urls. | ||
|
|
||
| ## Group C: real gh against a real repository | ||
|
|
||
| From a checkout of a GitHub repository with `gh auth` configured, seed a | ||
| temporary runtime dir with one session bound to an open PR that references | ||
| an open issue, a merged PR that references a closed issue, and a foreign-repo | ||
| PR number, then call `refreshWorkspaceSessionPrStates` from the built cli | ||
| dist twice. | ||
|
|
||
| Expected: round one reports `updated: 2` and the sidecar carries | ||
| `state: "open"` / `state: "completed"` issues on the two real PRs while the | ||
| foreign entry is untouched; round two reports `updated: 0`; each round | ||
| finishes well inside the 10s gh timeout. | ||
|
|
||
| ## Group D: Web Shell | ||
|
|
||
| ```bash | ||
| cd packages/web-shell | ||
| npx vitest run client/components/sidebar/SessionDetailsTooltip.test.tsx client/components/sidebar/sessionSearch.test.ts | ||
| ``` | ||
|
|
||
| Manual: run `qwen serve`, open the Web Shell, create a PR from the Git dialog | ||
| with `Fixes #N` in the body, wait for the first sweep (60s after daemon | ||
| start, then every 5 minutes), hover the session row. | ||
|
|
||
| Expected: the tooltip lists `Issue #N` under the PR rows with a green | ||
| circle-dot icon (purple check once the issue is closed as completed, muted | ||
| slash for not planned), the link opens the issue, a stacked PR closing the | ||
| same issue lists it once, and typing `N` or `#N` in the sidebar search finds | ||
| the session. The session-row badge still shows only the PR. |
106 changes: 106 additions & 0 deletions
106
docs/design/2026-08-27-webshell-session-issue-binding.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| # Web Shell 会话绑定 GitHub Issue 号(由 PR 派生) | ||
|
|
||
| 日期:2026-08-27 | ||
| 状态:已确认范围(路线 A) | ||
|
|
||
| ## 问题 | ||
|
|
||
| [会话绑定 PR 号](2026-08-20-webshell-session-pr-binding.md)落地后,侧栏能回答"哪个会话产出了 PR #N",但回答不了"哪个会话在处理 issue #N"。维护者的主力流程(bugfix / triage / autofix develop-issue)都从 issue 出发,最终以 PR 收口。 | ||
|
|
||
| Issue 和 PR 的语义不同:PR 是会话的**产出**,会话自己在 GitDialog 里点了 Create PR,daemon 有一个精确的写入点;Issue 是会话的**意图**,web-shell 里没有任何结构化写入点(`/bugfix #N` 是提示词参数,`create-issue` skill 是模型自己跑 `gh issue create`),而 PR 与 Issue 共用编号空间,扫描提示词或分支名会误绑。 | ||
|
|
||
| ## 方案:从已绑 PR 派生 | ||
|
|
||
| GitHub 上 PR 与 Issue 的关联已经存在——PR 正文里的 `Fixes #N` 就是 `closingIssuesReferences`。不新增写入点,也不新增 sidecar;daemon 现有的 PR 状态刷新定时器顺手把每个已绑 PR 关闭的 issue 及其状态快照进同一个 sidecar 条目。 | ||
|
|
||
| ### 数据模型 | ||
|
|
||
| `SessionPr` 条目增加可选字段: | ||
|
|
||
| ```json | ||
| { | ||
| "number": 10303, | ||
| "url": "https://github.com/QwenLM/qwen-code/pull/10303", | ||
| "createdAt": "...", | ||
| "state": "open", | ||
| "issues": [ | ||
| { | ||
| "number": 10293, | ||
| "url": "https://github.com/QwenLM/qwen-code/issues/10293", | ||
| "state": "open" | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| - `issues` 缺省表示"尚未抓取";空数组表示"抓过,没有"。二者区别决定 sweep 是否还需要为该 PR 发查询。 | ||
| - `state`:`open` / `completed` / `not_planned`(GitHub 的 `stateReason` NOT_PLANNED 与 DUPLICATE 都归 `not_planned`)。 | ||
| - 每个 PR 最多保留 10 个 issue(`SESSION_PR_ISSUE_LIST_LIMIT`,GraphQL 也只取 `first: 10`);url 与 PR url 同样只接受 http(s)、限长、无控制字符——core sidecar 校验、bridge 类型、SDK `isDaemonSessionPrInfo` 三层同步。 | ||
| - 派生数据跟着来源走:PR 条目被 cap 淘汰时 issue 一起走;同 PR 重绑(`upsertSessionPr` / bridge `updateSessionMetadata`)保留已有 `issues`,跨仓库同号 PR 不继承。客户端永远不能写 `issues`。 | ||
|
|
||
| ### 抓取 | ||
|
|
||
| 新增 core 工具 `fetchGitHubPullRequestIssues(cwd, env, numbers)`,一条 `gh api graphql`,用 gh 自带的 `{owner}` / `{repo}` 占位符解析仓库,按编号别名查询: | ||
|
|
||
| ```graphql | ||
| p10303: pullRequest(number: 10303) { | ||
| number url | ||
| closingIssuesReferences(first: 10) { nodes { number url state stateReason } } | ||
| } | ||
| ``` | ||
|
|
||
| 为什么不在 `gh pr list --json` 上追加 `closingIssuesReferences`: | ||
|
|
||
| 1. 该字段不带 issue state,仍需第二条查询; | ||
| 2. 实测 `--state all --limit 500` 加该字段从 4.9s 涨到 6.7s(gh 超时 10s),现有 sweep 的 list 查询保持原样零风险; | ||
| 3. 按编号查询不受 500 条窗口限制,老 PR 也能补齐。 | ||
|
|
||
| 每次调用最多 100 个别名,超出分批;任一批失败整体返回 `failed`。gh 对 NOT_FOUND 别名(绑定指向别的仓库的同号 PR)以非零退出码返回,但 stdout 仍带其它别名的完整数据——包装器在 stdout 有 JSON 时照常解析,未解析的别名直接缺席。 | ||
|
|
||
| ### Sweep 集成 | ||
|
|
||
| `refreshWorkspaceSessionPrStates` 每 workspace 每轮: | ||
|
|
||
| 1. 扫描 sidecar,挑出 `state !== 'merged' || issues === undefined` 的绑定(open/closed 的 closing references 会随正文编辑变化;merged 但无快照的是升级前的存量,只补一次)。 | ||
| 2. 若存在非 merged 绑定,跑原有 slim `gh pr list --state all` 刷 PR state(不变)。 | ||
| 3. 对第 1 步的编号去重后跑一次 GraphQL 拿 issues。 | ||
| 4. 每个 sidecar 一次 `updateSessionPrStates` 原地写入 state + issues:url 不匹配不写;`state` / `issues` 任一缺省则保留原值;都无变化则不写文件。 | ||
| 5. 收敛:merged 且无快照、又永远不可能被查询解析到的绑定写入空快照 `issues: []`(渲染上等同于没有),否则它会永远重新进入查询,违背"全 merged 且有快照零调用"的不变量。触发条件:查询成功但仓库不认识该编号(外仓同号 PR)、查询结构上不可能(无 gh / 无 git root)、平台根本没有 closing references(Aone)。瞬时失败(`failed`)不收敛。 | ||
| 6. 可解析性过滤:用列表查询返回的 PR url 推出本 workspace 仓库的 canonical 前缀,url 不在该仓库下的绑定不进入查询(与 Aone 的 refreshable 过滤对称),merged 的直接本地收敛;列表没跑或为空则放行进查询,由逐别名 NOT_FOUND 收敛。 | ||
|
|
||
| 成本:全 merged 且已有快照的 workspace 零调用;否则多一条 ~1–3s 的 GraphQL。已合入 PR 的 issue 之后被 reopen 不再跟踪(与"merged 是终态"同一取舍)。 | ||
|
|
||
| ### 线协议与展示 | ||
|
|
||
| - bridge `SessionPrInfo` / SDK `DaemonSessionPrInfo` 增加 `issues?`;所有 sidecar → 线协议投影统一走 core 新增的 `toSessionPrInfo`(原先散落在 session-list / session.ts / dispatch / backfill / bridge 共 8 处手写的 `{number, url, state?}`)。 | ||
| - `mergeSummaryPrs`:`issues` 与 `state` 一样以 sidecar 为准(live entry 停在绑定时刻),且与其它合并点一致只在 canonical url 相同时才采用——跨仓库重绑的 live 变更与 sidecar 写入之间,旧 sidecar 仍指向别的仓库的同号 PR。 | ||
| - 写入侧类型(bridge `SessionMetadataUpdate.pr`、SDK 三个 `updateSessionMetadata` 参数)用 `Omit<…, 'issues'>`,客户端绑 issue 是编译错误;运行时 bridge 也只从已知快照重建 `issues`。 | ||
| - web-shell: | ||
| - `SessionDetailsTooltip` 在 PR 行之后列出 issue(按 url 去重,stacked PR 关同一个 issue 只列一次),复用 GitHub 视觉词汇:open 绿 circle-dot、completed 紫 circle-check、not planned 灰 circle-slash;可见文本 `Issue #N`,sr-only 追加状态。 | ||
| - 侧栏搜索 `sessionMatchesGitQuery` 命中 issue 号(带不带 `#` 都行)。 | ||
| - 会话行 badge 保持只显示 PR,避免 `#N` 并排歧义。 | ||
| - 时延:GitDialog 创建 PR 后,issue 在下一轮 sweep(首轮 60s 延迟、之后 5 分钟)出现;`QWEN_SESSION_PR_REFRESH_MINUTES` 不变。 | ||
|
|
||
| ## 关键决策 | ||
|
|
||
| - **派生而非绑定**:唯一高精度的来源是 GitHub 自己的 closing references;显式输入绑定(`/bugfix #N` 拦截、欢迎页入口)留作后续路线,届时再考虑独立 sidecar。 | ||
| - **挂在 PR 条目上而非独立 sidecar**:派生数据的生命周期等于来源 PR,独立文件会引入第二套 cap、归档移动和写入 lane。 | ||
| - **保留 `gh pr list` 不动**:sweep 的 list 查询已经贴近超时上限,issue 单独走按编号 GraphQL。 | ||
|
|
||
| ## 影响文件 | ||
|
|
||
| | 层 | 文件 | | ||
| | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | core | `services/session-pr-service.ts`(`SessionPrIssue`、校验、`toSessionPrInfo`、`updateSessionPrStates` 扩展)、`utils/github-pr-issues.ts`(新增) | | ||
| | daemon | `serve/server/session-pr-refresh.ts`(sweep 第二阶段)、`session-list.ts`、`routes/session.ts`、`acp-http/dispatch.ts`、`routes/session-pr-backfill.ts`(投影) | | ||
| | bridge | `bridgeTypes.ts`、`bridge.ts`(类型、重绑保留、投影) | | ||
| | SDK | `daemon/types.ts`、`daemon/session-pr.ts`、`daemon/index.ts` | | ||
| | web-shell | `SessionPrStateIcon.tsx`(+css)、`sidebar/SessionDetailsTooltip.tsx`、`sidebar/sessionSearch.ts`、`i18n.tsx` | | ||
| | 测试 | 上述各层 collocated 单测 | | ||
|
|
||
| ## 范围边界(明确不做) | ||
|
|
||
| - 显式 issue 绑定入口与独立 issue sidecar。 | ||
| - 从提示词 / 分支名 / commit trailer 反推 issue 号。 | ||
| - 会话行 badge 显示 issue;回填路由立即抓 issue(下一轮 sweep 兜底)。 | ||
| - 已合入 PR 的 issue 被 reopen 后的状态跟踪。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.