Skip to content

fix(dashboard): handle User action requests from the needs-you drawer - #5571

Open
loopx-agent wants to merge 2 commits into
mainfrom
codex/attention-user-action-handling
Open

loopx-agent wants to merge 2 commits into
mainfrom
codex/attention-user-action-handling

Conversation

@loopx-agent

Copy link
Copy Markdown
Collaborator

Delivery brief

  • Goal / source: owner report: "I should be able to properly handle this gate." A user_action item in the personal workspace's needs you drawer could not be handled.
  • Current gap: for a non-decision User Todo (task_class=user_action), the drawer only showed "This request asks for a reply or an action, not an approval…" and an Explain this decision button. That button was dead: onExplainDecision was declared on PersonalWorkspaceCallbacks but no caller ever provided it. The owner could not mark the item done, reply, defer it, or close it, and the heading called it a decision.
  • Observable result: the owner can handle the item in place through the existing typed owners, with one confirmation each and a readback.
  • Owning boundary: the existing todo.update / gate.resolve Chat actions and the TS User completion owner (loopx/control_plane/todos/user_completion.ts). This adds no new action kind and no Python decision logic.
  • Decisive evidence: a packaged-browser scenario covering failure → recovery → confirm → readback, plus real ChatActionService tests that send the drawer's exact payloads to legacy, file and sqlite authorities.

Journey (before → after)

Before After
Mark done Not possible in the App 我已完成 / I've done this → confirm once → item leaves needs-you
Reply "Ask the Agent in conversation" (no link) 在对话中回复 / Reply in conversation opens that Goal's chat with an editable draft (关于「…」:). Nothing is sent automatically.
Defer Not possible Other options → 暂缓到明天 9:00 (todo.update defer, resume_when=resume_at:<tomorrow 09:00 local, tz-aware>)
Not needed Not possible Other options → 不再需要 / No longer needed (gate.resolve decision=cancel, which the User completion owner admits for a user_action; it closes only the reminder and never resumes or approves other work)
Explain Dead "解释此决定" button 请 Agent 说明 / Ask the Agent to explain: drafts an explanation request in the Goal chat. Decisions keep "Explain this decision", now wired the same way.
Preview failure Unhandled promise (silent) Inline alert: "未能准备这项变更:… 没有保存任何内容,可以重试". Buttons are disabled while the preview is pending.
No stable todo_id Same dead button Shows why it can't be completed here; reply/explain stay available
Read-only source Nothing Read-only explanation; no write affordance

User gates (user_gate) still show approve / reject / withdraw unchanged; only their preview errors are now surfaced.

Changes

  • attention-actions.tsx (new): all owner actions for one attention item (decision, User action, fallback note), with pending/error state. These actions moved out of context-drawer.tsx, which keeps shrinking (bounded future-facing refactor: one module now owns attention write affordances).
  • attention-details.ts: interaction now mirrors the typed task_class (decision | user_action | unknown). Prose never classifies an item. Adds todoIdentified, canHandleUserAction, and nextMorningResumeWhen.
  • personal-workspace-page.tsx: provides onReplyToAttention and a default onExplainDecision that open the item's Goal chat with a draft. The draft waits until that Goal's composer is visible.
  • i18n.tsx: matching English and Simplified Chinese copy.

Vocabulary: AttentionDetails.interaction extends a local display union that mirrors the registered Todo task_class vocabulary (loopx/control_plane/todos/summary_lanes.ts, contract.py). It creates no new shared contract. generate_semantic_inventory.py --changed-from HEAD reports no new carriers. The probe covers only loopx/, so the dashboard union was reviewed by hand.

Behavior change disclosure: a user_action row's interaction was unknown and is now user_action. The existing smoke assertion "prose never classifies interaction" was updated to keep its real invariant: only the typed class classifies, and approval wording never grants approve/reject.

Entrypoints

  • Frontend (packaged personal workspace): needs-you drawer from the Goal Tasks board and the manager needs-you lane. Changed and verified.
  • CLI / Lark: unchanged. The same todo.update / gate.resolve owners already serve them.

Validation (exact head)

  • npx tsc --noEmit: pass
  • npm run smoke:attention-details (TZ Asia/Shanghai, America/New_York, Europe/London): pass
  • personal-workspace-contract.test.mjs: pass
  • pytest tests/control_plane/test_chat_user_action_handling.py tests/control_plane/test_chat_gate_decisions.py tests/presentation/test_chat_bundle.py: 38 passed
  • Packaged bundle built with scripts/chat_bundle.py build --install, then:
    • dashboard-attention-details-browser-smoke.mjs (packaged, zh/en/mobile/read-only): pass
    • personal-workspace-browser-smoke.mjs packaged, all 42 scenarios, including the new attention-user-action: pass
  • loopx canary premerge --from-git-diff: passed. One inherited advisory: control-plane-maintainability-ratchet-smoke.py, a known baseline failure that does not mention the changed files.

The new browser scenario uses a synthetic fixture and never touches real Goals. It checks:

  • no approve/reject/"explain this decision" on a User action;
  • an injected preview failure shows an alert with no write, and the same click recovers;
  • the preview is todo.update complete on the right todo_id, and nothing is written before confirmation;
  • confirmation writes exactly once, and the item leaves needs-you on readback;
  • defer sends a valid future resume_at, and no-longer-needed sends gate.resolve cancel;
  • a row without an id offers no write, and reply drafts without sending or previewing;
  • the User gate still offers approve.

Visual evidence: before/after screenshots of the drawer (initial state, other options, preview failure, confirm, board readback, missing-id state, reply draft) were captured from the packaged build and shared with the owner. They are not committed.

Remaining gaps

  • Hard-lease Goals: the canonical owner currently rejects todo.update defer on a User Todo at preview ("requires the current active lease execution proof"). The drawer shows that error and writes nothing. Done and no-longer-needed work under hard lease (covered by tests). Whether User-Todo deferral should need a lease is a control-plane owner question and is out of scope here.
  • Re-surfacing: I did not verify that a deferred user_action returns to needs-you automatically when its resume_at arrives. Meanwhile it stays visible as deferred in the Tasks view.
  • No completion note: "I've done this" sends no note, because on the canonical user-update path note would replace the request's original reason. Replies go through the conversation draft instead.

Future-facing refactor pass: applied. Attention write affordances moved into attention-actions.tsx, and decision preview errors now share the same surfaced state.

Not self-merged: this changes apps/** product behavior and is left for the maintainer.

loopx-agent and others added 2 commits October 4, 2026 19:44
… drawer

A user_action attention item only offered a note and an 'Explain this
decision' button whose callback was never wired. The drawer now offers
'I've done this' (todo.update complete), 'Reply in conversation' (drafts
an editable message in the Goal chat), and Other options: defer until
tomorrow 09:00 (todo.update defer with resume_at), 'No longer needed'
(gate.resolve cancel, the outcome the User completion owner admits for a
user_action), and 'Ask the Agent to explain'. Preview failures surface
inline and can be retried; read-only sources and rows without a stable
todo_id explain why the action is unavailable. Attention owner actions
move from context-drawer.tsx into attention-actions.tsx.

Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Semantic smoke for user_action classification, stable-id and lifecycle
fencing and the defer preset; a browser scenario for done with preview
failure/recovery and readback, defer, no-longer-needed, reply drafting
and the missing-id state; and real ChatActionService coverage of the
exact drawer payloads across legacy, file and sqlite authorities.

Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@mergify

mergify Bot commented Oct 4, 2026

Copy link
Copy Markdown

This pull request has merge conflicts with main and cannot be merged
until they are resolved. Please rebase or merge the base branch, @loopx-agent.

Choose the remote for the base repository, not an out-of-date fork.
For a fork clone, first inspect git remote -v; upstream must point
to https://github.com/loopx-project/loopx.git. If it is absent, add it
with git remote add upstream https://github.com/loopx-project/loopx.git.
Then run:

git fetch upstream
git rebase upstream/main
# Resolve each conflict, git add the resolved files, then git rebase --continue.
git push --force-with-lease origin HEAD

For a same-repository clone whose origin points to
https://github.com/loopx-project/loopx.git, use origin instead of
upstream for fetch/rebase. If you prefer merging the base, use
git merge <base-remote>/main and push normally.

Keep the DCO Signed-off-by trailer on every commit when you rebase.
https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase Mergify: the pull request has merge conflicts with its base branch label Oct 4, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase Mergify: the pull request has merge conflicts with its base branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant