Skip to content

Commit a307493

Browse files
committed
Updated plan mode UI!
1 parent a10eef9 commit a307493

File tree

9 files changed

+157
-55
lines changed

9 files changed

+157
-55
lines changed

.agents/base2/base2.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,11 @@ The user asks you to implement a new feature. You respond in multiple steps:
211211
212212
${buildArray(
213213
`- Spawn file pickers, code-searcher, directory-lister, glob-matcher, commanders, and researchers to gather context as needed. The file-picker-max agent in particular is very useful to use to find relevant files. Read all the relevant files using the read_files tool. Read as many files as possible so that you have a comprehensive context on the user's request.`,
214-
`- After exploring the codebase, translate the user request into a clear and concise spec:
214+
`- After exploring the codebase, translate the user request into a clear and concise spec. If the user is just asking a question, you can answer it instead of writing a spec.
215215
216-
# Creating a spec
216+
## Creating a spec
217+
218+
Wrap your spec in <PLAN> and </PLAN> tags. The content inside should be markdown formatted (no code fences around the whole plan/spec). For example: <PLAN>\n# Plan\n- Item 1\n- Item 2\n</PLAN>.
217219
218220
The spec should include:
219221
- A brief title and overview. For the title is preferred to call it a "Plan" rather than a "Spec".
@@ -230,10 +232,14 @@ It should not include:
230232
231233
This is more like an extremely short PRD which describes the end result of what the user wants. Think of it like fleshing out the user's prompt to make it more precise, although it should be as short as possible.
232234
233-
Finally, the last optional section is Questions, which can be a numbered list, with alternate choices for each question demarcated by letters.
235+
## Questions
236+
237+
After closing the <PLAN> tags, the last optional section is Questions, which is a Questions header with a numbered list of questions and alternate choices demarcated by letters.
234238
235239
For example, here is nice short question, where the options are helpfully written out for the user:
236240
241+
Questions:
242+
237243
1. Do you want to:
238244
a) (DEFAULT) Keep Express and integrate Bun WebSockets
239245
b) Migrate the entire HTTP server to Bun.serve()

cli/src/chat.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { useShallow } from 'zustand/react/shallow'
33

44
import { routeUserPrompt } from './commands/router'
55
import { AgentModeToggle } from './components/agent-mode-toggle'
6-
import { BuildModeButtons } from './components/build-mode-buttons'
76
import { LoginModal } from './components/login-modal'
87
import { MessageRenderer } from './components/message-renderer'
98
import {
@@ -817,6 +816,8 @@ export const Chat = ({
817816
setFocusedAgentId={setFocusedAgentId}
818817
userOpenedAgents={userOpenedAgents}
819818
setUserOpenedAgents={setUserOpenedAgents}
819+
onBuildFast={handleBuildFast}
820+
onBuildMax={handleBuildMax}
820821
/>
821822
</scrollbox>
822823
</box>
@@ -859,13 +860,6 @@ export const Chat = ({
859860
customBorderChars: BORDER_CHARS,
860861
}}
861862
>
862-
{agentMode === 'PLAN' && hasReceivedPlanResponse && (
863-
<BuildModeButtons
864-
theme={theme}
865-
onBuildFast={handleBuildFast}
866-
onBuildMax={handleBuildMax}
867-
/>
868-
)}
869863
{slashContext.active && slashSuggestionItems.length > 0 ? (
870864
<SuggestionMenu
871865
items={slashSuggestionItems}

cli/src/components/__tests__/message-block.completion.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ const baseProps = {
4141
collapsedAgents: new Set<string>(),
4242
streamingAgents: new Set<string>(),
4343
onToggleCollapsed: () => {},
44+
onBuildFast: () => {},
45+
onBuildMax: () => {},
4446
}
4547

4648
describe('MessageBlock completion time', () => {

cli/src/components/__tests__/message-block.streaming.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ const baseProps = {
3939
collapsedAgents: new Set<string>(),
4040
streamingAgents: new Set<string>(),
4141
onToggleCollapsed: () => {},
42+
onBuildFast: () => {},
43+
onBuildMax: () => {},
4244
}
4345

4446
const createTimerStartTime = (elapsedSeconds: number): number | null =>

cli/src/components/build-mode-buttons.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const BuildModeButtons = ({
3636
<box
3737
style={{
3838
flexDirection: 'row',
39-
gap: 2,
39+
gap: 1,
4040
}}
4141
>
4242
<box

cli/src/components/message-block.tsx

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { AgentBranchItem } from './agent-branch-item'
66
import { ElapsedTimer } from './elapsed-timer'
77
import { renderToolComponent } from './tools/registry'
88
import { ToolCallItem } from './tools/tool-call-item'
9-
import { Thinking } from './thinking'
109
import { useTheme } from '../hooks/use-theme'
1110
import { getToolDisplayInfo } from '../utils/codebuff-client'
1211
import {
@@ -15,6 +14,9 @@ import {
1514
hasMarkdown,
1615
type MarkdownPalette,
1716
} from '../utils/markdown-renderer'
17+
import { BORDER_CHARS } from '../utils/ui-constants'
18+
import { BuildModeButtons } from './build-mode-buttons'
19+
import { Thinking } from './thinking'
1820

1921
import type { ContentBlock } from '../types/chat'
2022
import type { ThemeColor } from '../types/theme-system'
@@ -25,12 +27,6 @@ const trimTrailingNewlines = (value: string): string =>
2527
const sanitizePreview = (value: string): string =>
2628
value.replace(/[#*_`~\[\]()]/g, '').trim()
2729

28-
// Delete any complete <cb_plan>...</cb_plan> segment; hide any partial open <cb_plan>... (until closing tag arrives)
29-
const scrubPlanTags = (value: string): string =>
30-
value
31-
.replace(/<cb_plan>[\s\S]*?<\/cb_plan>/g, '')
32-
.replace(/<cb_plan>[\s\S]*$/g, '')
33-
3430
interface MessageBlockProps {
3531
messageId: string
3632
blocks?: ContentBlock[]
@@ -51,6 +47,8 @@ interface MessageBlockProps {
5147
collapsedAgents: Set<string>
5248
streamingAgents: Set<string>
5349
onToggleCollapsed: (id: string) => void
50+
onBuildFast: () => void
51+
onBuildMax: () => void
5452
}
5553

5654
export const MessageBlock = memo(
@@ -74,6 +72,8 @@ export const MessageBlock = memo(
7472
collapsedAgents,
7573
streamingAgents,
7674
onToggleCollapsed,
75+
onBuildFast,
76+
onBuildMax,
7777
}: MessageBlockProps): ReactNode => {
7878
const theme = useTheme()
7979
const resolvedTextColor = textColor ?? theme.foreground
@@ -131,7 +131,8 @@ export const MessageBlock = memo(
131131
(b.textType === 'reasoning' ||
132132
b.textType === 'reasoning_chunk' ||
133133
(typeof b.color === 'string' &&
134-
(b.color.toLowerCase() === 'grey' || b.color.toLowerCase() === 'gray')))
134+
(b.color.toLowerCase() === 'grey' ||
135+
b.color.toLowerCase() === 'gray')))
135136

136137
const renderThinkingBlock = (
137138
blocks: Extract<ContentBlock, { type: 'text' }>[],
@@ -156,7 +157,7 @@ export const MessageBlock = memo(
156157
return (
157158
<box key={thinkingId} style={{ marginLeft }}>
158159
<Thinking
159-
content={scrubPlanTags(combinedContent)}
160+
content={combinedContent}
160161
isCollapsed={isCollapsed}
161162
onToggle={() => onToggleCollapsed(thinkingId)}
162163
availableWidth={availWidth}
@@ -165,6 +166,38 @@ export const MessageBlock = memo(
165166
)
166167
}
167168

169+
const renderPlanBox = (planContent: string): React.ReactNode => {
170+
const planNodes = renderMarkdown(planContent, {
171+
codeBlockWidth: Math.max(10, availableWidth - 8),
172+
palette: markdownPalette,
173+
})
174+
return (
175+
<box
176+
style={{
177+
flexDirection: 'column',
178+
gap: 1,
179+
width: '100%',
180+
borderStyle: 'single',
181+
borderColor: theme.secondary,
182+
customBorderChars: BORDER_CHARS,
183+
paddingLeft: 1,
184+
paddingRight: 1,
185+
paddingTop: 0,
186+
paddingBottom: 1,
187+
}}
188+
>
189+
<text style={{ wrapMode: 'word', fg: theme.foreground }}>
190+
{planNodes}
191+
</text>
192+
<BuildModeButtons
193+
theme={theme}
194+
onBuildFast={onBuildFast}
195+
onBuildMax={onBuildMax}
196+
/>
197+
</box>
198+
)
199+
}
200+
168201
const renderToolBranch = (
169202
toolBlock: Extract<ContentBlock, { type: 'tool' }>,
170203
indentLevel: number,
@@ -439,10 +472,9 @@ export const MessageBlock = memo(
439472
: undefined
440473
const isNestedStreamingText =
441474
parentIsStreaming || nestedStatus === 'running'
442-
const rawNestedContent = isNestedStreamingText
475+
const filteredNestedContent = isNestedStreamingText
443476
? trimTrailingNewlines(nestedBlock.content)
444477
: nestedBlock.content.trim()
445-
const filteredNestedContent = scrubPlanTags(rawNestedContent)
446478
const renderKey = `${keyPrefix}-text-${nestedIdx}`
447479
const markdownOptionsForLevel = getAgentMarkdownOptions(indentLevel)
448480
const renderedContent = renderContentWithMarkdown(
@@ -586,9 +618,8 @@ export const MessageBlock = memo(
586618
const normalizedContent = isStreamingMessage
587619
? trimTrailingNewlines(content)
588620
: content.trim()
589-
const sanitizedContent = scrubPlanTags(normalizedContent)
590621
const displayContent = renderContentWithMarkdown(
591-
sanitizedContent,
622+
normalizedContent,
592623
isStreamingMessage,
593624
markdownOptions,
594625
)
@@ -611,10 +642,9 @@ export const MessageBlock = memo(
611642
return null
612643
}
613644
const isStreamingText = isLoading || !isComplete
614-
const rawContent = isStreamingText
645+
const filteredContent = isStreamingText
615646
? trimTrailingNewlines(block.content)
616647
: block.content.trim()
617-
const filteredContent = scrubPlanTags(rawContent)
618648
const renderKey = `${messageId}-text-${idx}`
619649
const renderedContent = renderContentWithMarkdown(
620650
filteredContent,
@@ -649,6 +679,14 @@ export const MessageBlock = memo(
649679
)
650680
}
651681

682+
case 'plan': {
683+
return (
684+
<box key={`${messageId}-plan-${idx}`} style={{ width: '100%' }}>
685+
{renderPlanBox(block.content)}
686+
</box>
687+
)
688+
}
689+
652690
case 'html': {
653691
const marginTop = block.marginTop ?? 0
654692
const marginBottom = block.marginBottom ?? 0
@@ -806,7 +844,6 @@ export const MessageBlock = memo(
806844
)}
807845
{isAi && (
808846
<>
809-
{/* Show elapsed time while streaming */}
810847
{isLoading && !isComplete && (
811848
<text
812849
attributes={TextAttributes.DIM}
@@ -823,7 +860,6 @@ export const MessageBlock = memo(
823860
/>
824861
</text>
825862
)}
826-
{/* Show completion time and credits when complete */}
827863
{isComplete && (
828864
<text
829865
attributes={TextAttributes.DIM}

cli/src/components/message-renderer.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ interface MessageRendererProps {
2929
setFocusedAgentId: React.Dispatch<React.SetStateAction<string | null>>
3030
userOpenedAgents: Set<string>
3131
setUserOpenedAgents: React.Dispatch<React.SetStateAction<Set<string>>>
32+
onBuildFast: () => void
33+
onBuildMax: () => void
3234
}
3335

3436
export const MessageRenderer = (props: MessageRendererProps): ReactNode => {
@@ -46,6 +48,8 @@ export const MessageRenderer = (props: MessageRendererProps): ReactNode => {
4648
setCollapsedAgents,
4749
setFocusedAgentId,
4850
setUserOpenedAgents,
51+
onBuildFast,
52+
onBuildMax,
4953
} = props
5054

5155
const onToggleCollapsed = useCallback(
@@ -96,6 +100,8 @@ export const MessageRenderer = (props: MessageRendererProps): ReactNode => {
96100
isWaitingForResponse={isWaitingForResponse}
97101
timerStartTime={timerStartTime}
98102
onToggleCollapsed={onToggleCollapsed}
103+
onBuildFast={onBuildFast}
104+
onBuildMax={onBuildMax}
99105
/>
100106
)
101107
})}
@@ -120,6 +126,8 @@ interface MessageWithAgentsProps {
120126
isWaitingForResponse: boolean
121127
timerStartTime: number | null
122128
onToggleCollapsed: (id: string) => void
129+
onBuildFast: () => void
130+
onBuildMax: () => void
123131
}
124132

125133
const MessageWithAgents = memo(
@@ -140,6 +148,8 @@ const MessageWithAgents = memo(
140148
isWaitingForResponse,
141149
timerStartTime,
142150
onToggleCollapsed,
151+
onBuildFast,
152+
onBuildMax,
143153
}: MessageWithAgentsProps): ReactNode => {
144154
const SIDE_GUTTER = 1
145155
const isAgent = message.variant === 'agent'
@@ -162,6 +172,8 @@ const MessageWithAgents = memo(
162172
isWaitingForResponse={isWaitingForResponse}
163173
timerStartTime={timerStartTime}
164174
onToggleCollapsed={onToggleCollapsed}
175+
onBuildFast={onBuildFast}
176+
onBuildMax={onBuildMax}
165177
/>
166178
)
167179
}
@@ -280,6 +292,8 @@ const MessageWithAgents = memo(
280292
collapsedAgents={collapsedAgents}
281293
streamingAgents={streamingAgents}
282294
onToggleCollapsed={onToggleCollapsed}
295+
onBuildFast={onBuildFast}
296+
onBuildMax={onBuildMax}
283297
/>
284298
</box>
285299
</box>
@@ -318,6 +332,8 @@ const MessageWithAgents = memo(
318332
collapsedAgents={collapsedAgents}
319333
streamingAgents={streamingAgents}
320334
onToggleCollapsed={onToggleCollapsed}
335+
onBuildFast={onBuildFast}
336+
onBuildMax={onBuildMax}
321337
/>
322338
</box>
323339
)}
@@ -344,6 +360,8 @@ const MessageWithAgents = memo(
344360
isWaitingForResponse={isWaitingForResponse}
345361
timerStartTime={timerStartTime}
346362
onToggleCollapsed={onToggleCollapsed}
363+
onBuildFast={onBuildFast}
364+
onBuildMax={onBuildMax}
347365
/>
348366
</box>
349367
))}
@@ -370,6 +388,8 @@ interface AgentMessageProps {
370388
isWaitingForResponse: boolean
371389
timerStartTime: number | null
372390
onToggleCollapsed: (id: string) => void
391+
onBuildFast: () => void
392+
onBuildMax: () => void
373393
}
374394

375395
const AgentMessage = memo(
@@ -389,6 +409,8 @@ const AgentMessage = memo(
389409
isWaitingForResponse,
390410
timerStartTime,
391411
onToggleCollapsed,
412+
onBuildFast,
413+
onBuildMax,
392414
}: AgentMessageProps): ReactNode => {
393415
const agentInfo = message.agent!
394416
const isCollapsed = collapsedAgents.has(message.id)
@@ -582,6 +604,8 @@ const AgentMessage = memo(
582604
isWaitingForResponse={isWaitingForResponse}
583605
timerStartTime={timerStartTime}
584606
onToggleCollapsed={onToggleCollapsed}
607+
onBuildFast={onBuildFast}
608+
onBuildMax={onBuildMax}
585609
/>
586610
</box>
587611
))}

0 commit comments

Comments
 (0)