Files
deepseek-harness/docs/subsystems/forge.zh.md
T
2026-10-29 11:47:17 +08:00

1182 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Forges
[English](forge.md) | 中文
实验性的 forge 能力 seam 从代码托管平台读取变更请求(change request,即 pull request 与 merge request)。[`dsh-experimental-forge`](../../packages/experimental/forge/README.zh.md) 是 Service Definition,提供 `ctx.forge`;[`dsh-experimental-forge-github`](../../packages/experimental/forge-github/README.zh.md) 是服务 GitHub 主机的 Service Provider;[`dsh-experimental-forge-session`](../../packages/experimental/forge-session/README.zh.md) 是把变更请求绑定到 Session 的 Consumer,提供 `ctx.forgeSession`;[`dsh-experimental-client-ui-forge`](../../packages/experimental/client-ui-forge/README.zh.md) 是提供 `ctx.forgePanel` 的 GUI Consumer。默认禁用的 [forge Bundle](../../packages/experimental/forge-bundle/README.zh.md) 可从插件页面组合这四个包,并为面板附带一个隔离的 [worktree 服务](../../packages/experimental/worktree/README.zh.md);除此之外,它们只能通过显式组合加载。
## 提供方与变更请求
一个提供方服务一个 forge 家族,例如 `github`。它把 Git 远端 URL 映射为仓库,把变更请求 URL 映射为引用,构造规范的变更请求 URL,按 head 分支查找打开状态的变更请求,并批量读取状态。`changeRequestKey` 从引用派生一个不区分大小写的键,因此 URL 拼写不同的监听方共享同一个轮询循环。
```ts type-equiv
/** One repository on a code host, identified the way its provider addresses it. */
interface ForgeRepository {
/** Provider family that serves the repository, such as `github`. */
readonly forge: string
/** Lowercase host name, such as `github.com`. */
readonly host: string
/** Owner or namespace path as the host spells it. */
readonly owner: string
/** Repository name as the host spells it. */
readonly name: string
}
```
```ts type-equiv
/** One change request (pull request or merge request) in a repository. */
interface ChangeRequestRef {
/** Repository that holds the change request. */
readonly repository: ForgeRepository
/** Host-assigned change request number. */
readonly number: number
}
```
```ts type-equiv
/** Canonical identity of a change request across providers; built by `changeRequestKey`. */
type ChangeRequestKey = Branded<'ChangeRequestKey'>
```
```ts type-equiv
/**
* Host-specific access registered with `ctx.forge`. One provider serves one
* forge family; methods that take a signal must stop work when it aborts.
*/
interface ForgeProvider {
/** Forge family name stored in every repository this provider returns. */
readonly forge: string
/** Publish a comment or completed review. Omission declares that this provider cannot write reviews. */
submitReview?(ref: ChangeRequestRef, request: ForgeReviewRequest, signal: AbortSignal): Promise<ForgeReviewResult>
/** Map a Git remote URL to its repository, or undefined when this provider does not serve it. */
repositoryFromRemote(url: string): ForgeRepository | undefined
/** Map a change request web URL to its reference, or undefined when this provider does not serve it. */
changeRequestFromUrl(url: string): ChangeRequestRef | undefined
/** Build the canonical web URL of a change request this provider serves. */
changeRequestUrl(ref: ChangeRequestRef): string
/** Read the status of several change requests, returning one result per requested key. */
fetch(
refs: readonly ChangeRequestRef[],
signal: AbortSignal,
): Promise<ReadonlyMap<ChangeRequestKey, ChangeRequestSnapshot | ForgeFailure>>
/** Read all checks for one change request on demand; omission declares that only the polling preview is available. */
fetchChecks?(ref: ChangeRequestRef, signal: AbortSignal): Promise<ChangeRequestSnapshot | ForgeFailure>
/** Find open change requests whose head is `branch` in `repository`, excluding the default branch and forks. */
findByHead(repository: ForgeRepository, branch: string, signal: AbortSignal): Promise<readonly ChangeRequestRef[]>
/** Report whether the provider can reach `host` with current credentials. */
readiness(host: string, signal: AbortSignal): Promise<ForgeReadiness>
/** Mark a draft change request ready for review, resolving to undefined once the host accepted it. */
markReady(ref: ChangeRequestRef, signal: AbortSignal): Promise<ForgeActionFailure | undefined>
/** Merge a change request while its head is still `request.headSha`, resolving to undefined once merged. */
merge(ref: ChangeRequestRef, request: MergeRequest, signal: AbortSignal): Promise<ForgeActionFailure | undefined>
/** Read the changes of a change request as the host computes them, as one unified patch with `a/` and `b/` path prefixes. */
diff(ref: ChangeRequestRef, signal: AbortSignal): Promise<string | ForgeFailure>
/** Read the text of `path` at `commit` in the change request's repository. */
fileText(ref: ChangeRequestRef, commit: string, path: string, signal: AbortSignal): Promise<string | ForgeFailure>
/** Build the web page of `path` at `commit` in the change request's repository. */
fileUrl(ref: ChangeRequestRef, commit: string, path: string): string
}
```
```ts type-equiv
/** Whether a provider can reach one host with the current credentials. */
interface ForgeReadiness {
/** `ready`, `unauthenticated` (sign-in needed), or `unavailable` (tool missing or host unreachable). */
readonly state: 'ready' | 'unauthenticated' | 'unavailable'
/** Human-readable detail, or null when ready. */
readonly message: string | null
}
```
## 状态轮询
`ctx.forge.watch` 让同一变更请求的所有监听方共享一个轮询循环,并把每次结果作为更新送达。每个 forge 同时最多只有一次读取在进行。轮询节奏、退避与超时规则见[注册表 README](../../packages/experimental/forge/README.zh.md#use-this-package)。
```ts type-equiv
/** Lifecycle state of a change request. */
type ChangeRequestState = 'open' | 'closed' | 'merged'
```
```ts type-equiv
/** Normalized outcome of one status check. */
type CheckState = 'pending' | 'success' | 'failure' | 'neutral' | 'skipped' | 'cancelled'
```
```ts type-equiv
/** One status check reported for the head commit of a change request. */
interface ChangeRequestCheck {
/** Check name as the host displays it. */
readonly name: string
/** Normalized check outcome. */
readonly state: CheckState
/** Web page for the check, or null when the host reports none. */
readonly url: string | null
/** Workflow that produced the check, or null when the host reports none. */
readonly workflow: string | null
/** ISO 8601 time the check started, or null before it starts and when the host reports none. */
readonly startedAt: string | null
/** ISO 8601 time the check completed, or null while it runs and when the host reports none. */
readonly completedAt: string | null
}
```
```ts type-equiv
/** Combined outcome of all checks on the head commit. */
type ChecksRollup = 'none' | 'pending' | 'success' | 'failure'
```
```ts type-equiv
/** Whether the change request can merge into its base without conflicts. */
type Mergeability = 'mergeable' | 'conflicting' | 'unknown'
```
```ts type-equiv
/** Aggregate review outcome required by the host's merge rules. */
type ReviewDecision = 'approved' | 'changes-requested' | 'review-required' | 'none'
```
```ts type-equiv
/** Status of one change request at the time its provider read it. */
interface ChangeRequestSnapshot {
/** Change request the snapshot describes. */
readonly ref: ChangeRequestRef
/** Web page of the change request. */
readonly url: string
/** Title as the author wrote it. */
readonly title: string
/** Lifecycle state. */
readonly state: ChangeRequestState
/** Whether the author marked the change request as a draft. */
readonly draft: boolean
/** Author login, or null when the host hides it. */
readonly author: string | null
/** Branch that carries the proposed commits. */
readonly headBranch: string
/** Commit object name at the head of the change request. */
readonly headSha: string
/** Branch the change request merges into. */
readonly baseBranch: string
/** Lines the change request adds. */
readonly additions: number
/** Lines the change request deletes. */
readonly deletions: number
/** Merge conflict status; `unknown` while the host computes it. */
readonly mergeability: Mergeability
/**
* Whether the host would merge the change request now for the signed-in
* user: open, not a draft, and meeting the base branch's merge rules.
*/
readonly mergeReady: boolean
/** Merge methods the repository accepts, the signed-in user's usual one first. */
readonly mergeMethods: readonly MergeMethod[]
/** Aggregate review outcome. */
readonly reviewDecision: ReviewDecision
/** Checks on the head commit in host order. */
readonly checks: readonly ChangeRequestCheck[]
/** Combined check outcome. */
readonly checksRollup: ChecksRollup
/** Whether the host reported more checks than `checks` contains. */
readonly checksTruncated: boolean
/** Number of review threads that are not resolved. */
readonly unresolvedThreads: number
/** ISO 8601 time of the host's last update to the change request. */
readonly updatedAt: string
/** ISO 8601 time the change request was merged or closed, or null while it is open. */
readonly closedAt: string | null
}
```
```ts type-equiv
/** Why a provider could not read a change request. */
interface ForgeFailure {
/**
* `not-found`: the host does not expose it; `unauthenticated`: credentials are missing or rejected;
* `too-large`: the host or the provider refuses an answer over its size limit, so reading again fails
* the same way; `unavailable`: any other failure.
*/
readonly code: 'not-found' | 'unauthenticated' | 'too-large' | 'unavailable'
/** Human-readable cause. */
readonly message: string
}
```
```ts type-equiv
/** Latest polling result delivered to watchers of one change request. */
type ChangeRequestUpdate = {
/** Canonical identity of the watched change request. */
readonly key: ChangeRequestKey
/** Epoch milliseconds when the result settled. */
readonly fetchedAt: number
} & ({
/** Status read from the host. */
readonly snapshot: ChangeRequestSnapshot
} | {
/** Reason the status could not be read. */
readonly failure: ForgeFailure
})
```
```ts type-equiv
/**
* Receives every polling result for one watched change request.
* @param update - the settled result.
*/
type ChangeRequestListener = (update: ChangeRequestUpdate) => void
```
## 变更请求操作
`ctx.forge.markReady` 把草稿标记为可评审,`ctx.forge.merge` 只在 head 仍是用户确认的提交时合并变更请求。主机拒绝时,两者都 resolve 为 `ForgeActionFailure` 而不是抛错,并在结束后立即轮询被监听的变更请求。快照的 `mergeReady` 与 `mergeMethods` 告诉消费方何时提供合并、可用哪些合并方式;GitHub 如何给出这两项由 [GitHub 提供方 README](../../packages/experimental/forge-github/README.zh.md#use-this-package) 说明。
```ts type-equiv
/** A merge the user confirmed for the head commit they saw. */
interface MergeRequest {
/** How the commits enter the base branch. */
readonly method: MergeMethod
/** Commit object name the head must still have; the merge is refused once the head moves. */
readonly headSha: string
}
```
```ts type-equiv
/** Why a provider could not change a change request. */
interface ForgeActionFailure {
/**
* `unauthenticated`: credentials are missing or rejected; `stale`: the head
* moved past the confirmed commit; `failed`: the host refused or could not be reached.
*/
readonly code: 'unauthenticated' | 'stale' | 'failed'
/** Human-readable cause, usually the host's own message. */
readonly message: string
}
```
`ctx.forge.submitReview` 将明确指定的评论或完整评审交给 provider;未实现此能力的 provider 返回不支持。消费者保存待提交草稿,并在再次提交前核对结果不明的写操作。
```ts type-equiv
/** One inline comment addressed to the old or new side of a pull request diff. */
interface ForgeLineComment {
readonly path: string
readonly side: 'L' | 'R'
readonly startLine: number
readonly endLine: number
readonly body: string
}
```
```ts type-equiv
/** A single published comment or one completed review containing all pending comments. */
type ForgeReviewRequest = {
/** Commit the user reviewed; providers must check it before writing and address that commit explicitly. */
readonly headSha: string
} & (
| { readonly kind: 'comment'; readonly comment: ForgeLineComment }
| { readonly kind: 'review'; readonly event: 'comment' | 'approve' | 'request-changes'; readonly body: string; readonly comments: readonly ForgeLineComment[] }
)
```
```ts type-equiv
/** Result of a review write; unknown outcomes must be checked on the host before another submission. */
type ForgeReviewResult =
| { readonly ok: true; readonly url: string }
| { readonly ok: false; readonly code: 'unsupported' | 'stale' | 'failed' | 'unknown'; readonly message: string }
```
## Session 绑定
`ctx.forgeSession` 从每个 Session 自己的日志派生其绑定:`gh pr create` 的结果绑定其输出的 URL,`gh pr view`、`gh pr edit` 与 `gh pr checkout` 调用绑定它们所操作的变更请求,`git push` 的结果通过 `ctx.forge` 触发按 head 分支的发现;面板也会报告其检出所跟踪的分支,触发同样的发现。track、dismiss 与发现操作都是 `plugin:forge-session/binding` 插件记录,位于模型可见对话之外。`forgeChangeRequests` 投影的客户端视图随 Session 摘要下发,列出绑定但不含其状态。识别与发现规则见 [Session 绑定 README](../../packages/experimental/forge-session/README.zh.md#use-this-package)。
```ts type-equiv
/**
* How a change request became bound to a Session: `created` when a
* `gh pr create` result printed its URL, `referenced` when a `gh pr view`,
* `gh pr edit`, or `gh pr checkout` call named it or its result printed its
* own URL, `discovered` when a lookup found it for a branch the Session pushed
* or works on, `tracked` when a caller bound it by URL.
*/
type BindingOrigin = 'created' | 'referenced' | 'discovered' | 'tracked'
```
```ts type-equiv
/** Payload of the `plugin:forge-session/binding` record. */
interface BindingRecord {
/**
* `track` binds or restores, `dismiss` hides, `discover` binds a branch lookup,
* and `reference` binds a CLI selector lookup without restoring dismissed entries.
*/
readonly action: 'track' | 'dismiss' | 'discover' | 'reference'
/** Web URL of the change request. */
readonly url: string
}
```
```ts type-equiv
/** One change request bound to a Session and not dismissed, with its latest polling result. */
interface SessionChangeRequest {
/** Web URL as first recorded. */
readonly url: string
/** How the change request became bound. */
readonly origin: BindingOrigin
/** Parsed reference, or null when no registered provider serves the URL. */
readonly ref: ChangeRequestRef | null
/** Latest polling result, or null before the first poll and without a provider. */
readonly update: ChangeRequestUpdate | null
}
```
```ts type-equiv
/**
* Receives the complete visible list after a binding or provider change, or a polling result.
* @param changeRequests - every visible binding of the watched Session, in first-recorded order.
*/
type SessionChangeRequestsListener = (changeRequests: readonly SessionChangeRequest[]) => void
```
## 面板
`ctx.forgePanel` 是 [forge 面板](../../packages/experimental/client-ui-forge/README.zh.md#use-this-package)的宿主 Remote。跟随一个 Session 时,它以流的形式推送 `ForgePanelState`:在 Session 执行环境中对其当前目录进行的只读 Git 检查、从上游远端或 `origin` 识别出的代码托管平台、该平台的登录状态、Session 的绑定及其轮询状态与最近的快照,以及它的 worktree 请求。它的操作可以列出基准分支、绑定和隐藏 URL、刷新、把已绑定的草稿标记为可评审或合并已绑定的变更请求、请求或撤回 worktree、停止正在进行的创建,以及恢复已删除的 worktree 或离开它回到源检出。请求是一条 `plugin:client-ui-forge/worktree` 记录。消息在 Session 的 Agent 空闲时进入其收件箱,面板会在这次投递唤醒 [Agent](../../packages/core/agent/README.zh.md) 之前启动它的维护任务,暂留这条消息的这一轮:对未完成的请求,它通过 `ctx.worktrees` 创建 worktree,尝试失败或被停止后等待下一次尝试或请求被撤回;对在所创建 worktree 中工作的 Session,它检查该 worktree,已被删除时等待它被恢复或离开。面板状态说明消息在等待什么,以及最近一次失败的尝试。对于没有被暂留的消息,由第一个步骤提议创建 worktree。第一轮开始之后的请求会被忽略。对 Session 检出的改动以其 Agent 的维护任务运行,因此其他客户端发来的消息会等它们完成。
```ts type-equiv
/** Git facts for the checkout that contains the Session's current directory. */
interface ForgeCheckout {
/** Top-level directory of the checkout. */
readonly root: string
/** Checked-out branch, or null when HEAD is detached. */
readonly branch: string | null
/** Whether the checkout is a linked worktree of another checkout's repository. */
readonly linked: boolean
/** Abbreviated commit HEAD names, or null while the checked-out branch has no commit. */
readonly commit: string | null
}
```
```ts type-equiv
/** Inspection result for the Session's current directory. */
type ForgeCheckoutState =
/** The first inspection has not finished. */
| { readonly state: 'pending' }
/** The current directory is not inside a Git checkout. */
| { readonly state: 'none' }
/** Git could not run or reported an error. */
| { readonly state: 'failed'; readonly message: string }
/** The checkout facts. */
| { readonly state: 'ready'; readonly checkout: ForgeCheckout }
```
```ts type-equiv
/** The worktree the panel created and entered for a Session before its first step. */
interface ForgeSessionWorktree {
/** Canonical checkout directory. */
readonly path: string
/** Branch created for the checkout. */
readonly branch: string
/** Requested base branch, or null when the checkout started from HEAD of the source checkout. */
readonly from: string | null
/** Commit object name the branch started from. */
readonly baseCommit: string
/** Canonical root of the source checkout. */
readonly repositoryRoot: string
}
```
```ts type-equiv
/** Payload of the `plugin:client-ui-forge/worktree` record. */
type WorktreeRecord =
/** Create a worktree before the Session's first step, starting from `from`, or from HEAD when it is null. */
| { readonly action: 'request'; readonly from: string | null }
/** Withdraw the open request. */
| { readonly action: 'cancel' }
/** The requested worktree was created and became the Session's working directory. */
| { readonly action: 'created'; readonly worktree: ForgeSessionWorktree }
/** Restoration started; the checkout stays unavailable until restoration completes. */
| { readonly action: 'restoring' }
/** The recorded worktree was completely restored. */
| { readonly action: 'restored' }
```
```ts type-equiv
/** One open worktree request. */
interface ForgeWorktreeRequest {
/** Base branch, or null for HEAD of the source checkout. */
readonly from: string | null
}
```
```ts type-equiv
/** Why the Session's checkout cannot host a new worktree. */
type ForgeWorktreeBlocker =
/** The current directory is not inside a Git checkout. */
| 'not-git'
/** The Session's sandbox cannot write the checkout or its Git directory, as for a linked worktree outside the writable root. */
| 'outside-writable-root'
/** The Session runs read-only. */
| 'read-only'
```
```ts type-equiv
/** One running worktree creation. */
interface ForgeWorktreeCreation {
/** Running step: `prepare` validates and creates the branch, then `checkout` writes the files. */
readonly phase: 'prepare' | 'checkout'
/** Branch the attempt creates. */
readonly branch: string
/** Base branch, or null for HEAD. */
readonly from: string | null
/**
* Whether a message waits for the creation before its turn starts. Only
* such a creation can be stopped, after which the message keeps waiting; a
* creation at the first step serves a message the Session already holds.
*/
readonly held: boolean
}
```
```ts type-equiv
/** One running restoration of the removed worktree a Session works in. */
interface ForgeWorktreeRestoration {
/** Running step: `prepare` registers the checkout again, creating a missing branch, then `checkout` writes the files. */
readonly phase: 'prepare' | 'checkout'
}
```
```ts type-equiv
/** The last attempt to create the requested worktree for a message, which failed. */
interface ForgeWorktreeFailure {
/** Branch the attempt created, or null when it failed before naming one. */
readonly branch: string | null
/** Base branch of the attempt, or null for HEAD. */
readonly from: string | null
/** Step that failed. */
readonly phase: 'prepare' | 'checkout'
/** Failure text. */
readonly message: string
}
```
```ts type-equiv
/**
* What a message waits for before its turn starts: the creation of the
* requested worktree, or the restoration or leaving of the removed worktree
* the Session works in.
*/
type ForgeWorktreeWaiting = 'creation' | 'restoration'
```
```ts type-equiv
/** The recorded worktree whose directory is missing, invalid, or incompletely restored. */
interface ForgeMissingWorktree {
/** Whether its branch exists, or null when an incomplete restoration cannot inspect it. */
readonly branchExists: boolean | null
}
```
```ts type-equiv
/** Worktree facts of one Session in the panel state. */
interface ForgePanelWorktree {
/**
* Whether the Session can still request or withdraw a worktree: a worktree
* service is mounted, no turn has started, and none was created.
*/
readonly editable: boolean
/** Why the checkout cannot host a worktree, or null when it can, before the first inspection, and after a failed one. */
readonly blocker: ForgeWorktreeBlocker | null
/** Open request, or null without one. */
readonly request: ForgeWorktreeRequest | null
/** Created worktree, or null before creation. */
readonly created: ForgeSessionWorktree | null
/** Running creation, or null. */
readonly creation: ForgeWorktreeCreation | null
/**
* The last failed attempt of the creation a message waits for, or null. It
* clears when another attempt starts, when the request changes or is
* withdrawn, and when the wait ends.
*/
readonly failure: ForgeWorktreeFailure | null
/** The created worktree the Session works in when its directory is gone, or null. */
readonly missing: ForgeMissingWorktree | null
/** Running restoration of the removed worktree, or null. */
readonly restoration: ForgeWorktreeRestoration | null
/** What a message of the Session waits for before its turn starts, or null while none waits. */
readonly waiting: ForgeWorktreeWaiting | null
}
```
```ts type-equiv
/** One bound change request in the panel state. */
interface ForgePanelChangeRequest extends SessionChangeRequest {
/** Latest snapshot a poll returned, kept while later polls fail, or null before the first successful poll. */
readonly snapshot: ChangeRequestSnapshot | null
}
```
```ts type-equiv
/** Everything the panel shows for one Session. */
interface ForgePanelState {
/** Current working directory of the Session. */
readonly directory: string
/** Inspection result for `directory`. */
readonly checkout: ForgeCheckoutState
/** Code host repository of the checkout's upstream remote or `origin`, or null when no provider serves them. */
readonly repository: ForgeRepository | null
/** Sign-in state for the repository's host, or null before the first probe and without a repository. */
readonly readiness: ForgeReadiness | null
/** Visible change request bindings with their latest polling results, in first-recorded order. */
readonly changeRequests: readonly ForgePanelChangeRequest[]
/** Worktree request and creation state. */
readonly worktree: ForgePanelWorktree
/**
* Files the `branch` scope changes, or null before the first inspection,
* outside a checkout, and without a fork point.
*/
readonly local: ForgeLocalChanges | null
}
```
```ts type-equiv
/** Local branches offered as the base of a new worktree. */
interface ForgeBranches {
/** Checked-out branch, or null when HEAD is detached. */
readonly current: string | null
/** Local branch names, `current` first and then most recently committed first, at most `maxBranches`. */
readonly branches: readonly string[]
}
```
会话列表经同一个 Remote 读取。`changeRequestStates` 以流的形式推送 `ForgeChangeRequestStatesFrame`:先是一个 `baseline`,包含调用方可读的已列出 Session 所绑定的每个变更请求最近一次已知的 `ForgeChangeRequestListState`,之后是发生变化的状态。宿主用一次运行服务所有流:已加载 Session 的绑定取自 `ctx.forgeSession`,其他 Session 的绑定取自缓存的 Session 摘要而不加载它们;每个已绑定的变更请求保持订阅,已关闭的按空闲间隔轮询以检测重新打开,每次变化都会存储,因此重启后在下次轮询之前就能显示最近已知的状态。Session 摘要还带有 `forgeWorktree` 投影的客户端视图:Session 的目录就是所创建的 worktree 或位于其下时为该 worktree,否则为 null。
```ts type-equiv
/**
* The last polled state of one change request that a listed Session binds, as
* the Session list shows it.
*/
type ForgeChangeRequestListState = Pick<
ChangeRequestSnapshot, 'url' | 'title' | 'state' | 'draft' | 'checksRollup' | 'reviewDecision' | 'mergeability'
> & {
/** Change request number on its code host. */
readonly number: number
}
```
```ts type-equiv
/**
* One frame of `forgePanel.changeRequestStates`: the known state of every
* change request the reader may see first, then the states that changed or
* became visible since the previous frame.
*/
type ForgeChangeRequestStatesFrame =
| { readonly type: 'baseline'; readonly states: readonly ForgeChangeRequestListState[] }
| { readonly type: 'changed'; readonly states: readonly ForgeChangeRequestListState[] }
```
变更视图经同一个 Remote 读取。`localDiff` 读取 Session 检出的某个 `ForgeLocalScope`;`changeRequestDiff` 经提供方的 `diff` 读取已绑定变更请求在其最新状态 head 处的变更;两者返回的文件都已在宿主上解析为 `ForgeDiffFile` 的 hunk。`localText` 与 `changeRequestText` 读取一个文件的新版本,用来补出 hunk 之间未变更的行。每次检查还会把 `branch` 范围的规模统计为 `ForgePanelState.local`,供面板的变更卡片使用。
```ts type-equiv
/** Which local changes a diff covers; each reads the Session's checkout through Git. */
type ForgeLocalScope =
/**
* The working tree and untracked files against the fork point: the base commit of the worktree the
* Session created, else the merge base of HEAD with the default branch of the code host remote.
*/
| { readonly kind: 'branch' }
/** The working tree and untracked files against HEAD. */
| { readonly kind: 'uncommitted' }
/** The working tree and untracked files against the index. */
| { readonly kind: 'unstaged' }
/** The index against HEAD. */
| { readonly kind: 'staged' }
```
```ts type-equiv
/** One hunk of a file diff. */
interface ForgeDiffHunk {
/** First old line the hunk covers, or the old line it comes before when it covers none. */
readonly oldStart: number
/** Old lines the hunk covers. */
readonly oldLines: number
/** First new line the hunk covers, or the new line it comes before when it covers none. */
readonly newStart: number
/** New lines the hunk covers. */
readonly newLines: number
/** Lines in patch form: a leading space for context, `+` for added, `-` for deleted, and `\` for a missing final newline. */
readonly lines: readonly string[]
}
```
```ts type-equiv
/** How one file changed. */
type ForgeDiffStatus = 'added' | 'deleted' | 'modified' | 'renamed'
```
```ts type-equiv
/** One changed file of a diff. */
interface ForgeDiffFile {
/** Path relative to the repository root after the change, or before it for a deleted file. */
readonly path: string
/** Path before a rename, else null. */
readonly previousPath: string | null
/** How the file changed. */
readonly status: ForgeDiffStatus
/** Whether the content is binary, which carries no hunks. */
readonly binary: boolean
/** Whether an untracked file exceeds `maxDiffBytes`, so it carries no hunks. */
readonly oversized: boolean
/** Whether Git does not track the file yet; only local diffs list such files. */
readonly untracked: boolean
/** Lines added. */
readonly additions: number
/** Lines deleted. */
readonly deletions: number
/** Hunks in file order; none for a binary file or a change of mode or name only. */
readonly hunks: readonly ForgeDiffHunk[]
}
```
```ts type-equiv
/** Local changes of the Session's checkout in one scope. */
interface ForgeLocalDiff {
/** Ref or abbreviated commit the `branch` scope compares with, such as `origin/master`; null for other scopes. */
readonly base: string | null
/** Changed files in Git's path order, untracked files last. */
readonly files: readonly ForgeDiffFile[]
/** Untracked files left out beyond `maxUntrackedFiles`. */
readonly omitted: number
}
```
```ts type-equiv
/** Changes of a change request as its code host computes them. */
interface ForgeChangeRequestDiff {
/** Head commit the diff describes. */
readonly headSha: string
/** Changed files in the host's order. */
readonly files: readonly ForgeDiffFile[]
/**
* Commits the checkout's branch has beyond the head when that branch is the change request's head
* branch and the head commit is present locally; null otherwise.
*/
readonly localAhead: number | null
/** Web page of each changed file at the head commit, by path. */
readonly fileUrls: Readonly<Record<string, string>>
}
```
```ts type-equiv
/** One file the `branch` scope changes, without its hunks. */
interface ForgeChangedFile {
/** Path relative to the repository root after the change. */
readonly path: string
/** Lines added, or 0 for a binary file. */
readonly additions: number
/** Lines deleted, or 0 for a binary file. */
readonly deletions: number
/** Whether the content is binary. */
readonly binary: boolean
}
```
```ts type-equiv
/** Size of the `branch` scope, which the panel's local changes card shows. */
interface ForgeLocalChanges {
/** Ref or abbreviated commit the scope compares with. */
readonly base: string
/** Changed files in Git's path order, untracked files last, at most `maxUntrackedFiles` of those. */
readonly files: readonly ForgeChangedFile[]
}
```
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis API
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxforge--forgeservice"></a>
### `ctx.forge` — `ForgeService`
Registry of forge providers. Watchers of one change request share a single polling loop; each forge has at most one fetch in flight, which batches every due change request of that forge.
```ts cordis-catalog
/**
* Register the provider of one forge family as an effect on the calling
* fiber. Watches of that forge receive an `unavailable` failure when the
* registration ends. Unregistering cancels and awaits its in-flight calls;
* results from the withdrawn registration are discarded.
* @param provider - provider to serve `provider.forge`.
* @returns the disposer that unregisters the provider and waits for its calls to finish.
* @throws when another provider already serves the same forge.
*/
register(provider: ForgeProvider): () => Promise<void>
/**
* Map a Git remote URL to a repository through the first provider that serves it.
* @param url - fetch or push URL as Git reports it.
* @returns the repository, or undefined when no provider serves the URL.
*/
repositoryFromRemote(url: string): ForgeRepository | undefined
/**
* Map a change request web URL through the first provider that serves it.
* @param url - web URL of a pull or merge request.
* @returns the change request, or undefined when no provider serves the URL.
*/
changeRequestFromUrl(url: string): ChangeRequestRef | undefined
/**
* Build the canonical web URL of a change request.
* @param ref - change request served by a registered provider.
* @returns the provider's canonical URL.
* @throws when no provider serves the change request's forge.
*/
changeRequestUrl(ref: ChangeRequestRef): string
/**
* Find open change requests whose head is `branch`, excluding the default branch and forks.
* @param repository - repository served by a registered provider.
* @param branch - head branch name on the host.
* @param signal - cancellation for the lookup.
* @returns matching change requests, newest first as the provider reports them.
* @throws when no provider serves the repository's forge or the lookup fails.
*/
async findByHead(repository: ForgeRepository, branch: string, signal: AbortSignal): Promise<readonly ChangeRequestRef[]>
/**
* Report whether the provider can reach the repository's host with current credentials.
* @param repository - repository served by a registered provider.
* @param signal - cancellation for the probe.
* @returns the provider's readiness for the host.
* @throws when no provider serves the repository's forge.
*/
async readiness(repository: ForgeRepository, signal: AbortSignal): Promise<ForgeReadiness>
/**
* Mark a draft change request ready for review, then poll it at once when it is watched.
* @param ref - change request served by a registered provider.
* @param signal - cancellation of the request.
* @returns undefined once the host accepted the change, or why it did not.
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
*/
async markReady(ref: ChangeRequestRef, signal: AbortSignal): Promise<ForgeActionFailure | undefined>
/**
* Merge a change request while its head is still `request.headSha`, then
* poll it at once when it is watched.
* @param ref - change request served by a registered provider.
* @param request - merge method and the head commit the user confirmed.
* @param signal - cancellation of the request.
* @returns undefined once the host merged it, or why it did not.
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
*/
async merge(ref: ChangeRequestRef, request: MergeRequest, signal: AbortSignal): Promise<ForgeActionFailure | undefined>
/**
* Read complete check details independently of periodic polling.
* @param ref - change request served by a registered provider.
* @param signal - cancellation of the detail view.
* @returns a complete snapshot or the provider's failure; unsupported providers return unavailable.
* @throws on cancellation, provider disposal, or expiry of this read's own `fetchTimeoutMs` deadline.
*/
async fetchChecks(ref: ChangeRequestRef, signal: AbortSignal): Promise<ChangeRequestSnapshot | ForgeFailure>
/**
* Read the changes of a change request as its host computes them.
* @param ref - change request served by a registered provider.
* @param signal - cancellation of the read.
* @returns one unified patch with `a/` and `b/` path prefixes, or why the host did not provide it.
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
*/
async diff(ref: ChangeRequestRef, signal: AbortSignal): Promise<string | ForgeFailure>
/**
* Publish one comment or a completed review, then refresh the watched status.
* @param ref - change request served by a registered provider.
* @param request - explicitly selected commit, comments, and review decision.
* @param signal - request cancellation.
* @returns a publication URL or a failure, including an unknown write outcome.
*/
async submitReview(ref: ChangeRequestRef, request: ForgeReviewRequest, signal: AbortSignal): Promise<ForgeReviewResult>
/**
* Read the text of one file at a commit of a change request's repository.
* @param ref - change request served by a registered provider.
* @param commit - commit object name.
* @param path - path relative to the repository root.
* @param signal - cancellation of the read.
* @returns the text, or why the host did not provide it.
* @throws when no provider serves the change request's forge, or with `signal.reason` when it aborts.
*/
async fileText(ref: ChangeRequestRef, commit: string, path: string, signal: AbortSignal): Promise<string | ForgeFailure>
/**
* Build the web page of one file at a commit of a change request's repository.
* @param ref - change request served by a registered provider.
* @param commit - commit object name.
* @param path - path relative to the repository root.
* @returns the provider's URL.
* @throws when no provider serves the change request's forge.
*/
fileUrl(ref: ChangeRequestRef, commit: string, path: string): string
/**
* Watch one change request as an effect on the calling fiber. The first
* watcher starts polling at once; listeners receive every later result,
* and `latest` returns the current one. Polling stops when the last watcher
* leaves. Listeners must not throw; a thrown error is logged and skipped.
* @param ref - change request to poll.
* @param listener - receives each polling result.
* @returns the disposer that stops this watch.
*/
watch(ref: ChangeRequestRef, listener: ChangeRequestListener): () => void
/**
* Poll a watched change request at once, including after a terminal state.
* A fetch already in flight is followed immediately by another fetch; an
* unwatched change request is unchanged.
* @param ref - change request to poll.
*/
refresh(ref: ChangeRequestRef): void
/**
* Read the latest polling result of a watched change request.
* @param ref - change request to read.
* @returns the latest result, or undefined before the first result or without a watch.
*/
latest(ref: ChangeRequestRef): ChangeRequestUpdate | undefined
```
Source: [`packages/experimental/forge/src/index.ts`](../../packages/experimental/forge/src/index.ts)
<a id="ctxforgepanel--forgepanelcontroller"></a>
### `ctx.forgePanel` — `ForgePanelController`
Serves the forge panel. Following a Session inspects the checkout of its current directory using the Session's permissions and execution world, recognizes the code host of the upstream remote or `origin`, probes sign-in for that host, and polls the Session's bound change requests through `ctx.forge` until the last follower of that Session leaves. A worktree request makes the panel create a branch and checkout through an optional `ctx.worktrees` and enter it while the Session's first message waits in the inbox, or at the first step proposal of a message no wait took. After a failed or stopped attempt the message keeps waiting for another attempt or for the request to be withdrawn. A message also waits while the worktree the Session works in is gone, until that worktree is checked out again or left for the source checkout. Changes to a Session's checkout run as Agent maintenance, so a message that arrives meanwhile waits for them.
```ts cordis-catalog
/**
* Stream the panel state of a Session: the current state first, then each
* later state. Intermediate states may be skipped when the reader is slower
* than the updates. The stream ends when `signal` aborts or the Remote stops.
* @param session - Session to follow.
* @param signal - ends the stream.
* @returns panel states in publication order.
*/
@Remote({ mode: 'stream' }) async *follow(session: Session, signal: AbortSignal): AsyncIterable<ForgePanelState>
/**
* Stream the last known state of every change request that a listed
* Session the caller may read binds: the stored states first, then each
* state a poll changes or a new binding reveals. Listing reads no Session
* log: loaded Sessions report their bindings, others the cached Session
* summary, and a Session without one shows nothing until it is opened.
* Closed requests are polled at the idle cadence so reopening is observed. The
* stream ends when `signal` aborts or the Remote stops.
* @param signal - ends the stream.
* @returns frames in publication order.
* @throws when the Host lacks the Session query, projection cache, storage, or Session store service.
*/
@Remote({ mode: 'stream' }) async *changeRequestStates(signal: AbortSignal): AsyncIterable<ForgeChangeRequestStatesFrame>
/**
* List the local branches that can start a worktree of the Session's current checkout.
* @param session - Session whose current directory is listed.
* @param signal - request cancellation.
* @returns the checked-out branch and at most `maxBranches` local branches, the checked-out one first.
* @throws when the current directory is not inside a checkout or Git fails.
*/
@Remote async branches(session: Session, signal: AbortSignal): Promise<ForgeBranches>
/**
* Bind a change request to a Session by URL, or restore a dismissed binding.
* @param session - Session that receives the binding.
* @param url - web URL of the change request.
* @param signal - request cancellation.
* @throws `forgePanel/unrecognized-url` when no registered provider serves the URL.
*/
@Remote async track(session: Session, url: string, signal: AbortSignal): Promise<void>
/**
* Hide a visible binding of a Session; other URLs leave the Session unchanged.
* @param session - Session that owns the binding.
* @param url - web URL of the change request.
* @param signal - request cancellation.
*/
@Remote async dismiss(session: Session, url: string, signal: AbortSignal): Promise<void>
/**
* Reinspect a followed Session's checkout, probe sign-in again, and poll its
* change requests at once; an unfollowed Session is unchanged.
* @param session - Session whose panel refreshes.
* @param signal - request cancellation.
* @returns settles once the inspection and the sign-in probe have ended; poll results reach the panel stream later.
*/
@Remote async refresh(session: Session, signal: AbortSignal): Promise<void>
/**
* Read the changes one scope covers in the checkout of the Session's
* current directory, with Git using the Session's sandbox policy.
* @param session - Session whose checkout is read.
* @param scope - which changes to read.
* @param signal - request cancellation.
* @returns the files, the base of the `branch` scope, and the untracked files left out.
* @throws `forgePanel/not-a-checkout` outside a checkout; `forgePanel/no-fork-point` for the `branch`
* scope of a checkout without one; `forgePanel/filter-config-failed` when filter configuration cannot be read
* completely, before any comparison; `forgePanel/diff-too-large` over `maxDiffBytes`; the Git error when Git fails.
*/
@Remote async localDiff(session: Session, scope: ForgeLocalScope, signal: AbortSignal): Promise<ForgeLocalDiff>
/**
* Read the new side of one changed file in a scope, which supplies the
* unchanged lines between its hunks: the index for `staged`, else the working tree.
* @param session - Session whose checkout is read.
* @param scope - the scope the diff was read in.
* @param path - path relative to the checkout's top-level directory, as the diff names it.
* @param signal - request cancellation.
* @returns the text.
* @throws `forgePanel/not-a-checkout` outside a checkout; `forgePanel/diff-too-large` over `maxDiffBytes`;
* an error for a path outside the checkout, binary content, or a failed read.
*/
@Remote async localText(session: Session, scope: ForgeLocalScope, path: string, signal: AbortSignal): Promise<string>
/**
* Load complete checks for the change request whose list the user opened.
* @param session - Session retaining the visible binding.
* @param url - binding URL.
* @param headSha - commit displayed by the card; a different head rejects the result.
* @param signal - cancellation when the list closes.
* @returns the complete snapshot at the displayed head.
* @throws `forgePanel/not-bound` for an invisible binding, `forgePanel/stale-head` for a moved head,
* or `forgePanel/checks-failed` when the provider cannot complete the read.
*/
@Remote async changeRequestChecks(session: Session, url: string, headSha: string, signal: AbortSignal): Promise<ChangeRequestSnapshot>
/**
* Read the changes of a change request of a Session as its code host
* computes them, at the head commit of its latest status. A patch already
* read at the same head commit and base branch is reused from memory, for
* any Session, until `maxCachedDiffBytes` evicts it.
* @param session - Session the change request is bound to.
* @param url - binding URL of the change request.
* @param signal - request cancellation.
* @returns the files, the head commit, each file's web page, and the commits the checkout has beyond the head.
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
* `forgePanel/diff-too-large` when the host or its provider refuses the diff over a size limit;
* `forgePanel/diff-failed` before its status was read and when the host does not provide the diff otherwise.
*/
@Remote async changeRequestDiff(session: Session, url: string, signal: AbortSignal): Promise<ForgeChangeRequestDiff>
/**
* Read one file of a change request's repository at a commit, which
* supplies the unchanged lines between the hunks of its diff.
* @param session - Session the change request is bound to.
* @param url - binding URL of the change request.
* @param commit - commit object name, the head commit of the diff.
* @param path - path relative to the repository root.
* @param signal - request cancellation.
* @returns the text.
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
* `forgePanel/diff-too-large` when the host or its provider refuses the file over a size limit;
* `forgePanel/diff-failed` when the host does not provide the file otherwise.
*/
@Remote async changeRequestText(session: Session, url: string, commit: string, path: string, signal: AbortSignal): Promise<string>
/**
* Publish a line comment or completed review for a visible Session binding.
* @param session - Session retaining the change request.
* @param url - visible binding URL.
* @param request - reviewed head, ranges, plain text, and explicit review decision.
* @param signal - request cancellation.
* @returns confirmed publication or a typed failure; unknown outcomes require reconciliation on the host.
*/
@Remote async submitReview(session: Session, url: string, request: ForgeReviewRequest, signal: AbortSignal): Promise<ForgeReviewResult>
/**
* Mark a draft change request of a Session ready for review; the change
* request is polled again once the host answers.
* @param session - Session the change request is bound to.
* @param url - binding URL of the change request.
* @param signal - request cancellation.
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
* `forgePanel/action-failed` with the host's reason when the host refuses or cannot be reached.
*/
@Remote async markReady(session: Session, url: string, signal: AbortSignal): Promise<void>
/**
* Merge a change request of a Session while its head is still `headSha`;
* the change request is polled again once the host answers.
* @param session - Session the change request is bound to.
* @param url - binding URL of the change request.
* @param method - how the commits enter the base branch.
* @param headSha - head commit the user confirmed.
* @param signal - request cancellation.
* @throws `forgePanel/not-bound` unless the URL is a visible binding of the Session that a provider serves;
* `forgePanel/stale-head` when the head moved past `headSha`; `forgePanel/action-failed` with the host's
* reason when the host refuses otherwise or cannot be reached.
*/
@Remote async merge(session: Session, url: string, method: MergeMethod, headSha: string, signal: AbortSignal): Promise<void>
/**
* Ask for a new worktree before the Session's first turn, replacing an open
* request and forgetting a failed attempt. While the next message waits in
* the inbox, or else at the first step proposal, the panel creates a branch
* from `from`, or from HEAD of the current checkout when it is null, and
* enters its checkout. A message that already waits after a failed or
* stopped attempt keeps waiting for {@link ForgePanelController.retryWorktree}.
* @param session - Session without a started turn.
* @param from - local base branch, or null for HEAD.
* @param signal - request cancellation.
* @throws `forgePanel/invalid-branch` when `from` is empty; `forgePanel/worktrees-unavailable`
* without a worktree service; `forgePanel/session-started` after a turn started or the worktree was
* created; `forgePanel/session-busy` while the worktree is being created.
*/
@Remote async requestWorktree(session: Session, from: string | null, signal: AbortSignal): Promise<void>
/**
* Withdraw the open worktree request of a Session with its failed attempt,
* which lets a waiting message go on in the current checkout; without a
* request the Session is unchanged. Withdrawing needs no worktree service.
* @param session - Session without a started turn.
* @param signal - request cancellation.
* @throws `forgePanel/session-started` after a turn started or the worktree was created;
* `forgePanel/session-busy` while the worktree is being created.
*/
@Remote async cancelWorktree(session: Session, signal: AbortSignal): Promise<void>
/**
* Stop the creation a message of the Session waits for; the message keeps
* waiting with the request still open. Without such a creation the Session
* is unchanged.
* @param session - Session whose creation stops.
* @param signal - request cancellation.
*/
@Remote async stopCreation(session: Session, signal: AbortSignal): Promise<void>
/**
* Make another attempt, with a new branch name, at the worktree a message
* of the Session waits for after a failed or stopped attempt.
* @param session - Session whose message waits.
* @param signal - request cancellation.
* @throws `forgePanel/not-waiting` when no message waits or an attempt is running.
*/
@Remote async retryWorktree(session: Session, signal: AbortSignal): Promise<void>
/**
* Check out the created worktree of a Session again at its recorded
* directory when that directory is gone: its branch when the branch still
* exists, otherwise a new branch of the same name at the base commit. Changes
* that were never committed are not recovered. Runs as Agent maintenance,
* inside the wait of a message that waits for the restoration, which then
* goes on; without a missing directory the Session is unchanged.
* @param session - Session whose worktree directory is gone.
* @param signal - request cancellation, which aborts the restoration.
* @throws `forgePanel/no-worktree` without a created worktree; `forgePanel/worktrees-unavailable`
* without a worktree service; `forgePanel/session-busy` while the Session's Agent is not loaded or
* has other work; `forgePanel/restore-failed` when Git fails.
*/
@Remote async restoreWorktree(session: Session, signal: AbortSignal): Promise<void>
/**
* Move a Session from its created worktree to the source checkout the
* worktree came from; the Session no longer works in the worktree. Runs as
* Agent maintenance, inside the wait of a message that waits for the removed
* worktree, which then goes on.
* @param session - Session with a created worktree.
* @param signal - request cancellation.
* @throws `forgePanel/no-worktree` without a created worktree; `forgePanel/session-busy` while the
* Session's Agent is not loaded or has other work; the working-directory service's error when the
* source checkout no longer exists.
*/
@Remote async continueInProject(session: Session, signal: AbortSignal): Promise<void>
```
Types: [Session](session.zh.md)
Source: [`packages/experimental/client-ui-forge/src/index.ts`](../../packages/experimental/client-ui-forge/src/index.ts)
<a id="ctxforgesession--forgesessionservice"></a>
### `ctx.forgeSession` — `ForgeSessionService`
Session-scoped change request bindings. The `forgeChangeRequests` projection derives them from `gh pr create`, `gh pr view`, `gh pr edit`, `gh pr checkout`, and `git push` results and from `plugin:forge-session/binding` records. After each push result, and when a caller reports the branch a Session works on, the service looks up open change requests whose head is that branch and records the unbound ones as discovered. Watching a Session polls its visible change requests through `ctx.forge`.
```ts cordis-catalog
/**
* List the change requests bound to a Session and not dismissed.
* @param session - Session whose bindings are read.
* @returns visible bindings in first-recorded order, each with its latest polling result.
*/
changeRequests(session: Session): SessionChangeRequest[]
/**
* Bind a change request to a Session by URL, or restore a dismissed binding.
* Appends a `track` record with the provider's canonical URL unless the
* change request is already visible.
* @param session - Session that receives the binding.
* @param url - web URL of the change request.
* @throws when no registered provider recognizes the URL.
*/
track(session: Session, url: string): void
/**
* Look up open change requests whose head is a branch the Session works on,
* and record the unbound ones as discovered, as after a push of that branch.
* A remote that no provider recognizes changes nothing; a failed lookup is
* logged and changes nothing.
* @param session - Session that receives the bindings.
* @param remote - URL of the remote that holds the branch.
* @param branch - branch name on that remote.
* @returns resolves once the lookup has recorded its bindings or failed.
*/
discoverBranch(session: Session, remote: string, branch: string): Promise<void>
/**
* Hide a visible binding. Appends a `dismiss` record; a URL that is not
* visibly bound leaves the log unchanged. Discovery never restores a
* dismissed binding; `track` does.
* @param session - Session that owns the binding.
* @param url - web URL of the change request.
*/
dismiss(session: Session, url: string): void
/**
* Watch a Session's visible change requests as an effect on the calling
* fiber. Every visible change request that a provider recognizes is polled
* through `ctx.forge` while the Session has a watcher. Listeners receive the
* complete list after each later binding or provider change, or polling result; read the
* current list with {@link changeRequests}. Listeners must not throw; a
* thrown error is logged and skipped.
* @param session - Session to watch.
* @param listener - receives the complete visible list.
* @returns the disposer that stops this watch.
*/
watch(session: Session, listener: SessionChangeRequestsListener): () => void
```
Types: [Session](session.zh.md)
Source: [`packages/experimental/forge-session/src/index.ts`](../../packages/experimental/forge-session/src/index.ts)
<a id="forge-events"></a>
### `forge/*` events
<a id="forgeproviders-changed--parallel"></a>
#### `forge/providers-changed` — parallel
Announces a provider registration or withdrawal after the registry changes. Listener failures are logged without preventing other listeners from running.
```ts cordis-catalog
/**
* Announces a provider registration or withdrawal after the registry changes.
* Listener failures are logged without preventing other listeners from running.
* @param forge - family whose provider changed.
* @mode parallel
*/
'forge/providers-changed'(forge: string): void | Promise<void>
```
Source: [`packages/experimental/forge/src/index.ts`](../../packages/experimental/forge/src/index.ts)
<!-- END GENERATED cordis-surface -->