Skip to content

Commit fee3441

Browse files
committed
Implement /plan and clean up custom review UI
1 parent 2e5fa72 commit fee3441

File tree

10 files changed

+274
-160
lines changed

10 files changed

+274
-160
lines changed

cli/src/chat.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,12 @@ export const Chat = ({
841841
setInputFocused(true)
842842
}, [closeReviewScreen, setInputFocused])
843843

844+
const handleReviewCustom = useCallback(() => {
845+
closeReviewScreen()
846+
setInputMode('review')
847+
setInputFocused(true)
848+
}, [closeReviewScreen, setInputMode, setInputFocused])
849+
844850
const handlePublish = useCallback(
845851
async (agentIds: string[]) => {
846852
await publishMutation.mutateAsync(agentIds)
@@ -1444,6 +1450,7 @@ export const Chat = ({
14441450
{reviewMode ? (
14451451
<ReviewScreen
14461452
onSelectOption={handleReviewOptionSelect}
1453+
onCustom={handleReviewCustom}
14471454
onCancel={handleCloseReviewScreen}
14481455
/>
14491456
) : (

cli/src/commands/__tests__/router-input.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -372,12 +372,12 @@ describe('command-registry', () => {
372372
}
373373
})
374374

375-
test('connect:chatgpt slash command presence matches feature flag', () => {
375+
test('connect slash command presence matches feature flag', () => {
376376
const { CHATGPT_OAUTH_ENABLED } = require('@codebuff/common/constants/chatgpt-oauth')
377-
const hasChatGptSlashCommand = SLASH_COMMANDS.some(
378-
(cmd) => cmd.id === 'connect:chatgpt',
377+
const hasConnectSlashCommand = SLASH_COMMANDS.some(
378+
(cmd) => cmd.id === 'connect',
379379
)
380-
expect(hasChatGptSlashCommand).toBe(CHATGPT_OAUTH_ENABLED)
380+
expect(hasConnectSlashCommand).toBe(CHATGPT_OAUTH_ENABLED)
381381
})
382382

383383
test('connect:chatgpt command registry availability matches feature flag', () => {

cli/src/commands/command-registry.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CLAUDE_OAUTH_ENABLED } from '@codebuff/common/constants/claude-oauth'
33
import open from 'open'
44

55
import { handleAdsEnable, handleAdsDisable } from './ads'
6+
import { buildPlanPrompt, buildReviewPromptFromArgs } from './prompt-builders'
67
import { useThemeStore } from '../hooks/use-theme'
78
import { handleHelpCommand } from './help'
89
import { handleImageCommand } from './image'
@@ -15,6 +16,7 @@ import { WEBSITE_URL } from '../login/constants'
1516
import { useChatStore } from '../state/chat-store'
1617
import { useFeedbackStore } from '../state/feedback-store'
1718
import { useLoginStore } from '../state/login-store'
19+
import { getChatGptOAuthStatus } from '../utils/chatgpt-oauth'
1820
import { AGENT_MODES, IS_FREEBUFF } from '../utils/constants'
1921
import { getSystemMessage, getUserMessage } from '../utils/message-history'
2022
import { capturePendingAttachments } from '../utils/pending-attachments'
@@ -508,8 +510,8 @@ const ALL_COMMANDS: CommandDefinition[] = [
508510
...(CHATGPT_OAUTH_ENABLED
509511
? [
510512
defineCommand({
511-
name: 'connect:chatgpt',
512-
aliases: ['chatgpt'],
513+
name: 'connect',
514+
aliases: ['connect:chatgpt', 'chatgpt'],
513515
handler: (params) => {
514516
useChatStore.getState().setInputMode('connect:chatgpt')
515517
params.saveToHistory(params.inputValue.trim())
@@ -527,19 +529,72 @@ const ALL_COMMANDS: CommandDefinition[] = [
527529
return { openChatHistory: true }
528530
},
529531
}),
532+
defineCommandWithArgs({
533+
name: 'plan',
534+
handler: (params, args) => {
535+
// In freebuff mode, require ChatGPT connection
536+
if (IS_FREEBUFF && !getChatGptOAuthStatus().connected) {
537+
params.setMessages((prev) => [
538+
...prev,
539+
getUserMessage(params.inputValue.trim()),
540+
getSystemMessage(
541+
'Connect your ChatGPT account to use /plan. Use /connect to get started.',
542+
),
543+
])
544+
params.saveToHistory(params.inputValue.trim())
545+
clearInput(params)
546+
useChatStore.getState().setInputMode('connect:chatgpt')
547+
return
548+
}
549+
550+
const trimmedArgs = args.trim()
551+
552+
params.saveToHistory(params.inputValue.trim())
553+
clearInput(params)
554+
555+
// If user provided plan text directly, send it immediately
556+
if (trimmedArgs) {
557+
params.sendMessage({
558+
content: buildPlanPrompt(trimmedArgs),
559+
agentMode: params.agentMode,
560+
})
561+
setTimeout(() => {
562+
params.scrollToLatest()
563+
}, 0)
564+
return
565+
}
566+
567+
// Otherwise enter plan mode
568+
useChatStore.getState().setInputMode('plan')
569+
},
570+
}),
530571
defineCommandWithArgs({
531572
name: 'review',
532573
handler: (params, args) => {
574+
// In freebuff mode, require ChatGPT connection
575+
if (IS_FREEBUFF && !getChatGptOAuthStatus().connected) {
576+
params.setMessages((prev) => [
577+
...prev,
578+
getUserMessage(params.inputValue.trim()),
579+
getSystemMessage(
580+
'Connect your ChatGPT account to use /review. Use /connect to get started.',
581+
),
582+
])
583+
params.saveToHistory(params.inputValue.trim())
584+
clearInput(params)
585+
useChatStore.getState().setInputMode('connect:chatgpt')
586+
return
587+
}
588+
533589
const trimmedArgs = args.trim()
534590

535591
params.saveToHistory(params.inputValue.trim())
536592
clearInput(params)
537593

538594
// If user provided review text directly, send it immediately without showing the screen
539595
if (trimmedArgs) {
540-
const reviewPrompt = `@thinker-gpt Please review: ${trimmedArgs}`
541596
params.sendMessage({
542-
content: reviewPrompt,
597+
content: buildReviewPromptFromArgs(trimmedArgs),
543598
agentMode: params.agentMode,
544599
})
545600
setTimeout(() => {
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* Centralized prompt builders for /plan and /review commands.
3+
* This ensures consistent behavior regardless of entry path.
4+
*/
5+
6+
// Base prompt for plan command - always gathers context first
7+
export const PLAN_BASE_PROMPT = 'Gather all the relevant context and then spawn @thinker-gpt Think about how to implement the following:'
8+
9+
// Base prompt for review command - always gathers context first
10+
export const REVIEW_BASE_PROMPT = 'Please gather all relevant context and then spawn @thinker-gpt to review:'
11+
12+
/**
13+
* Build a plan prompt from user input.
14+
* @param input - The user's plan request (e.g., "add OAuth login")
15+
* @returns The full prompt to send to the agent
16+
*/
17+
export function buildPlanPrompt(input: string): string {
18+
const trimmedInput = input.trim()
19+
if (!trimmedInput) {
20+
return PLAN_BASE_PROMPT
21+
}
22+
return `${PLAN_BASE_PROMPT}\n\n${trimmedInput}`
23+
}
24+
25+
/**
26+
* Review scope presets for the review screen.
27+
*/
28+
type ReviewScope = 'uncommitted' | 'branch' | 'custom'
29+
30+
/**
31+
* Get the default text for a review scope preset.
32+
*/
33+
function getReviewScopeText(scope: ReviewScope): string {
34+
switch (scope) {
35+
case 'uncommitted':
36+
return 'uncommitted changes'
37+
case 'branch':
38+
return 'this branch compared to main'
39+
case 'custom':
40+
return ''
41+
}
42+
}
43+
44+
/**
45+
* Build a review prompt from scope or custom input.
46+
* @param scope - The selected review scope (uncommitted, branch, or custom)
47+
* @param customInput - Optional custom review focus (when scope is 'custom')
48+
* @returns The full prompt to send to the agent
49+
*/
50+
export function buildReviewPrompt(scope: ReviewScope, customInput?: string): string {
51+
const scopeText = getReviewScopeText(scope)
52+
53+
// For custom input, append the user's specific focus
54+
if (scope === 'custom' && customInput?.trim()) {
55+
return `${REVIEW_BASE_PROMPT} ${customInput.trim()}`
56+
}
57+
58+
// For preset scopes, use the scope text
59+
if (scopeText) {
60+
return `${REVIEW_BASE_PROMPT} ${scopeText}`
61+
}
62+
63+
// Fallback for custom with no input
64+
return REVIEW_BASE_PROMPT
65+
}
66+
67+
/**
68+
* Build a review prompt from direct argument (e.g., /review foo).
69+
* This is used when the user provides review text directly after the command.
70+
* @param input - The user's review request
71+
* @returns The full prompt to send to the agent
72+
*/
73+
export function buildReviewPromptFromArgs(input: string): string {
74+
const trimmedInput = input.trim()
75+
// Use the same format as preset scopes for consistency
76+
return `${REVIEW_BASE_PROMPT} ${trimmedInput}`
77+
}
78+

cli/src/commands/router.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from './router-utils'
2020
import { handleClaudeAuthCode } from '../components/claude-connect-banner'
2121
import { handleChatGptAuthCode } from '../components/chatgpt-connect-banner'
22+
import { buildPlanPrompt, buildReviewPrompt } from './prompt-builders'
2223
import { getProjectRoot } from '../project-files'
2324
import { useChatStore } from '../state/chat-store'
2425
import { trackEvent } from '../utils/analytics'
@@ -311,6 +312,38 @@ export async function routeUserPrompt(
311312
return
312313
}
313314

315+
// Handle plan mode input
316+
if (inputMode === 'plan') {
317+
if (!trimmed) return
318+
saveToHistory(trimmed)
319+
setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
320+
setInputMode('default')
321+
setInputFocused(true)
322+
inputRef.current?.focus()
323+
324+
sendMessage({ content: buildPlanPrompt(trimmed), agentMode })
325+
setTimeout(() => {
326+
scrollToLatest()
327+
}, 0)
328+
return
329+
}
330+
331+
// Handle review mode input
332+
if (inputMode === 'review') {
333+
if (!trimmed) return
334+
saveToHistory(trimmed)
335+
setInputValue({ text: '', cursorPosition: 0, lastEditDueToNav: false })
336+
setInputMode('default')
337+
setInputFocused(true)
338+
inputRef.current?.focus()
339+
340+
sendMessage({ content: buildReviewPrompt('custom', trimmed), agentMode })
341+
setTimeout(() => {
342+
scrollToLatest()
343+
}, 0)
344+
return
345+
}
346+
314347
// Handle bash commands from queue (starts with '!')
315348
if (trimmed.startsWith('!') && trimmed.length > 1) {
316349
const command = trimmed.slice(1)

cli/src/components/chat-input-bar.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,13 @@ export const ChatInputBar = ({
348348
backgroundColor: theme.surface,
349349
}}
350350
>
351+
{modeConfig.label && (
352+
<box style={{ flexShrink: 0, paddingRight: 1 }}>
353+
<text>
354+
<span bg={theme.info} fg={theme.background}>{` ${modeConfig.label} `}</span>
355+
</text>
356+
</box>
357+
)}
351358
{modeConfig.icon && (
352359
<box
353360
style={{
@@ -431,6 +438,13 @@ export const ChatInputBar = ({
431438
width: '100%',
432439
}}
433440
>
441+
{modeConfig.label && (
442+
<box style={{ flexShrink: 0, paddingRight: 1 }}>
443+
<text>
444+
<span bg={theme.info} fg={theme.background}>{` ${modeConfig.label} `}</span>
445+
</text>
446+
</box>
447+
)}
434448
{modeConfig.icon && (
435449
<box
436450
style={{

0 commit comments

Comments
 (0)