Merge remote-tracking branch 'github/master' into xtr/trajectory-inspection-ui
# Conflicts: # apps/web/tests/snapshots/code-mode-round/ui.expected.md # apps/web/tests/snapshots/cordis-tool-round/ui.expected.md # apps/web/tests/snapshots/fresh-round-trip/ui.expected.md # apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md # apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md # apps/web/tests/snapshots/live-interactions/cancel.expected.md # apps/web/tests/snapshots/live-interactions/error-auth.expected.md # apps/web/tests/snapshots/live-interactions/retry.expected.md # apps/web/tests/snapshots/question-composer/answered.expected.md # apps/web/tests/snapshots/seeded-history/ui.expected.md # apps/web/tests/snapshots/steering/mid-steer.expected.md # apps/web/tests/snapshots/steering/settled.expected.md # packages/client/runtime/src/client/contract/session.ts # packages/client/ui-conversation/README.i18n.yaml
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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
|
||||
2026-07-23-web-permission-and-approval.md: cd402a039e55e7a24a038055dab5793aa0d08438
|
||||
2026-07-23-web-permission-and-approval.zh.md: ce4964789bc94a0962796bb2f5fbf1a94e8f5145
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Web UI permission presets and approval answering
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-23-web-permission-and-approval.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The web host booted an unconfined agent: `bootHost` composed `dsh-bash-local` and `dsh-fs-local`, so every web session ran with full file access, no approval channel, and no permission control — while the ACP composition had shipped the complete sandboxed product path (sandbox provider + policy home + confined bash/fs + approval + presets) for months. The web wire contract had already reserved the seats — `approval/requested`/`approval/resolved` mux frames, `POST /api/respond` with `ApprovalResponsePayload`, client-side `pendingBuffers` — but the host `respond` was a stub, no answerer bridged `ctx.approval` to the stream, no RPC exposed the permission select, and the PendingCard rendered approvals as visible-but-unanswerable.
|
||||
|
||||
## Decision
|
||||
|
||||
The web host composes the same sandboxed product path as the acp-agent composition: `dsh-sandbox-local`, `dsh-sandbox-policy`, `dsh-bash-sandbox`, `dsh-fs-sandbox`, `dsh-user-approval`, and `dsh-permission`, with `BootHostOptions.sandbox` supplying the deployment defaults (`mode`, default `workspace-write`; `approvalPolicy`, default `ask`).
|
||||
|
||||
`createApiProxy` owns the approval pending registry. Its `approval/request` waterfall answerer reads the approval id from the session's just-appended `approval/asked` audit event (an ask with no audit event is a foreign channel and delegates), mints one stable rpcId per question, broadcasts the answerable `approval/requested` frame to every open mux stream, and replays still-pending frames verbatim on each mux open — the refresh-recovery baseline the contract already promised. `respond` routes by the echoed rpcId, validates `ApprovalResponsePayload` with the existing zod schema, cross-checks the payload's audit correlation against the routed entry, resolves the answerer, and broadcasts `approval/resolved`; the ask's abort signal withdraws the question as `cancelled`.
|
||||
|
||||
The permission select rides two new unary RPCs, `session.permissions` and `session.setPermission`, projecting `ctx.permission` into a protocol-owned `PermissionOption` DTO (the ACP bridge precedent: each protocol owns its presentation shape). A permission-less composition serves an empty select and clients hide the control. Idle switches are held last-write-wins in a proxy-side pending map and flushed on `agent/prompt-submit`, because knob events must stay turn-enclosed for durable replay; the shared `hasOpenTurn` fold moved to `dsh-session` and replaced the private copies in `dsh-user-approval`, the ACP bridge, and the proxy.
|
||||
|
||||
Client-side, `Session` gained `permissions` and `setPermission`, and approval answering rides the runtime's `PendingWait` carrier. Per the designer draft, a pending approval takes over the composer: `ApprovalPanel` registers as a selector-routed entry of the conversation-declared `conversation.composer` chain (the ui-question pattern), replacing the InputBar with the justification headline, the paired command, and one-shot refuse/allow buttons; the `PendingApproval` domain face in ui-conversation's contract owns the `ApprovalResponsePayload` wire encoding over the carrier, and the broadcast resolved frame settles the wait and restores the composer. Question placeholders stay in the message flow. The sidebar mirrors the blocked state with an amber warning dot that outranks the running ring: the manager tracks per-session outstanding approvalIds (idempotent under mux-open replays, cleared per connection generation so the reopen replay is authoritative) rather than reading Session instances, so the dot lights for sessions never instantiated. The composer's bottom-row chip hosts the `PermissionSelect` control fed through the conversation inject face. The connection fixture mirrors the host: its resident approval is answerable once, and its permission select persists per session.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Reuse the ACP `session/set_config_option` shape on the web wire.** Rejected: the web contract's unary method registry (`RpcMethodMap` + per-method zod schemas) is its own dialect; a generic config-option surface would bypass the compiler-locked schema table for one select. A dedicated method pair keeps both sides derivable from the signature.
|
||||
|
||||
**A session event for pending approvals instead of a proxy-side registry.** Rejected: approval requests are transient interaction state, not durable session data — the `approval/asked`/`decided` audit pair already logs the durable half. Persisting requested frames would re-ask dead questions on replay.
|
||||
|
||||
**Registering the answerer only when a mux subscriber exists.** Rejected: the pending entry must survive client disconnects (refresh recovery is the point), so the registry outlives any one stream; a subscriber-gated answerer would fail asks closed during a reload window.
|
||||
|
||||
**Optimistic card removal on click.** Rejected: the broadcast resolved frame is the truth; removing on click would hide a question that a rejected receipt or transport failure left standing. The panel disables its buttons locally and re-arms them on failure instead.
|
||||
|
||||
## Consequences
|
||||
|
||||
Web sessions now start confined (`workspace-write` + `ask` by default) and a sandbox-denial escalation reaches the browser as an answerable card; the deployment can widen or narrow the default through `BootHostOptions.sandbox` without touching the assembly. Question answering shipped separately through the same registry pattern (ui-question over the question pending table). The permission select reads once per mount; live refresh from another client's switch is deferred. Coverage: proxy registry and permission RPC unit suites, session-object and fixture unit suites, and the keyless web smoke exercises the fixture-mode approval answer and preset switch in a real browser.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Agent Note: Web UI 权限预设与审批应答
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-23-web-permission-and-approval.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Web 承载层启动的是一个不受限的 agent(智能体):`bootHost` 组合了 `dsh-bash-local` 与 `dsh-fs-local`,因此每个 Web 会话都以完整文件访问权限运行,既无审批通道,也无权限管控——而 ACP 组合早在数月前就已交付完整的沙箱化产品路径(沙箱提供方 + 策略归属 + 受限的 bash/fs + 审批 + 预设)。Web 协议契约其实早已预留了对应位置——`approval/requested`/`approval/resolved` 的 mux 帧、携带 `ApprovalResponsePayload` 的 `POST /api/respond`、client 侧的 `pendingBuffers`——但 host 的 `respond` 只是一个 stub,没有应答者把 `ctx.approval` 桥接到流上,没有 RPC 暴露权限选择,PendingCard 把审批渲染成可见却无法应答的样子。
|
||||
|
||||
## 决策
|
||||
|
||||
Web 承载层组合与 acp-agent 相同的沙箱化产品路径:`dsh-sandbox-local`、`dsh-sandbox-policy`、`dsh-bash-sandbox`、`dsh-fs-sandbox`、`dsh-user-approval` 与 `dsh-permission`,由 `BootHostOptions.sandbox` 提供部署默认值(`mode`,默认 `workspace-write`;`approvalPolicy`,默认 `ask`)。
|
||||
|
||||
`createApiProxy` 拥有审批 pending 注册表。它的 `approval/request` waterfall(瀑布式事件)应答者从会话刚追加的 `approval/asked` 审计事件中读取审批 id(没有审计事件的 ask 属于外部通道,予以委托),为每个问题 mint 一个稳定的 rpcId,向每个打开的 mux 流广播可应答的 `approval/requested` 帧,并在每次 mux 打开时原样重放仍处于 pending 的帧——这正是契约早已承诺的刷新恢复基线。`respond` 按回显的 rpcId 路由,用既有的 zod schema 校验 `ApprovalResponsePayload`,将载荷的审计关联与所路由的条目交叉核对,解析应答者,并广播 `approval/resolved`;ask 的中断信号会以 `cancelled` 撤回该问题。
|
||||
|
||||
权限选择依托两个新的一元 RPC,`session.permissions` 与 `session.setPermission`,把 `ctx.permission` 投影为一个由协议拥有的 `PermissionOption` DTO(沿用 ACP bridge 的先例:每个协议拥有自己的呈现形状)。无权限的组合提供空的选择项,client 隐藏该控件。空闲期的切换以后写胜出(last-write-wins)的方式保存在 proxy 侧的 pending map 中,并在 `agent/prompt-submit` 时冲刷,因为旋钮事件必须保持轮次内闭合以支持持久回放;共享的 `hasOpenTurn` 折叠迁入 `dsh-session`,取代了 `dsh-user-approval`、ACP bridge 与 proxy 中各自的私有副本。
|
||||
|
||||
在 client 侧,`Session` 新增了 `permissions` 与 `setPermission`,审批应答则依托运行时的 `PendingWait` 载体。按照设计师草稿,处于 pending 的审批会接管 composer:`ApprovalPanel` 注册为由会话声明的 `conversation.composer` 链中一个按选择器路由的条目(即 ui-question 模式),以理由标题、配对的命令与一次性的拒绝/允许按钮取代 InputBar;ui-conversation 契约中的 `PendingApproval` 领域面拥有 `ApprovalResponsePayload` 在该载体上的协议编码(wire encoding),广播的 resolved 帧使该等待落定并恢复 composer。问题占位符仍留在消息流中。侧边栏用一枚琥珀色警示圆点同步呈现这一阻塞状态,且其优先级高于表示运行中的圆环:manager 跟踪每个会话尚未解决的 approvalId(对 mux 打开时的回放幂等,并按连接代次清除,以保证重开后的回放才是权威依据),而非读取 Session 实例,因此从未实例化过的会话也能点亮该圆点。composer 底行的 chip 经会话注入面挂载 `PermissionSelect` 控件。连接 fixture(测试前置数据)与 host 保持一致:它的常驻审批可应答一次,其权限选择项按会话持久保存。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
**在 Web 协议上复用 ACP 的 `session/set_config_option` 形状。** 不予采纳:Web 契约的一元方法注册表(`RpcMethodMap` + 逐方法的 zod schema)是它自成一体的方言;一个通用的 config-option 接口会为一个选择项绕开编译期锁定的 schema 表。一对专用方法让两侧都能从签名推导得出。
|
||||
|
||||
**用一个会话事件承载 pending 审批,而非 proxy 侧注册表。** 不予采纳:审批请求是瞬态的交互状态,而非持久的会话数据——`approval/asked`/`decided` 审计对已经记录了持久的那一半。持久化 requested 帧会在回放时重新问出已经作废的问题。
|
||||
|
||||
**仅在存在 mux 订阅者时才注册应答者。** 不予采纳:pending 条目必须在 client 断连后依然存活(刷新恢复正是要点所在),因此注册表的生命周期长于任何单个流;一个受订阅者门控的应答者,会让在重载窗口期间关闭的 ask 落空。
|
||||
|
||||
**点击即乐观移除卡片。** 不予采纳:广播的 resolved 帧才是真相;点击即移除会隐藏一个因拒绝回执或传输失败而仍然悬置的问题。面板改为在本地禁用其按钮,并在失败时重新启用。
|
||||
|
||||
## 后果
|
||||
|
||||
Web 会话现在从受限状态启动(默认 `workspace-write` + `ask`),一次沙箱拒绝的升级会以可应答的卡片形式抵达浏览器;部署方可以通过 `BootHostOptions.sandbox` 放宽或收紧默认值,无需触动装配。问题应答已通过同一注册表模式单独交付(ui-question 基于问题 pending 表)。权限选择在每次挂载时读取一次;来自另一个 client 切换的实时刷新暂缓实现。覆盖情况:proxy 注册表与权限 RPC 的单元测试套件、会话对象与 fixture 的单元测试套件,以及无密钥 Web 冒烟测试在真实浏览器中演练 fixture 模式的审批应答与预设切换。
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# 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 .agents/notes/implemented/feature/2026-07-29-web-message-icon-actions-and-clock.md
|
||||
2026-07-29-web-message-icon-actions-and-clock.md: e0072458e4c0d3e37998b5564ad14ce17aa41515
|
||||
2026-07-29-web-message-icon-actions-and-clock.zh.md: 1cc25a9656e7a100d78dd3b6b3675ca490f455f9
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: Web message IconActions and clocks
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-07-29-web-message-icon-actions-and-clock.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The web chat user bubble already had copy / branch / edit IconActions but no clock. Finalized assistant narration had no under-body action chrome at all, even though the Harness design shows a copy / branch / clock row after the answer settles. Streaming replies must not flash that chrome mid-token. Memoized rows also keep stable props across midnight, so a one-shot `Date.now()` would leave yesterday's messages stuck on `HH:mm`.
|
||||
|
||||
## Decision
|
||||
|
||||
**User bubbles prepend a date-aware local clock to the existing IconActions row; finalized assistant nodes append a copy / branch / clock row with `margin-top: 16px`; both seats re-format at the next local midnight.**
|
||||
|
||||
Both seats format `node.time` through `formatMessageClock`: same calendar day → `HH:mm`, earlier this year → `M月D日 HH:mm`, other years → `YYYY年M月D日 HH:mm`. `useCalendarDay` is a component-local day tick (timeout to the next local midnight) so memoized rows re-render when the calendar day changes without a new framework hook. `MessageItem` places the label before copy (figma `388:20051`). `AssistantMarkdown` places it after branch (figma `43:32997`) and only when `streaming` is false with a known event time; the streaming tail omits the row. Copy writes joined text blocks. Branch stays a chrome stub. Hover-capable pointers keep both footers opacity-hidden until hover/focus-within. Clipboard write and the clock helpers live in `message-chrome.ts`. The assembled surface is pinned by `apps/web/tests/message-actions.e2e.ts` (cold-seeded history + aria golden); aria normalization collapses every clock shape to `{{clock}}`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Show assistant IconActions during streaming.** Rejected: the request is to reveal the row only after output completes; mid-stream chrome would flicker and invite copying a partial answer.
|
||||
|
||||
**Wire branch to a real session fork.** Rejected for this change: same rationale as the archived [user IconActions note](../../archived/feature/2026-07-27-user-message-icon-actions.md) — the mutation path is unspecified; the button reserves the design seat.
|
||||
|
||||
**Publish the calendar day through a chat store or inject hook.** Rejected: the day tick is presentation-only local state with no cross-entry consumers; a component-local timeout matches the client rule that behavioral hooks may own state that does not subscribe to an external source.
|
||||
|
||||
## Consequences
|
||||
|
||||
Settled assistant answers expose copy and the event clock immediately; branch stays a stub. User and assistant clocks share the same day/year widening rules and refresh after midnight without a message mutation. Per-message paging remains a deferred footer seat in the package README. Package tests pin the three clock shapes and the midnight widen; the web e2e scenario pins the assembled IconActions chrome.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Agent Note: Web 消息 IconActions 与时钟
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-07-29-web-message-icon-actions-and-clock.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Web 聊天的用户气泡已有复制/分支/编辑 IconActions,但没有时钟。已定稿的 assistant 叙述下方完全没有操作栏,尽管 Harness 设计稿在回答结束后展示复制/分支/时钟。流式回复不得在 token 中途闪出该栏。经 memo 的行在跨午夜时仍保持稳定 props,因此一次性的 `Date.now()` 会让昨日消息一直卡在 `HH:mm`。
|
||||
|
||||
## 决策
|
||||
|
||||
**用户气泡在既有 IconActions 行前追加感知日期的本地时钟;已定稿的 assistant 节点在正文下追加带 `margin-top: 16px` 的复制/分支/时钟;两边都在下一个本地午夜重新格式化。**
|
||||
|
||||
两边都通过 `formatMessageClock` 格式化 `node.time`:同一日历日 → `HH:mm`,同年更早 → `M月D日 HH:mm`,跨年 → `YYYY年M月D日 HH:mm`。`useCalendarDay` 是组件本地的日刻度(定时到下一个本地午夜),因此 memo 行在日历日变化时会重渲染,且不新增框架 hook。`MessageItem` 把标签放在复制之前(figma `388:20051`)。`AssistantMarkdown` 把它放在分支之后(figma `43:32997`),且仅在 `streaming` 为 false 且已知事件时间时渲染;流式尾部省略该行。复制写入拼接后的 text 块。分支仍是 chrome stub。具备 hover 能力的指针在 hover/focus-within 前保持两条 footer 透明。剪贴板写入与时钟辅助函数放在 `message-chrome.ts`。组装面由 `apps/web/tests/message-actions.e2e.ts`(冷 seed 历史 + aria golden)钉住;aria 归一化把每种时钟形态折叠为 `{{clock}}`。
|
||||
|
||||
## 曾考虑的方案
|
||||
|
||||
**在流式过程中展示 assistant IconActions。** 否决:需求是输出完成后才展示该行;中途 chrome 会闪烁,并诱使复制半截回答。
|
||||
|
||||
**把分支接到真实的会话 fork。** 本次否决:与已归档的[用户 IconActions 笔记](../../archived/feature/2026-07-27-user-message-icon-actions.md)同一理由——变更路径尚未规定;按钮只预留设计座位。
|
||||
|
||||
**通过 chat store 或 inject hook 发布日历日。** 否决:日刻度只是展示层本地状态,没有跨入口消费者;组件本地 timeout 符合「行为 hook 可拥有不订阅外部源的状态」这一客户端规则。
|
||||
|
||||
## 后果
|
||||
|
||||
已定稿的 assistant 回答立刻暴露复制与事件时钟;分支仍为 stub。用户与 assistant 时钟共用同一套跨天/跨年加宽规则,并在午夜后无需消息变更即可刷新。逐消息分页仍是包 README 中的暂缓 footer 座位。包级测试钉住三种时钟形态与午夜加宽;Web e2e 场景钉住组装后的 IconActions chrome。
|
||||
Reference in New Issue
Block a user