Skip to content

W1: decision blocks render inline in chat (read-only) (#2150) - #2418

Merged
jaylfc merged 2 commits into
devfrom
exec/tsk-4micka
Aug 16, 2026
Merged

W1: decision blocks render inline in chat (read-only) (#2150)#2418
jaylfc merged 2 commits into
devfrom
exec/tsk-4micka

Conversation

@jaylfc

@jaylfc jaylfc commented Aug 15, 2026

Copy link
Copy Markdown
Owner

CARD TITLE (intent, not commit subject): W1: decision blocks render inline in chat (read-only) (#2150)

Autonomous build of board card tsk-4micka.

When an agent raises a decision via request_decision over the openclaw chat
bridge, attach a {kind: decision, decision_id} content block to the in-flight
chat message (using the existing content_blocks convention) so the decision
question surfaces inline in the conversation. The MessagesApp DecisionBlock
component fetches the decision from /api/decisions/{id} and renders it
read-only: the question text, the options as disabled buttons, the decision
type, and the current state (open, or answered with the chosen option plus
answerer and timestamp). Re-opening a thread re-fetches the decision, so an
already-answered question renders in its resolved state. Decisions raised
outside a chat context do not produce a block.

Files:
desktop/src/apps/MessagesApp.tsx | 221 ++++++++++++++++++++-
.../apps/chat/tests/render-helpers.test.tsx | 29 ++-
.../components/tests/DecisionBlock.test.tsx | 215 ++++++++++++++++++++
tests/test_bridge_session.py | 107 ++++++++++
tests/test_chat_messages.py | 34 ++++
tinyagentos/bridge_session.py | 47 +++++
tinyagentos/chat/message_store.py | 20 ++
8 files changed, 666 insertions(+), 8 deletions(-)

When an agent raises a decision via request_decision over the openclaw chat
bridge, attach a {kind: decision, decision_id} content block to the in-flight
chat message (using the existing content_blocks convention) so the decision
question surfaces inline in the conversation. The MessagesApp DecisionBlock
component fetches the decision from /api/decisions/{id} and renders it
read-only: the question text, the options as disabled buttons, the decision
type, and the current state (open, or answered with the chosen option plus
answerer and timestamp). Re-opening a thread re-fetches the decision, so an
already-answered question renders in its resolved state. Decisions raised
outside a chat context do not produce a block.
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@jaylfc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

Limit details: You’ve used all 2 included reviews currently available under your plan.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4ca01cbb-9f8f-4d47-8d00-c2089681f324

📥 Commits

Reviewing files that changed from the base of the PR and between 6f36d8c and fe8a695.

📒 Files selected for processing (8)
  • changelog.d/2150-decision-blocks-inline.md
  • desktop/src/apps/MessagesApp.tsx
  • desktop/src/apps/chat/__tests__/render-helpers.test.tsx
  • desktop/src/components/__tests__/DecisionBlock.test.tsx
  • tests/test_bridge_session.py
  • tests/test_chat_messages.py
  • tinyagentos/bridge_session.py
  • tinyagentos/chat/message_store.py

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

Comment thread desktop/src/apps/MessagesApp.tsx Outdated
key={opt.value}
type="button"
disabled
aria-pressed={false}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: aria-pressed={false} on disabled option buttons

aria-pressed is intended for toggle buttons, not regular buttons. Using it on disabled option buttons could confuse assistive technologies. Consider removing this attribute since the buttons are already non-interactive via disabled.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

...(data.edited_at !== undefined && { edited_at: data.edited_at }),
...(data.metadata !== undefined && { metadata: data.metadata }),
}
? {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Indentation inconsistency in message_edit ternary operator

The ? is indented with 15 spaces while the condition on the previous line has 16 spaces. This is inconsistent with other ternary operators in the file (e.g., message_delete at line 1084 aligns ? with :). Consider aligning the ternary operator for readability.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


Returns the updated blocks list, or None if the message was not found.
"""
async with self._db.execute(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: append_content_block not wrapped in a transaction

The SELECT and UPDATE are not atomic. Between reading content_blocks and writing the updated value, another coroutine could modify the same row, leading to lost updates. Consider wrapping the read-modify-write in a transaction.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

if tool_name == "request_decision":
decision_id = _extract_decision_id(body.get("result"))
if decision_id and self._chat_messages and self._chat_hub:
msg_id = session._pending_msg_ids.get(trace_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: trace_id mismatch could prevent decision block attachment

If a tool_result for request_decision arrives without a trace_id (or with a different one than the originating delta), the lookup session._pending_msg_ids.get(trace_id) returns None and the decision block is never attached. Consider using the openclaw message id or falling back to looking up the most recent pending message for the agent.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 15, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/chat/message_store.py 295 json_insert silently corrupts content_blocks to NULL on invalid JSON

SUGGESTION

File Line Issue
tinyagentos/chat/message_store.py 290 Docstring contradiction — describes old read-modify-write behavior
Files Reviewed (1 file)
  • tinyagentos/chat/message_store.py - 2 issues

Fix these issues in Kilo Cloud

Previous Review Summary (commit f09c31c)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit f09c31c)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
desktop/src/apps/MessagesApp.tsx 574 aria-pressed={false} on disabled option buttons
tinyagentos/chat/message_store.py 290 append_content_block not wrapped in a transaction
tinyagentos/bridge_session.py 457 trace_id mismatch could prevent decision block attachment

SUGGESTION

File Line Issue
desktop/src/apps/MessagesApp.tsx 1064 Indentation inconsistency in message_edit ternary operator
Files Reviewed (3 files)
  • desktop/src/apps/MessagesApp.tsx - 2 issues
  • tinyagentos/chat/message_store.py - 1 issue
  • tinyagentos/bridge_session.py - 1 issue

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 178.1K · Output: 45.4K · Cached: 1.9M

@jaylfc

jaylfc commented Aug 15, 2026

Copy link
Copy Markdown
Owner Author

nemotron-super review

VERDICT: No blocking issues found.

  • No blocking issues found

Automated first-pass review by the nemotron-super lane. The lead still reviews before merge.

@jaylfc

jaylfc commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

Reviewed. CI is green on 22 checks, but green describes CI and not the review surface, so I read kilo's findings and checked each against the source. Two are real and I am treating them as must-fix; two are not blocking.

Must-fix 1: lost-update race in append_content_block

chat/message_store.py does SELECT, mutate in Python, UPDATE, with no transaction around it:

SELECT content_blocks FROM chat_messages WHERE id = ?
blocks = json.loads(...); blocks.append(block)
UPDATE chat_messages SET content_blocks = ? WHERE id = ?

Two decision blocks appended to the same message concurrently will interleave and one is silently lost. There is no error and no log line, so the failure looks like a block that simply never rendered, which is the hardest kind to chase later.

Either wrap the read-modify-write in a transaction, or push the append into SQL so it is atomic in one statement.

Must-fix 2: aria-pressed on buttons that are not toggles

MessagesApp.tsx:574 sets aria-pressed={false} on a plain disabled button rendering a read-only decision option. aria-pressed declares a toggle button, so assistive tech announces these as unpressed toggles the user could activate, when they are inert display. Remove the attribute; disabled already conveys non-interactivity.

Calling this must-fix rather than a nit because correct ARIA on new UI is a standing requirement here, and this is new markup rather than pre-existing drift.

Not blocking

The ternary indentation at line 1064 is a formatting nit, deferrable.

The trace_id mismatch flagged at bridge_session.py:457 I have NOT confirmed. The concern is that a tool_result arriving without a matching trace_id leaves the decision block unattached with no fallback. That may well be correct, but I did not trace the delta and result paths far enough to say so, and I am not going to assert a finding I have not verified. Worth a look when the two above are addressed.

Otherwise the change is well scoped and the tests are substantial. Fix the two and I will re-review.

@jaylfc
jaylfc marked this pull request as draft August 16, 2026 09:56
@jaylfc

jaylfc commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

Converting to draft, and correcting a bad path in my own review above

Both must-fixes are still present. I re-verified each against the branch just now rather than inferring from the PR being untouched.

lost-update race   tinyagentos/chat/message_store.py:285-303   STILL PRESENT
                   SELECT -> json.loads -> blocks.append -> UPDATE, no transaction
aria-pressed       desktop/src/apps/MessagesApp.tsx:574        STILL PRESENT
                   aria-pressed={false} on a `disabled` non-toggle button

I am converting this PR to a draft. Not a comment on the work. Every check here is green, and a green rollup describes CI, never the review surface, so this PR would merge by accident on a routine sweep. A draft cannot. It is the same mechanical block I applied to #2415 for the same reason, and it reverses in one click the moment the two fixes land.

Correction to my review comment above

I cited the ARIA finding as desktop/src/apps/MessagesApp/MessagesApp.tsx:574. That path does not exist on this branch. The real file is desktop/src/apps/MessagesApp.tsx:574 - flat, with no MessagesApp/ directory. The line number was right; I invented a directory level.

Worth stating plainly because a wrong path in a review costs you a failed command before you can start, and I would rather correct it than have you discover it. I caught it only because I re-checked the finding with a control: my first grep of the wrong path returned nothing, which would have read as "already fixed" had I not confirmed the grep could find aria-pressed elsewhere in the branch first.

What unblocks this

Either fix is independently landable:

  1. Wrap the read-modify-write in a transaction, or push the append into a single atomic SQL statement.
  2. Remove aria-pressed from that button. disabled already conveys non-interactivity, and aria-pressed announces it to assistive tech as an unpressed toggle the user could activate.

Push both and mark ready for review, and I will re-check and merge.

…rt options

append_content_block did SELECT, mutate in Python, UPDATE with no transaction,
so concurrent appends interleaved and every write but the last was silently
lost. A new test appends 12 blocks concurrently: against the old code exactly
one survived, so this was not a rare interleaving but the normal outcome under
any concurrency.

The append now happens in a single UPDATE via json_insert, which is atomic in
the database rather than only within one process. The same file already
serialises reaction updates with _reaction_lock for this exact hazard, so the
risk was known here; content_blocks just never got the same treatment.

Also removes aria-pressed from the read-only decision option buttons. They are
disabled and inert, and aria-pressed declares a toggle, so assistive tech
announced them as unpressed toggles the user could activate. disabled already
conveys non-interactivity.
@jaylfc

jaylfc commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

Both must-fixes are done. I completed them myself, and my previous comment was wrong to ask you to.

I closed the last comment with "push the fixes and mark ready for review". There is nobody to do that. Lanes here are one-shot: they post [REVIEW] and exit, so the session that built this PR was already gone when I wrote it. A comment asking for a re-push that cannot happen is how a PR sits untouched for days. Small findings are the lead's to complete, and these were small, so they are done at fe8a6957.

The race was much worse than I reported it

I described it as two concurrent appends interleaving and losing one. I wrote a test that appends 12 blocks concurrently and ran it against the unfixed code first:

assert len(fetched["content_blocks"]) == count
E  AssertionError: assert 1 == 12
E   +  where 1 = len([{'kind': 'text', 'text': 'b11'}])

Eleven of twelve blocks lost. Only the last writer survived. This is not a rare interleaving that needs bad luck, it is the normal outcome whenever appends overlap, because every one of them reads the same array before any of them writes. Correcting my own severity estimate upward: I called it a lost-update race, which understated it.

The fix, and why this shape

The append is now a single UPDATE ... json_insert(...), so it is atomic in the database rather than merely within one process.

Worth knowing: this file already defends against this exact hazard elsewhere. ChatMessageStore.__init__ holds self._reaction_lock = asyncio.Lock(), commented "Serialises concurrent reaction read-modify-write operations". So the read-modify-write risk was understood here and content_blocks simply never got the same treatment. I used the SQL form rather than a second lock because a lock only holds within one process, while json_insert also holds if two workers ever share the database.

The three pre-existing append tests still pass unchanged, so the return value, the None-for-missing-message contract and block ordering are all preserved.

Verification, including what I did NOT run

tests/test_chat_messages.py    28 passed   (3 pre-existing append tests + the new race test)
tests/test_bridge_session.py   16 passed

Both run against the worktree copy, asserted rather than assumed: I confirmed tinyagentos.chat.message_store.__file__ resolved into the worktree before trusting a single result.

I did not run the desktop suitenode_modules is not installed in my worktree and I would rather say so than imply coverage I do not have. The ARIA change is a one-line attribute removal and no test asserts aria-pressed on these components (checked DecisionBlock.test.tsx and render-helpers.test.tsx), so CI is the verification for that half.

Still open, unchanged and non-blocking

The ternary indentation at line 1064, and the trace_id concern at bridge_session.py:457 which I flagged as not confirmed and still have not confirmed. Neither blocks the merge.

Taking this out of draft once CI comes back green.

@jaylfc

jaylfc commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

Resolving the last finding, then merging. All four of kilo's are now closed out.

I had flagged the trace_id concern as not confirmed and I was not willing to merge past my own open question, so I checked it.

The mechanism is real. bridge_session.py:245 reads trace_id = body.get("trace_id") or _new_id(), so a payload with no trace_id gets a fresh random one, and every lookup keyed on it misses. Both consumers are guarded by if msg_id:, which makes a miss a silent no-op with no exception and no log line.

But it is pre-existing and this PR does not make it worse. That line is byte-identical on origin/dev, and the already-shipped delta path at :286 performs the same _pending_msg_ids.get(trace_id) lookup. So the decision-block consumer added here inherits exactly the reliability of the path beside it rather than lowering it. Not a regression, and not grounds to hold this PR.

Carded as tsk-hpee75, deliberately written to demand reachability evidence before any code change: if the bridge client never omits trace_id, a guard here would be inert code and must not be written. "Measured, cannot happen" closes that card successfully.

Final state of kilo's four

MessagesApp.tsx:574        aria-pressed on an inert button      FIXED  fe8a6957
message_store.py:305       lost-update race                     FIXED  fe8a6957 (11 of 12 blocks
                                                                       lost before the fix)
bridge_session.py:457      trace_id mismatch                    REAL, PRE-EXISTING -> tsk-hpee75
MessagesApp.tsx:1063       ternary indentation                  nit, deferred

What I am merging on, stated plainly

20 checks green, no reds, no cancellations, on a settled run. CodeRabbit did not review this PR at all — no review from it exists, so I am not claiming bot coverage I do not have. I am merging on kilo's review plus my own line-level review, with every one of kilo's findings either fixed in code or adjudicated above with the evidence.

Merging.

@jaylfc
jaylfc marked this pull request as ready for review August 16, 2026 10:25
@jaylfc
jaylfc merged commit 132c690 into dev Aug 16, 2026
22 checks passed

Returns the updated blocks list, or None if the message was not found.

The append happens in a single UPDATE so it is atomic. Reading the array

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Docstring contradiction — describes old read-modify-write behavior

TheFirst sentence correctly states the append is atomic, but the following
sentences ("Reading the array into Python, appending and writing it back lets
two concurrent appends interleave...") describe the previous non-atomic
implementation. This docstring is now misleading for future maintainers.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

interleave, and the loser is dropped with no error and no log line.
"""
cursor = await self._db.execute(
"UPDATE chat_messages SET content_blocks = json_insert("

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[WARNING]: json_insert silently corrupts content_blocks to NULL on invalid JSON

If the existing content_blocks value is not valid JSON (e.g. from a prior
bug or manual edit), json_insert returns NULL and the UPDATE sets the
column to NULL, silently losing data. The previous implementation raised
json.JSONDecodeError on the same input, making corruption visible.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant