DSH / Atlas
2026-09-08implementedfeaturearchived 2026-09-30

Feedback dialog, categories, and the acknowledgement toast

反馈弹窗、分类与确认 toast

The Web client had two disconnected feedback paths with no visible outcome. `/feedback <text>` recorded a Session remark and rendered an acknowledgement row in the transcript; the Like/Dislike pair recorded a rating at once, with a note popover anchored under the row for free text. Neither path told the user what was submitted or where it went, neither collected a category, and a Dislike, the case in which a user is

English

Problem

The Web client had two disconnected feedback paths with no visible outcome. /feedback <text> recorded a Session remark and rendered an acknowledgement row in the transcript; the Like/Dislike pair recorded a rating at once, with a note popover anchored under the row for free text. Neither path told the user what was submitted or where it went, neither collected a category, and a Dislike, the case in which a user is most willing to explain, asked nothing. Issue #3515 and the design doc for it ask for one dialog reachable from the composer menu, from a bare /feedback, and from Dislike, with seven fixed categories, an optional description, a success toast, and a filled glyph for a recorded rating, while Like keeps recording at once.

Decision

command-feedback owns the category taxonomy as the FeedbackCategory union and the FEEDBACK_CATEGORIES tuple in its client-safe ./types export, and feedback/record becomes { text?, category? }: blank text is recorded as absent, and an entry with neither member still records, because the log delivery that the feedback authorizes is the content. The same package publishes the sessionFeedback.record Remote through TypertRemoteService, resolving the live Session by id and calling the existing recordFeedback producer, so the dialog records the same event as the command without command bookkeeping. message-feedback adds the optional category to MessageFeedbackItem and MessageFeedbackPutRequest, validates stored values against the tuple, and counts a category change as a material edit.

ui-message-feedback becomes the Web feedback surface. A per-session FeedbackSurface owns the message-feedback controller, a FeedbackDialogController for the draft, the submission, and the toast sequence, and the routing between them: a message target puts its selected judgment with the dialog's category and note through the message controller, while the Session target records through ctx.remote.sessionFeedback. A FeedbackDialog entry of conversation.input.overlay renders the Modal and Toast primitives from the dialog store. A decoration on the Host's feedback command opens the dialog for the Session from a menu pick or a bare Enter while /feedback <text> still reaches the Host; it uses the action kind in CommandUiSpec, which consumes the trigger token and runs a client callback without submitting anything. The later symmetric message feedback submission decision owns the rating entry rule: either unrecorded rating opens the dialog, while clicking the recorded rating retracts it. The note popover, clearNote, and clear remain absent because the dialog is the only note editor.

The Session Header's more-actions menu also opens the Session dialog through the feedbackUi.openSession Client service. The menu remains owned by session-log-export, while the existing per-session feedback controller owns the draft and submission. An observable tracks the optional feedback plugin's availability, so unloading feedback removes only its menu row and leaves export usable. Opening or dismissing the form writes no feedback event; Submit records one Session remark.

The dialog is the shared Modal card at the design's width; the design's checkbox for including the conversation log is not built, because the log travels with every feedback event and is not optional. An oversized description still fails on submit with note-too-large; the dialog stays open with its draft and a warning toast presents the localized failure.

Alternatives considered

A child slot for Header menu actions. A slot would let each plugin own its menu row, but requires a shared menu-item API beyond the feedback entry requested in #4445. The optional feedbackUi service keeps this change local while leaving dialog state and submission in the feedback plugin.

Encode the category into the note text. A prefix in free text is not filterable without parsing and would leak into the verbatim note that telemetry uploads; a durable id in the payload is what a consumer can group by.

Submit the dialog through the command plane as /feedback <text>. The command rejects empty text, cannot carry a category, and writes an acknowledgement row the design replaces with a toast; the Remote records the same event with neither constraint.

Keep the note popover beside the dialog. Two editors for one note with different reachability would leave the row two-line at some widths, the defect the popover was introduced to avoid, and the design shows only the thumbs.

A Toast per message control. The composer overlay already mounts once per Session, and the dialog owns the toast sequence, so one owner serves both message-rating paths and the Session dialog.

A dialog kind in CommandUiSpec. An action that consumes the token and runs a client callback is all the dialog needs; the File row uses the same action kind, so one definition serves both entries.

Consequences

Adding a category means adding it to the union, to the Host tuple, to the dialog's chip record, and to the feedback dictionaries; the client bundle purity gate forbids a value import from a Host package, so the dialog restates the taxonomy as a Record<FeedbackCategory, true> whose key order is the chip order and whose completeness the compiler checks. The frozen released-v2 payload inventory still lists feedback/record as text only: it governs artifacts migrated from older generations, which cannot carry the new members, while equal-version restoration applies the installed vocabulary. The message-feedback-layout web scenario that pinned the popover's geometry is deleted with the popover. The message-feedback and feedback-release web goldens and the feedback subsystem doc changed in the same PR; the SDK feedback producer records a categorized Session remark and a categorized Dislike, so both SDK expected outputs carry the new members.

中文

问题

Web 客户端有两条互不相连的反馈路径,且都没有可见结果。/feedback <text> 记录一条 Session 备注并在转录里渲染一行确认;赞踩对立即记录评分,自由文本则通过锚定在该行下方的备注浮层填写。两条路径都不告诉用户提交了什么、去了哪里,都不收集分类,而点踩这个用户最愿意解释的场景什么也不问。Issue #3515 及其设计稿要求:一个弹窗,可从输入框菜单、不带文本的 /feedback 和点踩三处打开,带七个固定分类、可选描述、成功 toast,以及已记录评分的实心图标;点赞保持立即记录。

决策

command-feedback 在其客户端可用的 ./types 导出中以 FeedbackCategory 联合类型与 FEEDBACK_CATEGORIES 元组拥有分类表,feedback/record 变为 { text?, category? }:空白文本记为缺省,两个成员都没有的条目仍会记录,因为反馈所授权的日志投递本身就是内容。同一个包通过 TypertRemoteService 发布 sessionFeedback.record Remote,按 id 找到 live Session 后调用已有的 recordFeedback 生产方,因此弹窗记录的是与命令相同的事件,只是没有命令簿记。message-feedback 给 MessageFeedbackItem 与 MessageFeedbackPutRequest 加上可选 category,按元组校验已存值,并把分类变化算作实质编辑。

ui-message-feedback 成为 Web 反馈界面。每个 Session 一个 FeedbackSurface,拥有消息反馈控制器、负责草稿、提交与 toast 序号的 FeedbackDialogController,以及两者之间的路由:消息目标经消息控制器 put 一条带弹窗分类与备注的所选评分,Session 目标经 ctx.remote.sessionFeedback 记录。conversation.input.overlay 的 FeedbackDialog 条目从弹窗 store 渲染 Modal 与 Toast 基元。宿主 feedback 命令上的装饰让菜单选中或不带参数的回车为 Session 打开弹窗,而 /feedback <text> 仍到达宿主;它使用 CommandUiSpec 中的 action 种类:裸调用消费触发 token 后运行一个客户端回调,不提交任何内容。后续的对称消息反馈提交决策拥有评分入口规则:任一未记录的评分都会打开弹窗,再次点击已记录的评分则撤回。备注浮层、clearNote 与 clear 继续保持移除,因为弹窗是唯一的备注编辑器。

会话标题栏的更多操作菜单也通过 feedbackUi.openSession Client 服务打开 Session 弹窗。菜单仍由 session-log-export 持有,现有的逐 Session 反馈控制器持有草稿与提交。一个可观察值跟踪可选反馈插件的可用状态,因此卸载反馈插件只移除其菜单行,导出仍然可用。打开或关闭表单不写反馈事件;点击提交才记录一条 Session 反馈。

弹窗是共用的 Modal 卡片,宽度按设计稿;设计稿里「包括当前对话的日志」复选框不做,因为日志随每个反馈事件一起投递,不是可选项。超长描述仍在提交时以 note-too-large 失败;弹窗保留草稿并通过警告 toast 展示本地化错误。

考虑过的替代方案

为标题栏菜单动作声明子 slot。 slot 可以让每个插件持有自己的菜单行,但需要共享菜单项 API,超出 #4445 所需反馈入口的范围。可选的 feedbackUi 服务将改动限制在局部,同时让弹窗状态与提交继续由反馈插件持有。

把分类编进备注文本。 自由文本里的前缀不解析就无法过滤,还会混进遥测上传的原样备注;载荷里的持久 id 才是消费方能分组的东西。

让弹窗经命令平面以 /feedback <text> 提交。 命令拒绝空文本、带不了分类,还会写一行设计稿已用 toast 取代的确认;Remote 记录同一个事件且没有这两个约束。

在弹窗之外保留备注浮层。 同一条备注有两个可达性不同的编辑器,会让该行在某些宽度下变成两行,正是当初引入浮层要避免的缺陷,而且设计稿只有两个拇指。

每个消息控件各自一个 Toast。 输入框浮层已经按 Session 挂载一次,弹窗又拥有 toast 序号,因此一个持有者同时服务两种消息评分路径与 Session 弹窗。

在 CommandUiSpec 里新增 dialog 种类。 一个消费 token 后运行客户端回调的 action 已经够用;「文件」行使用同一个 action 种类,一份定义即可服务两个条目。

后果

新增分类意味着把它加进联合类型、宿主元组、弹窗的标签记录和 feedback 词典;客户端打包纯度门禁止从宿主包做值导入,因此弹窗以 Record<FeedbackCategory, true> 重述分类表,键的顺序就是标签顺序,完整性由编译器检查。冻结的已发布 v2 载荷清单仍把 feedback/record 列为仅有 text:它管辖从旧代际迁移来的产物,那些产物不可能携带新成员,而同版本恢复应用的是已安装词汇。固定浮层几何的 message-feedback-layout Web 场景随浮层一起删除。message-feedback 与 feedback-release 的 Web 期望输出和反馈子系统文档在同一个 PR 中更新;SDK 的反馈生产方会记录一条带分类的 Session 备注和一条带分类的差评,因此两个 SDK 期望输出都携带新成员。