docs(permission): describe dismissal as a catalog invalidation

The package README, its Chinese pair, and the catalog spec now state both
public observables and pin the generation-change tick that withdraws displayed
options.
This commit is contained in:
Chinesezjc committed 2026-09-11 17:00:54 +08:00
1 parent 4a935ed8fd
commit 14bdce3644
5 files changed
+13 -7

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-permission-presets/README.md
README.md: 31417c133c542f3f81332e8547c8328c92395237
README.zh.md: 3731e903753261e467a7c7c59f2c9a8c78a2241c
README.md: 202d842a9b23574e3a2576811c9e2b4698b83e62
README.zh.md: 6353a49f61790c6d45f6b481faf76dae9f5eba4d
@@ -31,7 +31,7 @@ Mount this plugin alongside the settings, commands, and conversation packages. G
A pick submits the `/permission <preset>` command line. The argument-bearing form (`/permission <preset>` typed directly) still switches directly; the decoration replaces only the bare invocation. The built-in labels are `Read Only`, `Workspace Write`, `Full access`, and `Auto review` in English and `仅可查看`, `工作区内修改`, `完全权限`, and `Auto review` in Chinese. Explicit host labels remain unchanged, unknown kebab-case names render in title case, and `auto` carries an `EXP` badge plus an experimental-risk confirmation. `custom` is display state, never a target.
When the live catalog withdraws a preset, the composer closes its pending confirmation and shows the Session's current value instead of an unavailable optimistic pick. Every shared catalog publication also closes an open slash picker or its confirmation without consuming the draft; reopening reads the current catalog. A submitted command remains busy until its response settles.
When the live catalog withdraws a preset, the composer closes its pending confirmation and shows the Session's current value instead of an unavailable optimistic pick. Every catalog invalidation — the payload-free notification or a connection-generation change — closes an open slash picker or its confirmation without consuming the draft, while publishing the result of a read the picker waits for leaves it open with its failure and retry state; reopening reads the current catalog. A submitted command remains busy until its response settles.
### The Settings row
@@ -45,7 +45,7 @@ The row derives its options from the host's dynamic `defaultPreset` enum, uses t
<details>
<summary>Implementation internals — click to expand</summary>
The General row reads the explicitly exposed `permission` Settings descriptor through `ctx.settingsScope` and writes one `settings.mutate` path operation with the descriptor revision; its observable rides the slot system's `hooks` compartment, so the renderer owns React hook binding, and a push invalidation refetches the descriptor. The value is read only when a later session is created. The current-session surface is a popupSelect decoration hung on the host `/permission` command (`ctx.commandUi.decorate`): the host command keeps its slash-menu row, argument-bearing form, and durable lifecycle logging, while the decoration replaces only the bare invocation with the picker. One process-scoped directory subscribes to the payload-free catalog notification before its first Remote read and publishes only the latest complete success for the active connection generation. A winning failure or connection reset clears the old snapshot and makes the picker locally unavailable until a later existing trigger retries; stale-generation and disposed settlements are ignored. Its public state is only `{ value }`; failures remain internal to imperative loading. Both the slash popup and composer seat consume that shared catalog, while the Session `permissions` projection supplies only `currentValue`. Full access and Auto review each carry localized confirmation copy; Auto also carries the badge rendered by the shared popup shell.
The General row reads the explicitly exposed `permission` Settings descriptor through `ctx.settingsScope` and writes one `settings.mutate` path operation with the descriptor revision; its observable rides the slot system's `hooks` compartment, so the renderer owns React hook binding, and a push invalidation refetches the descriptor. The value is read only when a later session is created. The current-session surface is a popupSelect decoration hung on the host `/permission` command (`ctx.commandUi.decorate`): the host command keeps its slash-menu row, argument-bearing form, and durable lifecycle logging, while the decoration replaces only the bare invocation with the picker. One process-scoped directory subscribes to the payload-free catalog notification before its first Remote read and publishes only the latest complete success for the active connection generation. A winning failure or connection reset clears the old snapshot and makes the picker locally unavailable until a later existing trigger retries; stale-generation and disposed settlements are ignored. Its public observables are `{ value }` and `invalidations`, one tick per catalog notification or connection-generation change; failures remain internal to imperative loading. Both the slash popup and composer seat consume that shared catalog, while the Session `permissions` projection supplies only `currentValue`. Full access and Auto review each carry localized confirmation copy; Auto also carries the badge rendered by the shared popup shell.
</details>
@@ -31,7 +31,7 @@ kind: "package-reference"
选中即提交 `/permission <preset>` 命令行。带参形式(直接键入 `/permission <preset>`)仍直接切换;装饰只替换裸调用。内置标签在英文界面中是 `Read Only`、`Workspace Write`、`Full access` 和 `Auto review`,在中文界面中是「仅可查看」「工作区内修改」「完全权限」和 `Auto review`。显式 host 标签保持原样,未知 kebab-case 名称渲染为 Title Case;`auto` 带有 `EXP` badge,并在可见选择时要求实验风险确认。`custom` 只是显示状态,绝非目标。
实时目录撤销某个预设时,composer 关闭对应的待确认对话框,并用 Session 的当前值替代已不可用的乐观选择。每次共享目录发布还会关闭已打开的 slash 选择器或其确认对话框,不消费草稿;重新打开时读取当前目录。已经提交的命令在响应结束前继续保持忙碌状态。
实时目录撤销某个预设时,composer 关闭对应的待确认对话框,并用 Session 的当前值替代已不可用的乐观选择。每次目录失效(无 payload 的通知或连接代际变更)会关闭已打开的 slash 选择器或其确认对话框,不消费草稿;而发布某个 picker 正在等待的读取结果时,该 picker 保持打开并保留失败与重试状态;重新打开时读取当前目录。已经提交的命令在响应结束前继续保持忙碌状态。
### 设置行
@@ -45,7 +45,7 @@ kind: "package-reference"
<details>
<summary>实现细节——点击展开</summary>
General Settings 行经 `ctx.settingsScope` 读取显式暴露的 `permission` Settings 描述符,并携带描述符 revision 写入一条 `settings.mutate` 路径操作;其 observable 经 slot 系统的 `hooks` compartment 传递,因此 React 钩子绑定归渲染器,推送失效通知会重新获取描述符。该值只在之后创建会话时读取。当前会话界面是挂在宿主 `/permission` 命令上的 popupSelect 装饰(`ctx.commandUi.decorate`):宿主命令保留斜杠菜单行、带参形式与持久生命周期记账,装饰只把裸调用替换为选择器。一个进程级目录会在首次 Remote 读取前订阅无 payload 的目录通知,并且只发布当前连接代际中最新的完整成功结果。胜出的读取失败或连接 reset 会清空旧快照,使选择器在后续既有触发重试前处于本地不可用状态;旧连接代际与 dispose 后才返回的结果会被忽略。它的公共状态只有 `{ value }`,失败仅供命令式加载内部使用。slash popup 与 composer seat 共用这份目录,而 Session `permissions` 投影只提供 `currentValue`。Full access 与 Auto review 各自携带本地化确认文案;Auto 还携带由共享 popup 外壳渲染的 badge。
General Settings 行经 `ctx.settingsScope` 读取显式暴露的 `permission` Settings 描述符,并携带描述符 revision 写入一条 `settings.mutate` 路径操作;其 observable 经 slot 系统的 `hooks` compartment 传递,因此 React 钩子绑定归渲染器,推送失效通知会重新获取描述符。该值只在之后创建会话时读取。当前会话界面是挂在宿主 `/permission` 命令上的 popupSelect 装饰(`ctx.commandUi.decorate`):宿主命令保留斜杠菜单行、带参形式与持久生命周期记账,装饰只把裸调用替换为选择器。一个进程级目录会在首次 Remote 读取前订阅无 payload 的目录通知,并且只发布当前连接代际中最新的完整成功结果。胜出的读取失败或连接 reset 会清空旧快照,使选择器在后续既有触发重试前处于本地不可用状态;旧连接代际与 dispose 后才返回的结果会被忽略。它的公共 observable 是 `{ value }` 与 `invalidations`(每次目录通知或连接代际变更打一个点);失败仅供命令式加载内部使用。slash popup 与 composer seat 共用这份目录,而 Session `permissions` 投影只提供 `currentValue`。Full access 与 Auto review 各自携带本地化确认文案;Auto 还携带由共享 popup 外壳渲染的 badge。
</details>
@@ -153,7 +153,7 @@ async function bench() {
}
describe('ui-permission browser plugin', () => {
it('dismisses stale slash choices when the catalog publishes and removes that subscription on disposal', async () => {
it('dismisses stale slash choices on a catalog invalidation and removes that subscription on disposal', async () => {
const b = await bench()
const initial = b.dismissed.length
b.setCatalog({ options: CATALOG.options.filter(option => option.value !== 'auto') })
@@ -195,8 +195,11 @@ describe('PermissionCatalogDirectory', () => {
},
})
const directory = new PermissionCatalogDirectory(ctx)
expect(directory.invalidations.getSnapshot()).toEqual({ count: 0 })
generation.set(undefined)
// Generation loss and replacement each withdraw displayed options once.
expect(directory.invalidations.getSnapshot()).toEqual({ count: 1 })
expect(directory.store.getSnapshot()).toEqual({ value: null })
directory.refresh()
await expect(directory.load()).rejects.toThrow(/no active Host connection/)
@@ -206,6 +209,7 @@ describe('PermissionCatalogDirectory', () => {
expect(directory.store.getSnapshot()).toEqual({ value: null })
generation.set(2)
expect(directory.invalidations.getSnapshot()).toEqual({ count: 2 })
expect(directory.store.getSnapshot()).toEqual({ value: null })
newRead.resolve({ ok: true, value: SECOND })
await expect(directory.load()).resolves.toEqual(SECOND)
@@ -233,6 +237,8 @@ describe('PermissionCatalogDirectory', () => {
generation.set(1)
expect(calls).toBe(1)
// A repeated notification for the same generation is not an invalidation.
expect(directory.invalidations.getSnapshot()).toEqual({ count: 1 })
generation.setSilently(2)
await expect(directory.load()).resolves.toEqual(SECOND)