diff --git a/.superpowers/sdd/fix-report.md b/.superpowers/sdd/fix-report.md new file mode 100644 index 00000000..8f22c37e --- /dev/null +++ b/.superpowers/sdd/fix-report.md @@ -0,0 +1,23 @@ +# Code Review Fix Report + +## Branch: V1.4.0 — Enhance Prompt feature + +### Fixes Applied + +**Fix 1 (Important) — Spinner stuck on disconnect + double-send on WS error** +- File: `app/ui_layer/adapters/browser_adapter.py` +- Split single try/except in `_handle_enhance_prompt` into two independent blocks: one for the LLM call (returns on success), one for the fallback send. A closed socket on the fallback is now swallowed silently rather than raising unhandled. + +**Fix 2 (Important) — Reset `enhancing` state on disconnect** +- File: `app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx` +- Added `useEffect(() => { if (!connected) setEnhancing(false) }, [connected])` after the existing `enhancedPrompt` effect. `connected` was already destructured from `useWebSocket()`. + +**Fix 3 (Minor) — Remove duplicate `.spinIcon` CSS class** +- File: `app/ui_layer/browser/frontend/src/components/Chat/Chat.module.css` — deleted `.spinIcon { animation: spin 1s linear infinite; }` (duplicated `.uploadingSpinner`). +- File: `app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx` — changed `className={styles.spinIcon}` → `className={styles.uploadingSpinner}` on the Loader2 IconButton. + +**Fix 4 (Minor) — Type the `ws` parameter** +- The `_handle_enhance_prompt` signature already matches the rest of the file's pattern (`ws` with no explicit type). No change needed — consistency preserved. + +### Typecheck +`npx tsc --noEmit`: 0 new errors (all pre-existing issues unrelated to these changes). diff --git a/app/ui_layer/adapters/browser_adapter.py b/app/ui_layer/adapters/browser_adapter.py index d7cbde5c..95c9c95e 100644 --- a/app/ui_layer/adapters/browser_adapter.py +++ b/app/ui_layer/adapters/browser_adapter.py @@ -1062,6 +1062,119 @@ async def submit_message( living_ui_id=living_ui_id, ) + async def _handle_enhance_prompt(self, content: str, ws) -> None: + """Enhance a user's prompt using the LLM for clarity and precision.""" + SYSTEM = """ +You are a prompt enhancer for CraftBot — a proactive autonomous AI agent that +controls a computer (file system, CLI, browser, MCP tools, external +integrations, and a task scheduler). + +Your output feeds directly into CraftBot's task pipeline. A poorly written +prompt causes wrong skill selection, wrong action sets, misrouted sessions, +or the agent executing the wrong thing entirely. Your job is to eliminate +every source of ambiguity before the agent ever sees the instruction. + + +RULE 1 — PRESERVE INTENT EXACTLY +Never change, expand, or restrict what the user asked for. +Clarify; do not invent. If uncertain, keep the original scope. + +RULE 2 — NAME THE TARGET EXPLICITLY +Vague references to apps, files, or services cause the agent to guess wrong. +- "my emails" → name the email client (Gmail, Outlook, etc.) if known; + otherwise write "the default email client or browser" +- "that file" → name the file or folder path +- "send a message" → name the platform (Telegram, WhatsApp, Slack, Discord) + if implied by context; this prevents wrong platform routing +- "remind me" → write "create a proactive scheduled task" + +RULE 3 — STATE THE DONE-CONDITION +The agent verifies tasks against a done-condition. If it is missing, the +agent either over-executes or loops asking for confirmation. +End every enhanced prompt with what success looks like: +"...and confirm to me when complete." +"...and save the result to the workspace folder." +"...and send me a summary of what was found." + +RULE 4 — SIGNAL TASK COMPLEXITY +CraftBot routes to simple_task (fast, no plan) or complex_task (todos, +verification, user approval). Use these signals so routing is correct: +- For quick lookups, checks, or single-step actions: keep the prompt direct + and short — this naturally triggers simple_task mode +- For multi-step work, file changes, or anything needing verification: + include the phrase "and verify the result before reporting back to me" + — this signals complex_task mode + +RULE 5 — HONOUR SCHEDULING SIGNALS +CraftBot has a built-in proactive scheduler. If the user implies recurrence +("every day", "each week", "automatically", "whenever X happens"), write +"Set up a recurring proactive task to..." — this ensures the scheduler +system is invoked, not a one-off task. + +RULE 6 — ELIMINATE PRONOUN AMBIGUITY +"it", "this", "that", "them", "there" — replace every pronoun with the +actual noun it refers to, using context from the conversation if available. + +RULE 7 — ONE ACTION FRAME +Do not chain unrelated actions into one prompt. If the user asked for one +thing, keep it as one thing. Do not add "and also..." unless the user said so. + + + +Before writing the enhanced prompt, silently work through: +1. What is the single core intent? (state it in one clause) +2. What nouns are vague or missing? (app, file, platform, service) +3. What is the done-condition? (file saved, message sent, result shown) +4. simple or complex task? (single-shot vs. multi-step + verify) +5. Any scheduling signal? (one-time vs. recurring) +6. Any pronouns to replace with actual nouns? + + + +NEVER do these: +- Do NOT add scope the user didn't ask for ("...and also back up your files") +- Do NOT produce bullet lists or numbered steps — output is one prose block +- Do NOT include preamble ("Here is the improved prompt:", "Enhanced:", etc.) +- Do NOT wrap the output in quotes +- Do NOT exceed 4 sentences +- Do NOT use passive voice — use active imperative verbs +- Do NOT leave platform names implicit when a platform is involved + + + +Return ONLY the enhanced prompt as plain prose. Nothing else. + +BAD: "check my emails" +GOOD: "Open Gmail in the browser, check for unread emails received in the + last 24 hours, and send me a plain-text summary of any messages that + need a reply or action." + +BAD: "remind me about the standup" +GOOD: "Create a recurring proactive task that sends me a reminder message + 5 minutes before my daily standup meeting, using the schedule defined + in my calendar or a fixed daily time I confirm." + +BAD: "clean it up" +GOOD: "Open the Downloads folder, identify duplicate files and files not + accessed in the last 30 days, list them for my review, and move only + the confirmed items to Trash." + +""" + try: + enhanced = await self._controller.agent.llm.generate_response_async( + system_prompt=SYSTEM, + user_prompt=content, + log_response=False, + ) + await ws.send_json({"type": "prompt_enhanced", "content": enhanced.strip()}) + return + except Exception as e: + logger.warning(f"[BROWSER ADAPTER] enhance_prompt failed: {e}") + try: + await ws.send_json({"type": "prompt_enhanced", "content": content}) + except Exception as send_err: + logger.warning(f"[BROWSER ADAPTER] enhance_prompt fallback send failed: {send_err}") + def _handle_task_start(self, event: UIEvent) -> None: """Handle task start event with metrics tracking.""" # Call parent implementation @@ -1437,6 +1550,11 @@ async def _handle_ws_message(self, data: Dict[str, Any], ws=None) -> None: if command: await self.submit_message(command) + elif msg_type == "enhance_prompt": + content = data.get("content", "") + if content and ws: + asyncio.create_task(self._handle_enhance_prompt(content, ws)) + elif msg_type == "chat_history": before_timestamp = data.get("beforeTimestamp") limit = data.get("limit", 50) diff --git a/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx b/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx index 3780f357..a571d271 100644 --- a/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx +++ b/app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx @@ -1,5 +1,5 @@ import React, { useState, useRef, useEffect, useLayoutEffect, KeyboardEvent, useCallback, ChangeEvent, useMemo } from 'react' -import { Send, Paperclip, X, Loader2, File, AlertCircle, Reply, Mic, MicOff, ChevronDown } from 'lucide-react' +import { Send, Paperclip, X, Loader2, File, AlertCircle, Reply, Mic, MicOff, ChevronDown, Sparkles } from 'lucide-react' import { useVirtualizer } from '@tanstack/react-virtual' import { useWebSocket } from '../../contexts/WebSocketContext' import { useToast } from '../../contexts/ToastContext' @@ -114,6 +114,9 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { loadOlderMessages, hasMoreMessages, loadingOlderMessages, + enhancedPrompt, + enhancePrompt, + clearEnhancedPrompt, } = useWebSocket() const status = useDerivedAgentStatus({ actions, messages, connected }) @@ -130,6 +133,7 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { }, [messages]) const [input, setInput] = useState('') + const [enhancing, setEnhancing] = useState(false) const dispatch = useAppDispatch() const pendingPrefill = useAppSelector(selectPendingPrefill) const [pendingAttachments, setPendingAttachments] = useState([]) @@ -301,6 +305,26 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) { }, 0) }, [pendingPrefill, dispatch]) + // Consume enhanced prompt from context when WS response arrives + useEffect(() => { + if (enhancedPrompt === null) return + setInput(enhancedPrompt) + setEnhancing(false) + clearEnhancedPrompt() + inputRef.current?.focus() + }, [enhancedPrompt, clearEnhancedPrompt]) + + // Reset enhancing spinner if the WebSocket disconnects mid-request + useEffect(() => { + if (!connected) setEnhancing(false) + }, [connected]) + + const handleEnhancePrompt = useCallback(() => { + if (!input.trim() || enhancing) return + setEnhancing(true) + enhancePrompt(input.trim()) + }, [input, enhancing, enhancePrompt]) + const handleChatReply = useCallback(( sessionId: string | undefined, displayName: string, @@ -730,6 +754,13 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) {
} variant="ghost" tooltip="Attach file" onClick={handleAttachClick} /> + : } + variant="ghost" + tooltip={enhancing ? 'Enhancing...' : 'AI Enhance'} + onClick={handleEnhancePrompt} + disabled={!input.trim() || enhancing} + />