feat(web): projection-fed permission chip replaces the Access placeholder

PermissionSelect returns as the Access seat's wired occupant: options and
the current value read from the 'permissions' projection through the
standard-kit useProjection (no fetch, no mount timing — the resident
composer's mount-once fetch bug dies with the fetch), key absence renders
nothing (permission-less host, or a Draft with no session), and a pick
submits the '/permission <preset>' command line through the new
ComposerBarInjected.command callback (Session.command = command.execute
admission; the pushed projection frame lands the confirmed value). The
READONLY_OPTIONS placeholder and its local state leave InputBar.

The connection fixture mirrors the host: a permissions unit fold (three
knob events over the fixture preset table), the projections block +
baseline/push frames carry the key, and /permission joins the command
catalog with the same switch-through-knob-events handler shape.
This commit is contained in:
imccyu
2026-07-28 22:56:43 +08:00
parent 451b7b0446
commit 25265b312c
16 changed files with 295 additions and 38 deletions

View File

@@ -23,7 +23,7 @@ describe('createFixtureApi commands/skills', () => {
expect(response.rpcId).toBe(request.rpcId)
if (!response.result.ok) throw new Error('list failed')
const commands = response.result.value.commands
expect(commands.map(c => c.name)).toEqual(['compact', 'echo', 'goal-fixture'])
expect(commands.map(c => c.name)).toEqual(['compact', 'echo', 'goal-fixture', 'permission'])
// input hint rides only the commands declaring it.
const echo = commands.find(c => c.name === 'echo')
expect(echo?.input?.hint).toBeTruthy()

View File

@@ -71,7 +71,17 @@ describe('createFixtureApi', () => {
if (!empty.result.ok) throw new Error('empty failed')
// Fixture composes the todos unit (host parallel when tool-todo is mounted): null before any write.
expect(empty.result.value).toEqual({
events: [], hasMore: false, projections: { asOfSeq: -1, values: { todos: null } },
events: [], hasMore: false, projections: { asOfSeq: -1, values: {
todos: null,
// Permission unit composed: the composition-default select.
permissions: {
options: [
{ value: 'workspace-write', name: 'workspace-write', description: 'Write inside the workspace and permitted temporary directories; wider retries require approval.' },
{ value: 'danger-full-access', name: 'danger-full-access', description: 'Full file access without approval prompts.' },
],
currentValue: 'workspace-write',
},
} },
})
})
@@ -206,7 +216,7 @@ describe('createFixtureApi', () => {
const envelopes: RpcRequest<MuxFrame>[] = []
for await (const envelope of api.events.mux(req({}), abort.signal)) {
envelopes.push(envelope)
if (envelopes.length >= 4) abort.abort()
if (envelopes.length >= 5) abort.abort()
}
return envelopes
}
@@ -214,13 +224,14 @@ describe('createFixtureApi', () => {
const second = await openOnce()
expect(first[0]?.payload).toMatchObject({ type: 'session/subscribed', sessionId: 'fx-alpha' })
expect((first[0]?.payload as { lastSeq: number }).lastSeq).toBeGreaterThan(0)
// Projection baseline frames follow the subscribed frame (title + todos units).
// Projection baseline frames follow the subscribed frame (title + todos + permissions units).
expect(first[1]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'title', value: 'Fixture 历史会话' })
expect(first[2]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'todos' })
expect(first[3]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
expect(second[3]?.rpcId).toBe(first[3]?.rpcId) // stable rpcId across replays (host replay semantics)
expect(first[4]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
expect(second[4]?.rpcId).toBe(first[4]?.rpcId)
expect(first[3]?.payload).toMatchObject({ type: 'session/projection', sessionId: 'fx-alpha', key: 'permissions' })
expect(first[4]?.payload).toMatchObject({ type: 'approval/requested', toolName: 'dangerous_tool' })
expect(second[4]?.rpcId).toBe(first[4]?.rpcId) // stable rpcId across replays (host replay semantics)
expect(first[5]?.payload).toMatchObject({ type: 'question/requested', sessionId: 'fx-alpha' })
expect(second[5]?.rpcId).toBe(first[5]?.rpcId)
})
it('steer with no replay in flight falls through to a fresh queued turn; non-text blocks stringify empty', async () => {