Skip to content

feat(web): add Plan Mode toggle and plan preview UI#1406

Merged
RealKai42 merged 11 commits intomainfrom
kaiyi/debug-plan-mode
Mar 11, 2026
Merged

feat(web): add Plan Mode toggle and plan preview UI#1406
RealKai42 merged 11 commits intomainfrom
kaiyi/debug-plan-mode

Conversation

@RealKai42
Copy link
Copy Markdown
Collaborator

@RealKai42 RealKai42 commented Mar 11, 2026

Summary

  • Add a silent Plan Mode toggle switch in the prompt toolbar (model/thinking bar), using a dedicated toggle_plan_mode RPC instead of injecting /plan into context
  • Propagate plan_mode state from backend to frontend via StatusUpdate wire events
  • Display plan content as a collapsible inline Markdown preview in the QuestionDialog when ExitPlanMode is triggered
  • Add plan_mode field to StatusUpdate wire type and snapshot tests

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any.
  • I have added tests that prove my fix is effective or that my feature works.
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation.

Open with Devin

Copilot AI review requested due to automatic review settings March 11, 2026 12:07
Signed-off-by: Kai <me@kaiyi.cool>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 6 additional findings.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a Web UI Plan Mode toggle backed by a new Wire 1.4 toggle_plan_mode JSON-RPC method, propagates plan-mode state via StatusUpdate events, and adds an inline plan preview in the question dialog for ExitPlanMode.

Changes:

  • Add plan_mode to StatusUpdate and bump Wire protocol version to 1.4 (types + docs + snapshot).
  • Implement toggle_plan_mode JSON-RPC request on the backend and expose it via the Web session stream hook + UI toggle.
  • Render a collapsible Markdown “Plan Preview” in QuestionDialog when the plan content is provided in QuestionItem.body.

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
web/src/hooks/wireTypes.ts Extend Wire TS types: StatusUpdate.plan_mode, QuestionItem.body/other_*.
web/src/hooks/useSessionStream.ts Track planMode from StatusUpdate, send toggle_plan_mode, bump init protocol to 1.4 and advertise supports_plan_mode.
web/src/features/chat/global-config-controls.tsx Add Plan Mode switch UI.
web/src/features/chat/components/question-dialog.tsx Add collapsible plan preview UI (Markdown render).
web/src/features/chat/components/prompt-toolbar/index.tsx Show “plan” badge and keep toolbar visible while plan mode is active.
web/src/features/chat/components/chat-prompt-composer.tsx Wire planMode/toggle props into controls.
web/src/features/chat/chat.tsx Thread planMode/toggle props through workspace.
web/src/features/chat/chat-workspace-container.tsx Connect session-stream planMode + togglePlanMode() to the workspace.
tests/core/test_wire_message.py Update snapshot for new plan_mode field in StatusUpdate.
src/kimi_cli/wire/types.py Add plan_mode to StatusUpdate model.
src/kimi_cli/wire/server.py Add toggle_plan_mode request handling + emit StatusUpdate(plan_mode=...).
src/kimi_cli/wire/protocol.py Bump WIRE_PROTOCOL_VERSION to 1.4.
src/kimi_cli/wire/jsonrpc.py Add JSONRPCTogglePlanModeMessage and inbound method allowlist entry.
src/kimi_cli/soul/slash.py Emit StatusUpdate(plan_mode=...) when /plan changes state.
src/kimi_cli/soul/kimisoul.py Include plan_mode in step StatusUpdate.
docs/**/customization/wire-mode.md Document supports_plan_mode, toggle_plan_mode, and StatusUpdate.plan_mode.
docs/**/release-notes/changelog.md, CHANGELOG.md Add release-note entry for the web plan mode toggle.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2626 to +2637
// Toggle plan mode via silent RPC (no context message)
const togglePlanMode = useCallback(() => {
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
return;
}
const message: JsonRpcRequest = {
jsonrpc: "2.0",
method: "toggle_plan_mode",
id: uuidV4(),
};
wsRef.current.send(JSON.stringify(message));
}, []);
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

togglePlanMode() sends a JSON-RPC request but the hook doesn’t track the request id or handle its error responses specially. In handleMessage(), any JSON-RPC error (except initialize retries) is treated as fatal, so a toggle_plan_mode failure would incorrectly put the whole session stream into the "error" state. Consider recording the toggle request id(s) and handling errors for those ids as non-fatal (e.g., show a toast and keep the socket/session alive).

Copilot uses AI. Check for mistakes.
Comment on lines +444 to +445
<CollapsibleTrigger className="flex items-center gap-2 w-full text-xs text-muted-foreground hover:text-foreground transition-colors py-1">
<ChevronRightIcon className="size-3.5 transition-transform [[data-state=open]>&]:rotate-90" />
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The Chevron rotation class [[data-state=open]>&]:rotate-90 doesn’t match the existing Collapsible patterns in this codebase (which use group-data-[state=...] on an icon inside a group trigger). As written, the icon is unlikely to rotate when the collapsible opens/closes. Consider adding a group class to the CollapsibleTrigger and switching to a group-data-[state=open]:rotate-90 (or equivalent) variant on the icon.

Suggested change
<CollapsibleTrigger className="flex items-center gap-2 w-full text-xs text-muted-foreground hover:text-foreground transition-colors py-1">
<ChevronRightIcon className="size-3.5 transition-transform [[data-state=open]>&]:rotate-90" />
<CollapsibleTrigger className="group flex items-center gap-2 w-full text-xs text-muted-foreground hover:text-foreground transition-colors py-1">
<ChevronRightIcon className="size-3.5 transition-transform group-data-[state=open]:rotate-90" />

Copilot uses AI. Check for mistakes.
Comment on lines +441 to +454
{/* Plan body preview */}
{currentQuestion.body && (
<Collapsible defaultOpen className="mx-4 mb-2">
<CollapsibleTrigger className="flex items-center gap-2 w-full text-xs text-muted-foreground hover:text-foreground transition-colors py-1">
<ChevronRightIcon className="size-3.5 transition-transform [[data-state=open]>&]:rotate-90" />
<span>Plan Preview</span>
</CollapsibleTrigger>
<CollapsibleContent>
<div className="border-l-2 border-blue-400/40 pl-3 mt-1 max-h-[360px] overflow-y-auto">
<MessageResponse>{currentQuestion.body}</MessageResponse>
</div>
</CollapsibleContent>
</Collapsible>
)}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

ExitPlanMode questions include other_label/other_description (e.g. "Revise") for the free-text option, but the dialog UI still renders the synthetic free-text row without any label/description, making the intended action unclear. Since this PR adds plan review UX based on currentQuestion.body, consider also rendering currentQuestion.other_label and currentQuestion.other_description alongside the free-text input (falling back to defaults when empty).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +2627 to +2638
const sendSetPlanMode = useCallback((enabled: boolean) => {
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
return;
}
const message: JsonRpcRequest = {
jsonrpc: "2.0",
method: "set_plan_mode",
id: uuidV4(),
params: { enabled },
};
wsRef.current.send(JSON.stringify(message));
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Web client treats set_plan_mode JSON-RPC error as fatal, crashing the chat session

When sendSetPlanMode sends the set_plan_mode RPC (web/src/hooks/useSessionStream.ts:2631-2638), the response is handled by the generic handleMessage callback. If the server returns an error (e.g., "Plan mode is not supported" from src/kimi_cli/wire/server.py:563-569), the generic error handler (around line 1862-1896 of useSessionStream.ts) treats it as fatal: it sets setStatus("error"), calls onError, and calls completeStreamingMessages(). This puts the entire chat session into an error state and shows an error toast, even though a failed plan mode toggle should be a non-fatal, recoverable condition. The sendSetPlanMode function should either handle the response itself or the generic error handler should have an allowlist for non-fatal RPC error responses.

Prompt for agents
In web/src/hooks/useSessionStream.ts, the sendSetPlanMode function (around line 2627-2638) sends a set_plan_mode JSON-RPC request but does not track its message ID. The generic error handler in handleMessage (around line 1862-1896) treats all non-initialize error responses as fatal stream errors.

Fix: Track the set_plan_mode request ID (similar to initializeIdRef) so that the error handler can recognize it and handle it gracefully (e.g., log a warning or show a non-fatal toast) instead of crashing the chat session. Alternatively, add the set_plan_mode message ID to a set of non-fatal RPC IDs that get special handling in the error branch.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 13 additional findings in Devin Review.

Open in Devin Review

{/* ── Tab bar ── */}
<div className="flex items-center gap-1.5 px-1">
{activityStatus && (
{activityStatus && (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Broken indentation in prompt-toolbar JSX

Line 102 of web/src/features/chat/components/prompt-toolbar/index.tsx has lost its indentation. The {activityStatus && ( expression is at column 0 instead of being indented within the parent <div> element. This is a formatting-only issue that doesn't affect rendering but violates the code style convention used throughout the codebase.

Suggested change
{activityStatus && (
{activityStatus && (
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@RealKai42 RealKai42 merged commit 0af2129 into main Mar 11, 2026
14 checks passed
@RealKai42 RealKai42 deleted the kaiyi/debug-plan-mode branch March 11, 2026 15:13
x5iu pushed a commit to x5iu/kimi-cli that referenced this pull request Mar 15, 2026
- Add StatusUpdate.plan_mode field for real-time plan state broadcast
- Add SessionState.plan_mode for cross-session persistence
- Add set_plan_mode_from_manual() for idempotent state setting
- Add set_plan_mode Wire protocol method and handler
- Broadcast StatusUpdate after plan mode changes in /plan slash command
- Bump Wire protocol version to 1.4
- Fix test_display_block_diff_str_replace to use Edit tool
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.

2 participants