feat(app): let the conversation lead the Goal Chat view - #5077
Conversation
Scheduled checks, heartbeats and routine runs fold into one Background work line. Drafts use owner-facing kind and status labels instead of raw action strings, and only the newest draft awaiting the owner stays open. Needs-you items, waiting or failed runs and non-draft proposals remain visible. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
7c7df22 to
3afdfe4
Compare
There was a problem hiding this comment.
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
Exact head: 3afdfe4504d468582d0a1e1a3f76e97336629627; base: 60e1d030a725e212e49a889324a016bb8c052053 on codex/app-honest-work-surface.
动机
按 App conversation RFC 与现行 design 标准,Goal Chat 应把用户对话、结果和待处理决定放在主要位置,后台运行及 schedule 可保持安静。此 PR 折叠后台工作并把 action/status 显示为中英文用户用语,方向成立。必须同时保证重载之后主视图仍展示真正的最新草稿。
改动思路
沿用现有 ChannelTimeline、typed action adapter、drawer 和 i18n owner,仅改变展示投影,不添加动作执行或 schedule 状态写入。保留失败、中断、等待及非 ready 提议的既有入口,后台项可展开后继续管理。新增 ready 提议折叠目前依赖数组位置,与实际存储列表顺序冲突。
具体改动
关键代码讲解
已阅读全部 6 个变更文件,以及 workspaceProposal、dedupeProposals、list restore、schedule producer、action store 排序与抽屉调用。
ChannelTimeline 根据既有 typed status 选择后台项和 ready 草稿,再由原 row 进入 drawer;workspaceProposal 将存储记录转成展示模型,但没有保留用于比较新旧的时间;ChatActionStore.list 则按权威 updated_at 新到旧排序。这三个可达符号之间的顺序假设造成下面的回归,i18n/CSS 本身没有改变动作权限。
[P2] 重载后把最新草稿折叠、留下最旧草稿。 channel-timeline.tsx:165–167 的 slice(0, -1) 假设最后一条是最新提议。然而 ChatActionStore.list 按 updated_at 降序返回,restore 保持该顺序,adapter 也没有提供用于排序的时间。两条不同目标的 ready 草稿按真实 /api/actions 返回的 [newest, oldest] 进入 Goal Chat 时,新草稿进入“另有 1 个待确认提议”,旧草稿留在主视图;只倒置展示顺序就会倒置结果。用户重载或重新进入 Goal 后,会优先看到过时的可确认动作。
最小修复:在现有 typed adapter 保留/使用权威 proposal 时间或等价的稳定新旧关系,显式选出最新 ready 提议;不要让恢复顺序或插入顺序决定“最新”。补充真实 store 的 newest-first 列表恢复、刷新/重新进入、后续新建草稿及顺序倒置的浏览器断言,确保最新仍可见、其余仍可通过键盘展开进入 drawer。
本地验证:Dashboard build/typecheck、开发与打包版各 17 个现有浏览器场景通过;Goal order、delegation preflight、workspace theme、workspace contract smokes、diff 检查及 6 文件 public boundary scan 通过。额外用真实本地 action store 与 HTTP 路由取得两条持久化 preview 的列表,再将相同返回值送入仓库原生浏览器 fixture:base 两条均可见;head 在 newest-first 顺序下隐藏最新;倒序后隐藏最旧。开发版与打包版一致。桌面/移动首屏均观察到旧草稿留在主要区域;键盘展开及 drawer 路径仍有效。该分层验证没有执行 monitor 或改变生产 Goal 状态,也不宣称整个后端动作流程已通过 live 验收。
对主干的风险
语义与 CI 对齐
具体回归发生在已有 typed-action 恢复路径,影响后续访问和累积草稿的用户体验,现有 17 场景缺少这一排序反例,因此正常场景全绿不能覆盖它。默认展示变化已在 PR 中说明;没有 default-off 承诺,没有新增权限、公共协议或第二状态 owner。类型状态的显式匹配方向正确,但排序判断必须回到其权威数据。
未来改动准备已应用于统一 i18n action/status 标签;排序应作为同一展示边界内的修复,不需要新框架。完整异步 App 协作/结果返回仍是既有后续工作。首屏 owner presentation approval 未由这次技术评审认证,仍需按项目原有 gate 保留。
我的整体评价
REQUEST_CHANGES:展示压缩是有价值的范围,但最新草稿恢复顺序已出现可复现的回归。先修复上面的 P2 并加入重载排序反例,再对整个不变 head 复评。没有合并或自合并授权。
English verdict: REQUEST_CHANGES
`ChannelTimeline` folded every ready draft except the last one in the array and kept that one on the first screen. The array order is not recency: the store returns proposals newest-first by (`updated_at`, `proposal_id`), the workspace restores exactly that order, and a draft created in this session is appended last. A restored workspace therefore folded the newest draft and offered the oldest one -- the opposite of the intended "the conversation leads" reading, and the card the owner would confirm first was the stale one. Read recency from the record instead of the position. The stored times now reach the row through the typed adapter, a focused `proposal-recency.ts` owns the comparison (mirroring the store's own tie-break so the answer matches a real list), and `dedupeProposals` keeps the newest record for a subject rather than whichever duplicate happened to be last. The team-plan fixture gave two stored drafts the same timestamp, which real stored actions never do and which made the Goal view nondeterministic; the manager-channel plan is now explicitly the earlier draft. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
The browser fixture answered `/api/actions` in insertion order, which no real `ChatActionStore.list` ever returns, so the seventeen scenarios could pass while the workspace read the wrong draft as newest. Sort the fixture's list response by (`updated_at`, `proposal_id`) newest first, exactly as the store does. Add a `newest-draft` scenario over two real stored drafts: a restore in store order, a reload and re-entry, a list served deliberately out of contract order, and a draft created in this session appended last. Each case asserts the newest draft keeps the conversation, the older one is folded with its count, and the fold still opens the drawer by keyboard. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
There was a problem hiding this comment.
Approval conclusion (author-owned PR; GitHub blocks formal self-approval)
详细中文评审
结论:APPROVE。Exact head:727b9de1470ff192518337748bd2b7441a9b90b6;base:60e1d030a725e212e49a889324a016bb8c052053(codex/app-honest-work-surface)。上一轮评审提出的 P2 已修复并补上真实反例,首屏展示也按项目首屏 gate 取得了 owner 确认。
动机
按 App conversation RFC 与 design 标准,Goal Chat 应把用户对话、结果和待处理决定放在主要位置,后台运行与 schedule 收进可展开的安静区域,action/status 用用户语言展示。上一轮指出:这种“一条待确认草稿领衔、其余折叠”的展示必须保证重载后领衔的是真正最新的草稿。原实现用 readyProposalItems.slice(0, -1) 折叠数组末尾之前的所有草稿,而 ChatActionStore.list 是按 updated_at 倒序返回、restore 原样保留该顺序的,于是最新草稿被折叠、最旧草稿留在主视图;会话内新建草稿则被追加到末尾,同一个切片又给出了相反结果——展示结论实际由数组如何拼装决定。
改动思路
把“哪条更新”从位置假设改回记录本身。适配器把存储的 created_at/updated_at 带进行模型,新增聚焦模块 proposal-recency.ts 负责比较(按 store 自身的 (updated_at, proposal_id) 倒序,连并列时的次序都与真实列表一致),折叠逻辑改为按该规则选出最新一条、其余按同一规则倒序折叠;dedupeProposals 也从“后写覆盖”改为“保留最新记录”,避免同一修复出现两处不同强度。浏览器 fixture 原先按插入顺序返回 /api/actions,与真实 store 不符,正是 17 个场景全绿却测不出该回归的原因,因此改为按 store 顺序返回,并新增 newest-draft 场景覆盖恢复、刷新重进、会话内新建草稿以及故意倒序四种到达顺序。
具体改动
关键代码讲解
proposal-recency.ts:14 proposalRecencyKey / :23 compareProposalRecency:以(updatedAt ?? createdAt ?? "", previewId)为键,按 store 的字符串比较与并列次序倒序排列;缺时间时退化为确定性顺序而非抛错。channel-timeline.tsx:170 readyByRecency:[...readyProposalItems].sort(compareProposalRecency)后slice(1)即为折叠集合,首个即首屏草稿;折叠项仍按同一规则倒序渲染,foldedProposalIds继续驱动可见性过滤,applying/applied/failed/gated行完全不受影响。personal-workspace-page.tsx:78 dedupeProposals:同一 subject 下保留较新记录(compareProposalRecency(proposal, current) < 0),键与顺序不变。personal-workspace-page.tsx:694 workspaceProposal:透传updatedAt/createdAt;personal-workspace-model.ts增加两个可选字段。- 测试侧:
proposal-recency.test.mjs钉住恢复序、倒序、并列、宿主回调无时间与单条草稿;newest-draft.mjs通过真实入口断言首屏行、折叠计数与键盘展开进入 drawer;fixture.mjs改为按 store 排序;team-plan.mjs的两条草稿原本时间戳相同(真实存储不会如此),现明确区分先后。
对主干的风险
范围只在这块展示投影:不写状态、不改变 apply/cancel/regenerate/defer 的权限与路径,未新增持久化字段或公共协议,折叠的标记、文案与计数语义均未改动,因此没有权限或数据兼容风险。首屏可见变化只有“领衔的草稿换成最新那条”,已由 owner 通过 before/after 首屏预览确认。残余风险有两处:宿主直接返回且完全不含时间的 preview 会退化为确定性而非时间顺序(存储路径永远带时间,单测已钉住该回退);本轮未查询远端 CI(goal 策略 wait_for_ci=false)。验证覆盖:开发版与打包版各 18/18 浏览器场景通过(含 review 要求的 typed-actions/team-plan/steward-journey),smoke:proposal-recency、smoke:goal-order、smoke:delegation-preflight、theme 与 contract smoke、tsc --noEmit、build:chat + chat_bundle.py verify --source 均通过,loopx canary premerge --from-git-diff 直接检查与选中检查 0 失败,public boundary scan 干净,exact-diff quality receipt 有效。另附一个环境说明:本机用软链依赖树跑开发版时 conversation-input 会因 Vite 拒绝服务 Vite 根之外的字体而多出一条 403,把该依赖放进工作树后同场景通过,且在未修复的 head 上同样复现,与本次改动无关。
我的整体评价
APPROVE。problem_context 的 long_horizon 与 user_experience 均为 improved:重复进入 Goal 时不再随机退化到某一条历史草稿,首屏给出的是用户最近动过的那条,折叠计数与键盘可达性保持原样。observable_semantics 为 intentional_change_validated,并单独记录了本轮相对上一 head 的真实差异(领衔草稿改变、折叠形状不变)。repository_reuse 为 reused:复用既有 typed adapter、既有折叠标记与 i18n key,并把顺序规则收口到一处;scope_fit、code_volume、change_proportionality 通过(约 57 行生产与配置、独立 33 行规则模块、48 行单测与约 164 行浏览器 fixture/场景);authority_semantics 为 not_applicable,default_off_isolation 不适用(未新增可选能力)。合并授权来自维护者,本评审只说明该 exact head 的批准条件已经满足。
English verdict: APPROVE - exact head 727b9de on base 60e1d03. The reviewed P2 is repaired the way the review asked: recency now comes from the stored record instead of the array position, so a restore, a reload, a session-created draft and a deliberately reversed list all lead with the newest unconfirmed draft. The proof is real: the browser fixture now serves /api/actions in the store's actual (updated_at, proposal_id) newest-first order -- which is why 17 scenarios could pass while this was broken -- and a new newest-draft scenario fails on the reviewed head with the newest row folded, then passes here. dedupeProposals was made order-independent by the same rule, one fixture that gave two stored drafts identical timestamps was corrected, and the fold markup, wording, counts, gated/applied visibility and drawer keyboard path are unchanged. Validation: 18/18 scenarios in development and 18/18 packaged against the built bundle, tsc --noEmit, the recency unit smoke, the goal-order/delegation/theme/contract smokes, canary premerge with 0 direct and 0 run failures, a clean public boundary scan and a valid exact-diff quality receipt. The first-viewport presentation carries the owner's approval. Unrelated and non-blocking: with a symlinked dependency tree the development conversation-input scenario logs an extra 403 for a font outside the Vite root, identically on the unrepaired head. Merge authority comes from the maintainer, not from this review.
e11bc8d
into
codex/app-honest-work-surface
|
Merge decision (maintainer) Reviewed exact head Changed surfaces: Checks named in the review: 18/18 scenarios in development and 18/18 packaged, Failures and skips: remote CI was not consulted ( Why the coverage is enough: the diff changes which draft is rendered inline and the rule that decides it, with a unit smoke for the rule, a browser scenario that exercises four arrival orders through the real surface, and a mutation check proving the new assertions fail on the reviewed head. The unrelated proposal behaviors (failed recovery rows, gated rows, team-plan cards, monitor controls, drawer keyboard path) are covered by the existing scenarios, which still pass. First-screen gate: the owner approved the before/after first viewport before this head was pushed. Merge authority came from the maintainer, not from the review. |
Stacked on #5071. The Goal conversation is now the main content of the Goal Chat view. Background machinery no longer stacks up above it.
Delivery brief
docs/architecture/rfcs/app-conversation-and-async-inbox-v0.md) asks for the conversation to dominate the view, with delegation and returns shown inline.monitor.create · ready.channel-timeline.tsx). No change to the action, proposal or schedule contracts.What stays visible
These stay visible on purpose, and no text-based inference decides that something is safe to hide:
Only proposals whose status is
readyfold behind the newest one.Behavior change disclosure
data-action-kindfor tooling and tests. Thetyped-actions,steward-journeyandteam-planscenarios now select by that attribute and assert the owner-facing label.Entry points
Validation
tsc --noEmit: passed.npm run build, including the packaged chat bundle: passed.typed-actionsnow asserts that a schedule is hidden until Background work is opened, then drives the schedule drawer actions (run now, pause, edit, stop) from the fold.Future-facing pass
Applied: kind and status labels come from one i18n map (
proposal.kind.*,proposal.status.*) instead of string concatenation. Deferred: delegation and returned results inline in the conversation (async inbox) will be the next slice.First-screen gate
This changes the Goal Chat first viewport. The owner reviews before/after screenshots before merge; leave the merge to the maintainer.
Maintainer Repair At The Merged Head
Reviewed and merged head
727b9de1470ff192518337748bd2b7441a9b90b6on base60e1d030a725e212e49a889324a016bb8c052053, merged intocodex/app-honest-work-surfacease11bc8d146035ce9001b907dd2d51112b8190cedwith an identical tree.The review's P2 is fixed the way it asked. Recency now comes from the stored record instead of the array position: the typed adapter carries
created_at/updated_at, a focusedproposal-recency.tsowns the comparison (mirroringChatActionStore.list's(updated_at, proposal_id)descending order, ties included),ChannelTimelineselects the newest ready draft with it, anddedupeProposalskeeps the newest record for a subject instead of the last one. The fold markup, wording, count, gated/applied visibility and drawer keyboard path are unchanged.The proof had to be made real first: the browser fixture answered
/api/actionsin insertion order, which is why seventeen scenarios passed while the first screen offered the oldest draft. It now serves the store's actual order, and a newnewest-draftscenario covers a restore, a reload and re-entry, a deliberately reversed list, and a draft created in this session. Reverting the fold change makes that scenario fail with the newest row folded, so the assertion is not vacuous.First-Screen Presentation
Approved by the owner before this head was pushed, per the repository first-screen gate. Before → after at the same viewport, from the same two stored drafts:
Older pending draft: 调整发布前的验收清单led the first screen and the newest draft was folded.Newest pending draft: 确认候选版本的回滚条件leads, with另有 1 个待确认提议folding the older one.Desktop (1512) and mobile (390) captures were reviewed in both states.
Validation At The Merged Head
18/18 browser scenarios in development and 18/18 against the packaged chat bundle (
build:chatpluschat_bundle.py verify --source);smoke:proposal-recency,smoke:goal-order,smoke:delegation-preflight, theme and contract smokes;tsc --noEmit;loopx canary premerge --from-git-diffwith 0 direct and 0 run failures; public boundary scan clean; exact-diff change-quality receipt valid. Remote CI was not consulted (wait_for_ci=false). With a symlinked dependency tree the developmentconversation-inputscenario logs an extra 403 for a font resolved outside the Vite root; it reproduces on the unrepaired head and disappears once the dependency sits inside the worktree, so it is an environment artifact, not a PR regression.