Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .superpowers/sdd/fix-report.md
Original file line number Diff line number Diff line change
@@ -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).
118 changes: 118 additions & 0 deletions app/ui_layer/adapters/browser_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<rules>
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.
</rules>

<reasoning_protocol>
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?
</reasoning_protocol>

<anti_patterns>
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
</anti_patterns>

<output_format>
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."
</output_format>
"""
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
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 32 additions & 1 deletion app/ui_layer/browser/frontend/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -114,6 +114,9 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) {
loadOlderMessages,
hasMoreMessages,
loadingOlderMessages,
enhancedPrompt,
enhancePrompt,
clearEnhancedPrompt,
} = useWebSocket()

const status = useDerivedAgentStatus({ actions, messages, connected })
Expand All @@ -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<PendingAttachment[]>([])
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -730,6 +754,13 @@ export function Chat({ livingUIId, placeholder, emptyMessage }: ChatProps) {
<div className={styles.inputArea}>
<input ref={fileInputRef} type="file" multiple className={styles.hiddenFileInput} onChange={handleFileSelect} />
<IconButton icon={<Paperclip size={18} />} variant="ghost" tooltip="Attach file" onClick={handleAttachClick} />
<IconButton
icon={enhancing ? <Loader2 size={18} className={styles.uploadingSpinner} /> : <Sparkles size={18} />}
variant="ghost"
tooltip={enhancing ? 'Enhancing...' : 'AI Enhance'}
onClick={handleEnhancePrompt}
disabled={!input.trim() || enhancing}
/>

<div className={styles.micGroup} ref={langDropdownRef}>
<button
Expand Down
24 changes: 24 additions & 0 deletions app/ui_layer/browser/frontend/src/contexts/WebSocketContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ interface WebSocketState {
lastSeenMessageId: string | null
// Reply state for reply-to-chat/task feature
replyTarget: ReplyTarget | null
// Enhanced prompt result from backend LLM
enhancedPrompt: string | null
}

interface WebSocketContextType extends WebSocketState {
Expand Down Expand Up @@ -197,6 +199,9 @@ interface WebSocketContextType extends WebSocketState {
// Reply-to-chat/task methods
setReplyTarget: (target: ReplyTarget) => void
clearReplyTarget: () => void
// Enhance prompt
enhancePrompt: (content: string) => void
clearEnhancedPrompt: () => void
// Chat pagination
loadOlderMessages: () => void
// Action pagination
Expand Down Expand Up @@ -239,6 +244,8 @@ const defaultState: WebSocketState = {
lastSeenMessageId: getInitialLastSeenMessageId(),
// Reply state
replyTarget: null,
// Enhance prompt result
enhancedPrompt: null,
}

const WebSocketContext = createContext<WebSocketContextType | undefined>(undefined)
Expand Down Expand Up @@ -316,6 +323,12 @@ export function WebSocketProvider({ children }: { children: ReactNode }) {
if (path) navigateRef.current(path)
break
}

case 'prompt_enhanced': {
const { content } = msg as unknown as { type: string; content: string }
setState(prev => ({ ...prev, enhancedPrompt: content }))
break
}
}
}, [])

Expand Down Expand Up @@ -449,6 +462,14 @@ export function WebSocketProvider({ children }: { children: ReactNode }) {
}
}, [dispatch])

const enhancePrompt = useCallback((content: string) => {
sendOrQueue(JSON.stringify({ type: 'enhance_prompt', content }))
}, [sendOrQueue])

const clearEnhancedPrompt = useCallback(() => {
setState(prev => ({ ...prev, enhancedPrompt: null }))
}, [])

const sendOptionClick = useCallback((value: string, sessionId?: string, messageId?: string) => {
// Optimistically record the selection in local state so the UI lock
// survives virtualizer remounts, WS reconnects, and parent re-renders
Expand Down Expand Up @@ -728,6 +749,9 @@ export function WebSocketProvider({ children }: { children: ReactNode }) {
startLocalLLM,
requestSuggestedModels,
pullOllamaModel,
enhancedPrompt: state.enhancedPrompt,
enhancePrompt,
clearEnhancedPrompt,
sendOptionClick,
uploadAgentProfilePicture,
removeAgentProfilePicture,
Expand Down
1 change: 1 addition & 0 deletions app/ui_layer/browser/frontend/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export type WSMessageType =
| 'living_ui_data_changed'
| 'living_ui_question'
| 'living_ui_error'
| 'prompt_enhanced'

export interface WSMessage {
type: WSMessageType
Expand Down