add /loop-n command for repeated prompt execution
This commit is contained in:
@@ -161,6 +161,17 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
|
const [showAbortStatus, setShowAbortStatus] = React.useState(false);
|
||||||
const [textareaScrollTop, setTextareaScrollTop] = React.useState(0);
|
const [textareaScrollTop, setTextareaScrollTop] = React.useState(0);
|
||||||
|
|
||||||
|
// Loop state for /loop-n command
|
||||||
|
const [loopState, setLoopState] = React.useState<{
|
||||||
|
active: boolean;
|
||||||
|
current: number;
|
||||||
|
total: number;
|
||||||
|
prompt: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// Get session status from store for loop detection
|
||||||
|
const sessionStatus = useSessionStore((state) => state.sessionStatus);
|
||||||
|
|
||||||
const isDesktopExpanded = isExpandedInput && !isMobile;
|
const isDesktopExpanded = isExpandedInput && !isMobile;
|
||||||
|
|
||||||
const sendableAttachedFiles = React.useMemo(
|
const sendableAttachedFiles = React.useMemo(
|
||||||
@@ -823,6 +834,46 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
setMessage('');
|
setMessage('');
|
||||||
return; // Don't send to assistant
|
return; // Don't send to assistant
|
||||||
}
|
}
|
||||||
|
// NEW: /loop-n - loop prompt n times
|
||||||
|
else if (commandName?.startsWith('loop-') && currentSessionId) {
|
||||||
|
const loopNumStr = commandName.slice(5);
|
||||||
|
const loopNum = parseInt(loopNumStr, 10);
|
||||||
|
if (isNaN(loopNum) || loopNum < 1 || loopNum > 100) {
|
||||||
|
console.warn('Invalid loop count:', loopNumStr);
|
||||||
|
setMessage('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Extract prompt after /loop-n
|
||||||
|
const loopArgs = normalizedCommand.replace(/^\/loop-\d+\s*/, '');
|
||||||
|
if (!loopArgs.trim()) {
|
||||||
|
console.warn('No prompt provided for /loop-n');
|
||||||
|
setMessage('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Start loop - set state first
|
||||||
|
setLoopState({
|
||||||
|
active: true,
|
||||||
|
current: 1,
|
||||||
|
total: loopNum,
|
||||||
|
prompt: loopArgs,
|
||||||
|
});
|
||||||
|
toast.info(`Loop started: ${loopNum} iterations`);
|
||||||
|
// Clear input
|
||||||
|
setMessage('');
|
||||||
|
// Send first message immediately using local variable (don't wait for state update)
|
||||||
|
void sendMessage(
|
||||||
|
loopArgs,
|
||||||
|
currentProviderId,
|
||||||
|
currentModelId,
|
||||||
|
currentAgentName,
|
||||||
|
primaryAttachments,
|
||||||
|
agentMentionName,
|
||||||
|
additionalParts.length > 0 ? additionalParts : undefined,
|
||||||
|
currentVariant,
|
||||||
|
inputMode
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect all attachments for error recovery
|
// Collect all attachments for error recovery
|
||||||
@@ -901,6 +952,38 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
// Update ref with latest handleSubmit on every render
|
// Update ref with latest handleSubmit on every render
|
||||||
handleSubmitRef.current = handleSubmit;
|
handleSubmitRef.current = handleSubmit;
|
||||||
|
|
||||||
|
// Handle loop: detect when AI finishes and send next iteration
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!loopState?.active || !currentSessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = sessionStatus?.get(currentSessionId);
|
||||||
|
if (status?.type === 'idle') {
|
||||||
|
// AI finished, check if we should continue looping
|
||||||
|
if (loopState.current <= loopState.total) {
|
||||||
|
if (loopState.current < loopState.total) {
|
||||||
|
const next = loopState.current + 1;
|
||||||
|
setLoopState({ ...loopState, current: next });
|
||||||
|
useSessionStore.getState().sendMessage(
|
||||||
|
loopState.prompt,
|
||||||
|
currentProviderId,
|
||||||
|
currentModelId,
|
||||||
|
currentAgentName,
|
||||||
|
[],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
currentVariant,
|
||||||
|
inputMode
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Loop complete - all iterations sent
|
||||||
|
setLoopState(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [loopState, sessionStatus, currentSessionId, currentProviderId, currentModelId, currentAgentName, currentVariant, inputMode]);
|
||||||
|
|
||||||
// Primary action for send button - respects queue mode setting
|
// Primary action for send button - respects queue mode setting
|
||||||
const handlePrimaryAction = React.useCallback(() => {
|
const handlePrimaryAction = React.useCallback(() => {
|
||||||
const canQueue = inputMode === 'normal' && hasContent && currentSessionId && sessionPhase !== 'idle';
|
const canQueue = inputMode === 'normal' && hasContent && currentSessionId && sessionPhase !== 'idle';
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ interface StatusRowProps {
|
|||||||
wasAborted?: boolean;
|
wasAborted?: boolean;
|
||||||
abortActive?: boolean;
|
abortActive?: boolean;
|
||||||
retryInfo?: { attempt?: number; next?: number } | null;
|
retryInfo?: { attempt?: number; next?: number } | null;
|
||||||
|
// Loop state
|
||||||
|
loopInfo?: { current: number; total: number; prompt: string } | null;
|
||||||
|
onStopLoop?: () => void;
|
||||||
// Abort state (for mobile/vscode)
|
// Abort state (for mobile/vscode)
|
||||||
showAbort?: boolean;
|
showAbort?: boolean;
|
||||||
onAbort?: () => void;
|
onAbort?: () => void;
|
||||||
@@ -108,6 +111,8 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
|||||||
wasAborted,
|
wasAborted,
|
||||||
abortActive,
|
abortActive,
|
||||||
retryInfo,
|
retryInfo,
|
||||||
|
loopInfo,
|
||||||
|
onStopLoop,
|
||||||
showAbort,
|
showAbort,
|
||||||
onAbort,
|
onAbort,
|
||||||
showAbortStatus,
|
showAbortStatus,
|
||||||
@@ -254,9 +259,26 @@ export const StatusRow: React.FC<StatusRowProps> = ({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Abort (mobile only) + Todo */}
|
{/* Right: Abort (mobile only) + Todo + Loop indicator */}
|
||||||
<div className="relative flex items-center gap-2 flex-shrink-0" ref={popoverRef}>
|
<div className="relative flex items-center gap-2 flex-shrink-0" ref={popoverRef}>
|
||||||
{abortButton}
|
{abortButton}
|
||||||
|
{loopInfo && (
|
||||||
|
<div className="flex items-center gap-1.5 text-[var(--status-info)]">
|
||||||
|
<span className="typography-ui-label">
|
||||||
|
Loop {loopInfo.current}/{loopInfo.total}
|
||||||
|
</span>
|
||||||
|
{onStopLoop && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onStopLoop}
|
||||||
|
className="flex items-center justify-center h-[1.2rem] w-[1.2rem] text-[var(--status-error)] transition-opacity hover:opacity-80 focus-visible:outline-none flex-shrink-0"
|
||||||
|
aria-label="Stop loop"
|
||||||
|
>
|
||||||
|
<RiCloseCircleLine size={18} aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{todoTrigger}
|
{todoTrigger}
|
||||||
|
|
||||||
{/* Popover dropdown */}
|
{/* Popover dropdown */}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
|||||||
import { useUpdateStore } from '@/stores/useUpdateStore';
|
import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||||
import { cn, formatDirectoryName, hasModifier } from '@/lib/utils';
|
import { cn, formatDirectoryName, hasModifier } from '@/lib/utils';
|
||||||
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
|
import { PROJECT_ICON_MAP, PROJECT_COLOR_MAP, getProjectIconImageUrl } from '@/lib/projectMeta';
|
||||||
import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell, requestDirectoryAccess } from '@/lib/desktop';
|
import { isDesktopLocalOriginActive, isTauriShell, requestDirectoryAccess } from '@/lib/desktop';
|
||||||
import { useLongPress } from '@/hooks/useLongPress';
|
import { useLongPress } from '@/hooks/useLongPress';
|
||||||
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||||
import { sessionEvents } from '@/lib/sessionEvents';
|
import { sessionEvents } from '@/lib/sessionEvents';
|
||||||
@@ -548,7 +548,6 @@ export const NavRail: React.FC<NavRailProps> = ({ className, mobile }) => {
|
|||||||
iconBackground?: string | null;
|
iconBackground?: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const isDesktopApp = React.useMemo(() => isDesktopShell(), []);
|
|
||||||
const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []);
|
const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []);
|
||||||
|
|
||||||
const formatLabel = React.useCallback(
|
const formatLabel = React.useCallback(
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
RiCloudLine,
|
RiCloudLine,
|
||||||
RiFoldersLine,
|
RiFoldersLine,
|
||||||
RiGitBranchLine,
|
RiGitBranchLine,
|
||||||
RiMicLine,
|
|
||||||
RiNotification3Line,
|
RiNotification3Line,
|
||||||
RiPaletteLine,
|
RiPaletteLine,
|
||||||
RiListUnordered,
|
RiListUnordered,
|
||||||
|
|||||||
@@ -709,6 +709,8 @@ export const useMessageStore = create<MessageStore>()(
|
|||||||
const [commandToken, ...firstLineArgs] = firstLine.split(" ");
|
const [commandToken, ...firstLineArgs] = firstLine.split(" ");
|
||||||
const command = commandToken.slice(1).trim();
|
const command = commandToken.slice(1).trim();
|
||||||
if (command.toLowerCase() === "shell") return null;
|
if (command.toLowerCase() === "shell") return null;
|
||||||
|
// Ignore loop-n commands - these are handled client-side
|
||||||
|
if (command.toLowerCase().startsWith("loop-")) return null;
|
||||||
if (!command) return null;
|
if (!command) return null;
|
||||||
const restOfInput = firstLineEnd === -1 ? "" : trimmedContent.slice(firstLineEnd + 1);
|
const restOfInput = firstLineEnd === -1 ? "" : trimmedContent.slice(firstLineEnd + 1);
|
||||||
const argsFromFirstLine = firstLineArgs.join(" ").trim();
|
const argsFromFirstLine = firstLineArgs.join(" ").trim();
|
||||||
|
|||||||
Reference in New Issue
Block a user