feat(web): permission presets and approval answering for the web UI
The web host now composes the sandboxed product path (sandbox-local + sandbox-policy behind bash-sandbox/fs-sandbox, with user-approval and permission on top); BootHostOptions.sandbox carries the deployment defaults (workspace-write + ask). createApiProxy owns the approval pending registry: a ctx.approval ask becomes an answerable approval/requested mux frame with a stable rpcId, replayed verbatim on every mux open until settled; respond routes by the echoed rpcId, validates the ApprovalResponsePayload audit correlation, and broadcasts approval/resolved; the ask's abort signal withdraws the question as cancelled. session.permissions / session.setPermission project ctx.permission into a protocol-owned PermissionOption select; idle switches are held last-write-wins and flushed into the next prompted turn (the ACP bridge's anchoring pattern). The shared hasOpenTurn fold moved to dsh-session, deduplicating the private copies in user-approval, the ACP bridge, and the proxy. Client, per the designer draft: a pending approval takes over the composer (ApprovalPanel replaces the InputBar — amber strip, justification headline, paired command, one-shot refuse/allow, keyed by rpcId so a queued second approval remounts live; the resolved frame restores the composer); the sidebar session row shows an amber waiting-approval dot that outranks the running ring (manager-tracked approvalId set, idempotent under mux-open replays, cleared per connection generation, lit for uninstantiated sessions too); the permission selector is a composer bottom-row chip over an invisible native select, with a presentation-only title-case transform (workspace-write renders as Workspace Write; wire names untouched). Question placeholders stay in the message flow. The connection fixture mirrors the host behavior for keyless browser acceptance.
This commit is contained in:
@@ -18,6 +18,6 @@ None; this package neither assembles nor sends a provider request.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **State dots have two live data states (running/none)** — the done/error/amber sources arrive with P-II approvals and notifications; the four-color primitive is already wired.
|
||||
- **State dots have three live data states (running/amber approval-waiting/none)** — the done/error sources arrive with notifications; the four-color primitive is already wired.
|
||||
- **Group-by menu ships by-workspace only** — Update/Status grouping strategies are drawn without specs and deferred.
|
||||
- **"New task completed" unread marking is local viewing state** — completion-time > last-seen never reaches the host.
|
||||
|
||||
@@ -102,7 +102,11 @@ export function SessionRowItem({ row, selected, now, onOpen, onToggle }: {
|
||||
</button>
|
||||
)
|
||||
: <span className={css.slot} />}
|
||||
<span className={css.slot}>{row.running && <StateDot state="ongoing" />}</span>
|
||||
{/* Waiting-approval (amber) outranks the running ring: the session is
|
||||
blocked on the user, which is the more actionable fact. */}
|
||||
<span className={css.slot}>
|
||||
{row.waitingApproval ? <StateDot state="warning" /> : row.running && <StateDot state="ongoing" />}
|
||||
</span>
|
||||
<span className={css.title}>{row.title}</span>
|
||||
<span className={css.time}>{formatRelativeTime(row.updatedAt, now)}</span>
|
||||
<span className={css.rowActions}>
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface SessionRow {
|
||||
hasChildren: boolean
|
||||
expanded: boolean
|
||||
running: boolean
|
||||
/** An approval question is pending (amber warning dot outranks the running ring). */
|
||||
waitingApproval: boolean
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
@@ -159,6 +161,7 @@ function sessionRow(g: Group, s: SessionSummary, depth: number, hasChildren: boo
|
||||
hasChildren,
|
||||
expanded,
|
||||
running: s.running,
|
||||
waitingApproval: s.waitingApproval,
|
||||
updatedAt: s.updatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ async function bench() {
|
||||
await ctx.plugin(SlotsService).await()
|
||||
const list = createSnapshotStore<SessionListState>({
|
||||
ids: [sid('a')],
|
||||
byId: { [sid('a')]: { id: sid('a'), title: 'alpha', displayTitle: 'alpha', cwd: '/proj', running: false, updatedAt: 1 } },
|
||||
byId: { [sid('a')]: { id: sid('a'), title: 'alpha', displayTitle: 'alpha', cwd: '/proj', running: false, waitingApproval: false, updatedAt: 1 } },
|
||||
current: undefined,
|
||||
})
|
||||
const sessions = {
|
||||
|
||||
@@ -31,6 +31,7 @@ interface SummaryInit {
|
||||
cwd?: string
|
||||
parentId?: string
|
||||
running?: boolean
|
||||
waitingApproval?: boolean
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
@@ -40,6 +41,7 @@ function summary(init: SummaryInit): SessionSummary {
|
||||
title: init.title ?? init.id,
|
||||
displayTitle: init.title ?? init.id,
|
||||
running: init.running ?? false,
|
||||
waitingApproval: init.waitingApproval ?? false,
|
||||
updatedAt: init.updatedAt ?? 0,
|
||||
}
|
||||
if (init.cwd !== undefined) s.cwd = init.cwd
|
||||
@@ -290,4 +292,17 @@ describe('SidebarRoot', () => {
|
||||
expect(busyRow.querySelector('[data-state="ongoing"]')).toBeTruthy()
|
||||
expect(idleRow.querySelector('[data-state="ongoing"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('waiting-approval shows the amber warning dot and outranks the running ring', () => {
|
||||
mount(
|
||||
summary({ id: 'blocked', title: 'blocked one', cwd: '/p', running: true, waitingApproval: true, updatedAt: 2 }),
|
||||
summary({ id: 'busy', title: 'busy one', cwd: '/p', running: true, updatedAt: 1 }),
|
||||
)
|
||||
act(() => { fireEvent.click(screen.getByText('p')) })
|
||||
const blockedRow = screen.getByText('blocked one').closest('[role="treeitem"]')!
|
||||
const busyRow = screen.getByText('busy one').closest('[role="treeitem"]')!
|
||||
expect(blockedRow.querySelector('[data-state="warning"]')).toBeTruthy()
|
||||
expect(blockedRow.querySelector('[data-state="ongoing"]')).toBeNull()
|
||||
expect(busyRow.querySelector('[data-state="ongoing"]')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ interface SummaryInit {
|
||||
cwd?: string
|
||||
parentId?: string
|
||||
running?: boolean
|
||||
waitingApproval?: boolean
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
@@ -23,6 +24,7 @@ function summary(init: SummaryInit): SessionSummary {
|
||||
id: sid(init.id),
|
||||
displayTitle: init.displayTitle ?? init.title ?? init.id,
|
||||
running: init.running ?? false,
|
||||
waitingApproval: init.waitingApproval ?? false,
|
||||
updatedAt: init.updatedAt ?? 0,
|
||||
}
|
||||
if (init.title !== undefined) s.title = init.title
|
||||
|
||||
Reference in New Issue
Block a user