PR 292: Complex operations planning, review round 1

rancher/rancher-ai-ui, by torchiaf, issue 293. Reviewed on 2026-10-29.

Preview: rancheraipr292planr.mf-created-dashboard.ui.rancher.space. Built from 52a3762, backend mf-rancher-16 (Rancher v2.16.0-head, agent v1.2.0-alpha.5 driven by llm-mock, the same agent image CI uses).

Verdict: request changes

The plan card works: rows per subtask, agent tags, the progress bar, collapse, the approval flow, Cancel Execution, the failed state with Retry options, and the new Chat settings. But:

1. The feature2. Bugs3. Minor4. What was checked5. Try it yourself

1. The feature

When the agent runs with PLAN_ENABLED=true (adaptive mode, two or more agents), it splits a request into subtasks, gives each one to an agent and streams <plan> events. The PR draws those events as a plan card above the answer:

Without approval

Prompt → plan card with two agents → collapse and expand → both rows checked → the answer below it → the new pin option in the chat menu.
Plan running
Running: the first task in progress, the second pending.
Plan done
Done: "All tasks have been completed".
Chat menu
Chat menu: "Unpin Planning Messages" (pinned by default).

With approval

Plan waits for approval → Start Execution → Cancel Execution while it runs → Completed → the note's "configuration" link opens Settings → Chat.
Awaiting approval
Awaiting approval, with the note under the card.
Running with Cancel Execution
Running: Cancel Execution.
Completed
Completed.
Chat settings
Settings → AI Assistant → Chat (bottom of the page).

What the UI receives (live websocket frames)

Planning without approval
     0 ms  →  {"prompt":"Create a namespace and a Pod in it", ...}
   201 ms  ←  <plan> status=in_progress  tasks=[in_progress, pending]
  3834 ms  ←  <plan> status=in_progress  tasks=[completed, pending]     ← no running task for 4 ms
  3838 ms  ←  <plan> status=in_progress  tasks=[completed, in_progress]
  6477 ms  ←  <plan> status=completed    tasks=[completed, completed]
  6480 ms  ←  Both tasks are done: ...
  6586 ms  ←  </message>

2. Bugs

BLOCKERConsole input is staged, not sent, while a plan is pending or running

The last commit (52a3762, "Stage sliding badge messages on pending plannings") makes ensureConnectionAndSendMessage stage the text back into the box whenever the plan is pending or in_progress. That function is also what the console's own Enter and Send call (@input:content), so everything typed during a plan stays in the box. Two flows break:

1a. Feedback on a plan awaiting approval. The note says "You can request changes before executing the plan above", and the agent treats any answer other than yes/no as feedback that regenerates the plan. The feedback never reaches the agent: no frame goes out, and the plan is never regenerated.

Approval ON. Type a change request → Enter → the text goes back into the box → Send → same. 0 frames sent.

1b. A subtask asks a question. When a child agent needs more information, the agent pauses the plan with that subtask still in_progress, and the user's next message is forwarded as the answer. The answer can't be sent. There is no Stop button (no response is pending) and no Cancel (approval is off), so the only way out is a new chat. The approval note even promises this case: "I might need your input on various decisions".

Approval OFF. The Rancher agent asks for the namespace name → type the answer → Enter / Send do nothing → stuck.
Feedback stuck in the box
1a: the feedback is still in the box, and the plan still awaits approval.
Answer stuck in the box
1b: the answer "demo-292" is still in the box, and the plan spins forever.

Suggested fix: apply the planning gate only to the sliding badge / banner messageBox path, which is what the commit was for. Let the console send as before (it already blocks Enter while a response streams).

BUGStop / Cancel Execution shows the generic stop notice. This is the failing e2e.

After Cancel Execution (or the console's Stop) during a plan, the notice is "You stopped the previous response.". The agent sent planning-manual-stop, so it should be "The execution of the previous plan has been stopped."

Approval ON → Start Execution → Cancel Execution → the generic notice.
Wrong stop notice
"You stopped the previous response." after a plan stop.

Why: notifyStopMessage only uses the plan text when the stored plan is still pending or in progress at </message>. But the agent always sends the cancelled <plan> first, so by then the status is already cancelled and the check falls back to message-manual-stop:

  5291 ms  →  {"prompt":"","tags":["stop"]}
  5344 ms  ←  <plan> status=cancelled  tasks=[pending, pending]           ← plan state becomes cancelled here
  5348 ms  ←  <stop-message>{"reason": "planning-manual-stop"}</stop-message>
  5350 ms  ←  </message>                                                ← isPlanningActive is already false

CI shows the same thing. Run 40885134621 at 52a3762 fails only Message Planning > Planning with approval > Execution stopped, at spec line 352: "Expected not to find content: 'You stopped the previous response' but continuously found it". The unit test "Should parse planning stop message with planning-manual-stop reason" never checks the notice text and never sends the cancelled frame, so it misses this.

Suggested fix: decide from the stop reason and whether this message got a cancelled plan, not from the plan status at </message>. Then make the unit test send the real frame order and assert the text.

BUGCollapsed header shows &#39;

When the card is collapsed while a task runs, the header is t('ai.planning.header.executing', { task }). t() HTML-escapes the arguments and the template prints the result as text, so a quote in the task name shows as &#39;. The expanded rows print the task directly and look fine. The planner often quotes names, e.g. cluster 'local'.

Collapse while the first task runs → "Executing: Create namespace &#39;demo-292&#39; …" → expand → the rows are fine.
Escaped quote in header
The collapsed header.

Suggested fix: call t() raw there (it is rendered as text, so that is safe), or build the label without passing the task through t().

3. Minor

4. What was checked

CaseSetupResult
Plan runs to completionapproval off / onOKRows, agent names (rancher → Rancher, fleet → Continuous Delivery), progress bar, final state
Collapse / expand while runningapproval offOKHeader shows the running task BUG 3
Start Execution / Cancelapproval onOK"yes" / "no" sent with the confirmation tag
Cancel Executionapproval onOKPlan cancelled BUG 2 wrong notice
Console Stop during a planapproval offOKPlan cancelled BUG 2 wrong notice
Subtask failsapproval offOKRed row, "Plan execution has been stopped", Retry / Restart / Request details options
Feedback while awaiting approvalapproval onBLOCKERNot sent
Answer to a subtask's questionapproval offBLOCKERNot sent, chat stuck
Chat settings sectionSettings → AI AssistantOKReflects PLAN_ENABLED / PLAN_APPROVAL_ENABLED from llm-config
Host compatibility (2.13+)@rancher/shell 3.0.8 to 3.0.14OK--rc-disabled-background, icon-pin / icon-pin-outlined and StatusBar exist in all of them
Unit testsjest at 52a3762OK794 passed, 2 skipped
CIrun 40885134621lint, i18n and unit green. e2e: 1 failing (Execution stopped = BUG 2). The hooks failures from b0a8564 are fixed.

5. Try it yourself

The preview talks to mf-rancher-16, whose agent is in llm-mock mode, so every LLM answer has to be queued first. The agent currently runs with planning ON and approval ON; the flags are in Settings → Chat. Log in to the preview, open Liz, start a new chat, then paste this in DevTools before sending "Create a namespace and a Pod in it":

const push = (t, stream = true) => fetch('/api/v1/namespaces/cattle-ai-agent-system/services/http:llm-mock:80/proxy/v1/control/push', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-Api-Csrf': (document.cookie.match(/CSRF=([^;]+)/) || [])[1] },
  body: JSON.stringify({ text: { chunks: stream ? t.match(/[\s\S]{1,2}/g) : [t] } }),
});
// 1) planner  2) child answer + evaluator, per task  3) final answer
await push(JSON.stringify({ subtasks: [
  { task: "Create a new namespace in the cluster 'local'.", agent: 'rancher', status: 'pending' },
  { task: "Create a pod inside the newly created namespace in cluster 'local'.", agent: 'fleet', status: 'pending' },
] }), false);
await push('Namespace demo-292 created. '); await push('yes');   // use 'input' instead of 'yes' to reproduce bug 1b
await push('Pod nginx is Running. ');      await push('yes');   // use 'no' to see the failed state
await push('Both tasks are done.');

Clear the queue with the same URL ending in /control/clear.