feat: desktop draft welcome presets and /plan-feature command
Add starter preset chips under the composer on the desktop draft welcome screen (explore, what changed, plan, review). Clicking a chip submits immediately; prompt chips send a natural-language prompt, command chips reuse built-in slash commands. Add a new /plan-feature built-in command, modeled on /workspace-review: visible + hidden magic prompts that run a guided, batched-question planning dialogue (investigate the code, ask up to 3 clarifying questions at a time, surface pitfalls, then produce an implementation plan). Wired into command autocomplete, the submit handler, the draft plan chip, and the Magic Prompts settings page, with i18n across all locales.
This commit is contained in:
@@ -52,6 +52,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { GitHubIssuePickerDialog } from '@/components/session/GitHubIssuePickerDialog';
|
||||
import { GitHubPrPickerDialog } from '@/components/session/GitHubPrPickerDialog';
|
||||
import { Icon } from "@/components/icon/Icon";
|
||||
import type { IconName } from "@/components/icon/icons";
|
||||
import { useChatSearchDirectory } from '@/hooks/useChatSearchDirectory';
|
||||
import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { useProjectsStore } from '@/stores/useProjectsStore';
|
||||
@@ -66,6 +67,7 @@ import { buildSessionTargetOptions } from '@/sync/session-worktree-contract';
|
||||
import { usePermissionStore } from '@/stores/permissionStore';
|
||||
import { extractGitChangedFiles } from './changedFiles';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import type { I18nKey } from '@/lib/i18n';
|
||||
import { fetchResponseStyleInstruction } from '@/lib/responseStyle';
|
||||
import { wrapSystemReminder } from '@/lib/systemReminder';
|
||||
import { getSyncMessages } from '@/sync/sync-refs';
|
||||
@@ -86,6 +88,23 @@ import {
|
||||
} from './attachmentCitations';
|
||||
import type { Message, Part } from '@opencode-ai/sdk/v2/client';
|
||||
|
||||
// Starter presets shown under the composer on the desktop draft welcome screen.
|
||||
// `promptKey` presets send a plain natural-language prompt; `command` presets
|
||||
// send a built-in slash command (e.g. /workspace-review) through the normal submit path.
|
||||
type DraftPreset = {
|
||||
id: string;
|
||||
icon: IconName;
|
||||
labelKey: I18nKey;
|
||||
promptKey?: I18nKey;
|
||||
command?: string;
|
||||
};
|
||||
const DRAFT_PRESETS: readonly DraftPreset[] = [
|
||||
{ id: 'explore', icon: 'compass-3', labelKey: 'chat.draftPresets.explore.label', promptKey: 'chat.draftPresets.explore.prompt' },
|
||||
{ id: 'changes', icon: 'git-branch', labelKey: 'chat.draftPresets.changes.label', promptKey: 'chat.draftPresets.changes.prompt' },
|
||||
{ id: 'plan', icon: 'survey', labelKey: 'chat.draftPresets.plan.label', command: '/plan-feature' },
|
||||
{ id: 'review', icon: 'search-eye', labelKey: 'chat.draftPresets.review.label', command: '/workspace-review' },
|
||||
];
|
||||
|
||||
const MAX_VISIBLE_TEXTAREA_LINES = 8;
|
||||
const EMPTY_QUEUE: QueuedMessage[] = [];
|
||||
const EMPTY_MESSAGES: Message[] = [];
|
||||
@@ -1103,7 +1122,7 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
const availableSkills = useSkillsStore((s) => s.skills);
|
||||
const knownSlashNames = React.useMemo(() => {
|
||||
const names = new Set<string>([
|
||||
'init', 'review', 'undo', 'redo', 'timeline', 'compact', 'summary', 'workspace-review',
|
||||
'init', 'review', 'undo', 'redo', 'timeline', 'compact', 'summary', 'workspace-review', 'plan-feature',
|
||||
]);
|
||||
for (const command of availableCommands) names.add(command.name.toLowerCase());
|
||||
for (const skill of availableSkills) names.add(skill.name.toLowerCase());
|
||||
@@ -1931,6 +1950,28 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (commandName === 'plan-feature' && (currentSessionId || newSessionDraftOpen)) {
|
||||
try {
|
||||
await sessionActions.waitForConnectionOrThrow();
|
||||
const visibleText = await renderMagicPrompt('session.plan.visible');
|
||||
const instructionsText = await renderMagicPrompt('session.plan.instructions');
|
||||
await sendMessage(
|
||||
visibleText,
|
||||
providerIdToSend,
|
||||
modelIdToSend,
|
||||
agentNameToSend,
|
||||
[],
|
||||
agentMentionName,
|
||||
[{ text: instructionsText, synthetic: true }],
|
||||
variantToSend,
|
||||
inputMode,
|
||||
);
|
||||
scrollToBottom?.();
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.planFeatureFailed'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const currentSessionDirectory = currentSessionId
|
||||
@@ -2054,6 +2095,18 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
}
|
||||
}, [inputMode, getCurrentInputSnapshot, currentSessionId, sessionPhase, queueModeEnabled, handleQueueMessage]);
|
||||
|
||||
// Draft welcome presets: populate the composer and submit immediately.
|
||||
// getCurrentInputSnapshot reads textareaRef.current.value first, so setting it
|
||||
// synchronously lets handleSubmit pick up the preset text in the same tick.
|
||||
const submitPresetPrompt = React.useCallback((text: string) => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.value = text;
|
||||
}
|
||||
setMessage(text);
|
||||
void handleSubmitRef.current();
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// Early return during IME composition to prevent interference with autocomplete.
|
||||
// Uses keyCode === 229 fallback for WebKit where compositionend fires before keydown.
|
||||
@@ -4401,6 +4454,28 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
|
||||
{isMobile && <MobileSessionStatusBar />}
|
||||
</div>
|
||||
</div>
|
||||
{newSessionDraftOpen && !isDesktopExpanded && !isMobile && !isVSCode && !isMiniChatSurface ? (
|
||||
<div className="chat-input-column mt-4 flex flex-wrap justify-center gap-2">
|
||||
{DRAFT_PRESETS.map((preset) => (
|
||||
<button
|
||||
key={preset.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const text = preset.command ?? (preset.promptKey ? t(preset.promptKey) : '');
|
||||
if (text) submitPresetPrompt(text);
|
||||
}}
|
||||
className="group inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-[var(--interactive-hover)] hover:text-foreground"
|
||||
style={{
|
||||
backgroundColor: currentTheme?.colors?.surface?.elevated,
|
||||
borderColor: currentTheme?.colors?.interactive?.border,
|
||||
}}
|
||||
>
|
||||
<Icon name={preset.icon} className="h-3.5 w-3.5 shrink-0 opacity-70 transition-opacity group-hover:opacity-100" />
|
||||
<span>{t(preset.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</form>
|
||||
|
||||
{/* Issue Picker Dialog */}
|
||||
|
||||
@@ -152,6 +152,10 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
? [{ id: 'openchamber:workspace-review', name: 'workspace-review', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.workspaceReviewDescription'), isOpenChamber: true }]
|
||||
: []
|
||||
),
|
||||
...(canStartSessionCommand
|
||||
? [{ id: 'openchamber:plan-feature', name: 'plan-feature', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.featurePlanDescription'), isOpenChamber: true }]
|
||||
: []
|
||||
),
|
||||
];
|
||||
const allCommands = [...builtInCommands, ...customCommands, ...skillCommands];
|
||||
|
||||
@@ -197,6 +201,10 @@ export const CommandAutocomplete = React.forwardRef<CommandAutocompleteHandle, C
|
||||
? [{ id: 'openchamber:workspace-review', name: 'workspace-review', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.workspaceReviewDescription'), isOpenChamber: true }]
|
||||
: []
|
||||
),
|
||||
...(canStartSessionCommand
|
||||
? [{ id: 'openchamber:plan-feature', name: 'plan-feature', source: 'openchamber' as const, description: t('chat.commandAutocomplete.command.featurePlanDescription'), isOpenChamber: true }]
|
||||
: []
|
||||
),
|
||||
];
|
||||
|
||||
const filtered = (searchQuery
|
||||
|
||||
Reference in New Issue
Block a user