Skip to content
20 changes: 9 additions & 11 deletions apps/backend/agents/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,20 +395,18 @@ async def run_agent_session(

# Extract meaningful tool input for display
if inp:
if "pattern" in inp:
tool_input_display = f"pattern: {inp['pattern']}"
elif "file_path" in inp:
fp = inp["file_path"]
if len(fp) > 50:
fp = "..." + fp[-47:]
tool_input_display = fp
if "file_path" in inp:
tool_input_display = inp["file_path"]
elif "command" in inp:
cmd = inp["command"]
if len(cmd) > 50:
cmd = cmd[:47] + "..."
tool_input_display = cmd
tool_input_display = inp["command"]
elif "pattern" in inp:
tool_input_display = f"/{inp['pattern']}/"
elif "path" in inp:
tool_input_display = inp["path"]
elif "url" in inp:
tool_input_display = inp["url"]
elif "query" in inp:
tool_input_display = f'"{inp["query"]}"'

debug(
"session",
Expand Down
30 changes: 26 additions & 4 deletions apps/backend/qa/fixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,39 @@ async def run_qa_fixer_session(
# Safely extract tool input (handles None, non-dict, etc.)
inp = get_safe_tool_input(block)

# DEBUG: Print what keys are in inp

if inp:
if "file_path" in inp:
fp = inp["file_path"]
if len(fp) > 50:
fp = "..." + fp[-47:]
if len(fp) > 80:
fp = "..." + fp[-77:]
tool_input_display = fp
elif "command" in inp:
cmd = inp["command"]
if len(cmd) > 50:
cmd = cmd[:47] + "..."
if len(cmd) > 80:
cmd = cmd[:77] + "..."
tool_input_display = cmd
elif "pattern" in inp:
pat = inp["pattern"]
if len(pat) > 60:
pat = pat[:57] + "..."
tool_input_display = f"/{pat}/"
elif "path" in inp:
p = inp["path"]
if len(p) > 80:
p = "..." + p[-77:]
tool_input_display = p
elif "url" in inp:
url = inp["url"]
if len(url) > 80:
url = url[:77] + "..."
tool_input_display = url
elif "query" in inp:
q = inp["query"]
if len(q) > 60:
q = q[:57] + "..."
tool_input_display = f'"{q}"'
Comment on lines 165 to +195
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | πŸ”΅ Trivial

🧩 Analysis chain

🏁 Script executed:

# First, let's check if these files exist and examine fixer.py around the mentioned lines
echo "=== Checking file_path handling in fixer.py (around line 170) ==="
sed -n '165,180p' apps/backend/qa/fixer.py | cat -n

echo -e "\n=== Checking file_path handling in reviewer.py (around line 228) ==="
sed -n '220,235p' apps/backend/qa/reviewer.py | cat -n

echo -e "\n=== Checking file_path handling in session.py (around line 400) ==="
sed -n '395,410p' apps/backend/agents/session.py | cat -n

Repository: AndyMik90/Auto-Claude

Length of output: 3025


🏁 Script executed:

# Search more broadly for all file_path handling with context
echo "=== All file_path assignments in fixer.py ==="
rg -n "file_path" apps/backend/qa/fixer.py -B 2 -A 2

echo -e "\n=== All file_path assignments in reviewer.py ==="
rg -n "file_path" apps/backend/qa/reviewer.py -B 2 -A 2

echo -e "\n=== All file_path assignments in session.py ==="
rg -n "file_path" apps/backend/agents/session.py -B 2 -A 2

Repository: AndyMik90/Auto-Claude

Length of output: 1195


Align file_path truncation behavior across agent modules.

fixer.py truncates file_path to 80 characters when displaying tool inputs, while reviewer.py and session.py show file_path verbatim. This inconsistency means the same tool input displays differently depending on which agent is running, creating a confusing user experience. Standardize the truncation approach across all three modules.

πŸ€– Prompt for AI Agents
In `@apps/backend/qa/fixer.py` around lines 167 - 197, The fixer module currently
truncates "file_path" before assigning tool_input_display, causing inconsistency
with reviewer.py and session.py; update the handling in the block that inspects
inp (the "file_path" branch) to stop truncating and instead set
tool_input_display = inp["file_path"] (i.e., show the full path verbatim) so the
behavior matches the other agent modules; keep the existing truncation rules for
other keys (e.g., "command", "pattern", "url", "query") unchanged.

Comment on lines 166 to +195
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Coerce tool input values to strings before truncation.

len()/slicing assumes str. If get_safe_tool_input() yields a Path, dict, or None (common for tool schemas), this can raise TypeError and abort the response stream. Convert to str (or guard None) before truncating.

πŸ› οΈ Proposed fix
-                            if "file_path" in inp:
-                                fp = inp["file_path"]
+                            if "file_path" in inp:
+                                fp = str(inp["file_path"])
                                 if len(fp) > 80:
                                     fp = "..." + fp[-77:]
                                 tool_input_display = fp
                             elif "command" in inp:
-                                cmd = inp["command"]
+                                cmd = str(inp["command"])
                                 if len(cmd) > 80:
                                     cmd = cmd[:77] + "..."
                                 tool_input_display = cmd
                             elif "pattern" in inp:
-                                pat = inp["pattern"]
+                                pat = str(inp["pattern"])
                                 if len(pat) > 60:
                                     pat = pat[:57] + "..."
                                 tool_input_display = f"/{pat}/"
                             elif "path" in inp:
-                                p = inp["path"]
+                                p = str(inp["path"])
                                 if len(p) > 80:
                                     p = "..." + p[-77:]
                                 tool_input_display = p
                             elif "url" in inp:
-                                url = inp["url"]
+                                url = str(inp["url"])
                                 if len(url) > 80:
                                     url = url[:77] + "..."
                                 tool_input_display = url
                             elif "query" in inp:
-                                q = inp["query"]
+                                q = str(inp["query"])
                                 if len(q) > 60:
                                     q = q[:57] + "..."
                                 tool_input_display = f'"{q}"'
πŸ€– Prompt for AI Agents
In `@apps/backend/qa/fixer.py` around lines 166 - 195, The truncation logic that
inspects inp["file_path"], inp["command"], inp["pattern"], inp["path"],
inp["url"], and inp["query"] assumes these values are str; coerce each extracted
value to a safe string (e.g., val = "" if val is None else str(val)) before
calling len() or slicing, then apply the existing truncation rules and assign
into tool_input_display; update the branches that set fp, cmd, pat, p, url, q to
perform this coercion to avoid TypeError for Path/dict/None inputs.


debug(
"qa_fixer",
Expand Down
15 changes: 10 additions & 5 deletions apps/backend/qa/reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,17 @@ async def run_qa_agent_session(
# Extract tool input for display
if inp:
if "file_path" in inp:
fp = inp["file_path"]
if len(fp) > 50:
fp = "..." + fp[-47:]
tool_input_display = fp
tool_input_display = inp["file_path"]
elif "command" in inp:
tool_input_display = inp["command"]
elif "pattern" in inp:
tool_input_display = f"pattern: {inp['pattern']}"
tool_input_display = f"/{inp['pattern']}/"
elif "path" in inp:
tool_input_display = inp["path"]
elif "url" in inp:
tool_input_display = inp["url"]
elif "query" in inp:
tool_input_display = f'"{inp["query"]}"'

debug(
"qa_reviewer",
Expand Down
7 changes: 6 additions & 1 deletion apps/backend/task_logger/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,12 @@ def tool_start(
)

if print_to_console:
print(f"\n[Tool: {tool_name}]", flush=True)
# Include truncated input for live status display
if display_input:
short_input = display_input
print(f"\n[Tool: {tool_name}] {short_input}", flush=True)
else:
print(f"\n[Tool: {tool_name}]", flush=True)

def tool_end(
self,
Expand Down
28 changes: 28 additions & 0 deletions apps/frontend/src/main/agent/agent-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,34 @@ export class AgentEvents {
return { phase: 'failed', message: log.trim().substring(0, 200) };
}


// Live tool action detection - update message without changing phase
// Works in all phases to show real-time tool activity
const toolMatch = log.match(/\[(?:Tool|Fixer Tool|QA Tool):\s*(\w+)\]\s*(.*)?/i);
if (toolMatch) {
const toolName = toolMatch[1];
const details = toolMatch[2]?.trim().replace(/\r$/, '') || '';

// Build message with action verb and optional details
const toolVerbs: Record<string, string> = {
'Read': 'Reading',
'Edit': 'Editing',
'Write': 'Writing',
'Glob': 'Searching files',
'Grep': 'Searching',
'Bash': 'Running',
'Task': 'Running subagent',
'WebFetch': 'Fetching',
'WebSearch': 'Searching web'
};
const verb = toolVerbs[toolName] || ('Using ' + toolName);



const message = details ? (verb + ' ' + details) : (verb + '...');
return { phase: currentPhase, message };
}

return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export function registerAgenteventsHandlers(
taskProjectId
);


const phaseToStatus: Record<string, TaskStatus | null> = {
idle: null,
planning: "in_progress",
Expand Down
60 changes: 58 additions & 2 deletions apps/frontend/src/renderer/components/TaskCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useCallback, memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive, GitPullRequest, MoreVertical } from 'lucide-react';
import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive, GitPullRequest, MoreVertical, FileText, Search, FolderSearch, Terminal, Pencil } from 'lucide-react';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
Expand Down Expand Up @@ -44,6 +44,29 @@ const CategoryIcon: Record<TaskCategory, typeof Zap> = {
testing: FileCode
};

// Data-driven tool styles for live action status
const TOOL_STYLES: Array<{
patterns: string[];
icon: typeof FileText;
color: string;
}> = [
{ patterns: ['reading'], icon: FileText, color: 'text-blue-500 bg-blue-500/10' },
{ patterns: ['searching files', 'globbing'], icon: FolderSearch, color: 'text-amber-500 bg-amber-500/10' },
{ patterns: ['searching code', 'grep'], icon: Search, color: 'text-green-500 bg-green-500/10' },
{ patterns: ['editing'], icon: Pencil, color: 'text-purple-500 bg-purple-500/10' },
{ patterns: ['writing'], icon: FileCode, color: 'text-cyan-500 bg-cyan-500/10' },
{ patterns: ['running', 'executing'], icon: Terminal, color: 'text-orange-500 bg-orange-500/10' },

This comment was marked as outdated.

];

// Helper to detect tool type from execution message and return styling
function getToolStyleFromMessage(message: string): { icon: typeof FileText; color: string } | null {
const lowerMessage = message.toLowerCase();
const match = TOOL_STYLES.find(style =>
style.patterns.some(pattern => lowerMessage.startsWith(pattern))
);
return match ? { icon: match.icon, color: match.color } : null;
}

interface TaskCardProps {
task: Task;
onClick: () => void;
Expand All @@ -61,6 +84,14 @@ function taskCardPropsAreEqual(prevProps: TaskCardProps, nextProps: TaskCardProp
}

// Compare only the fields that affect rendering
// DEBUG: Log message changes
if (prevTask.executionProgress?.message !== nextTask.executionProgress?.message) {
console.log('[TaskCard memo] Message changed:', {
taskId: prevTask.id,
prev: prevTask.executionProgress?.message,
next: nextTask.executionProgress?.message
});
}
const isEqual = (
prevTask.id === nextTask.id &&
prevTask.status === nextTask.status &&
Expand All @@ -70,6 +101,7 @@ function taskCardPropsAreEqual(prevProps: TaskCardProps, nextProps: TaskCardProp
prevTask.reviewReason === nextTask.reviewReason &&
prevTask.executionProgress?.phase === nextTask.executionProgress?.phase &&
prevTask.executionProgress?.phaseProgress === nextTask.executionProgress?.phaseProgress &&
prevTask.executionProgress?.message === nextTask.executionProgress?.message &&
prevTask.subtasks.length === nextTask.subtasks.length &&
prevTask.metadata?.category === nextTask.metadata?.category &&
prevTask.metadata?.complexity === nextTask.metadata?.complexity &&
Expand Down Expand Up @@ -104,7 +136,7 @@ export const TaskCard = memo(function TaskCard({ task, onClick, onStatusChange }
interval: null
});

const isRunning = task.status === 'in_progress';
const isRunning = task.status === 'in_progress' || task.status === 'ai_review';
const executionPhase = task.executionProgress?.phase;
const hasActiveExecution = executionPhase && executionPhase !== 'idle' && executionPhase !== 'complete' && executionPhase !== 'failed';

Expand Down Expand Up @@ -137,6 +169,20 @@ export const TaskCard = memo(function TaskCard({ task, onClick, onStatusChange }
));
}, [task.status, onStatusChange, t]);

// Memoize live action status to avoid recreating on every render
const liveActionStatus = useMemo(() => {
const message = task.executionProgress?.message;
const phase = task.executionProgress?.phase;
// Don't show live status for terminal phases (complete/failed)
if (!message || phase === 'complete' || phase === 'failed') return null;
const toolStyle = getToolStyleFromMessage(message);
return {
icon: toolStyle?.icon ?? Loader2,
colorClass: toolStyle?.color ?? 'text-muted-foreground bg-muted/50',
message,
};
}, [task.executionProgress?.message, task.executionProgress?.phase]);

// Memoized stuck check function to avoid recreating on every render
const performStuckCheck = useCallback(() => {
// IMPORTANT: If the execution phase is 'complete' or 'failed', the task is NOT stuck.
Expand Down Expand Up @@ -480,6 +526,16 @@ export const TaskCard = memo(function TaskCard({ task, onClick, onStatusChange }
</div>
)}

{/* Live action status - shows current tool activity */}
{isRunning && !isStuck && liveActionStatus && (
<div className={cn("mt-2 flex items-start gap-1.5 rounded-md px-2 py-1.5 text-xs", liveActionStatus.colorClass)}>
<liveActionStatus.icon className="h-3.5 w-3.5 shrink-0 animate-pulse mt-0.5" />
<span className="break-all leading-tight">
{liveActionStatus.message}
</span>
</div>
)}
Comment on lines 609 to 620
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Minor: Implementation differs from PR objective regarding truncation/tooltip.

The PR objective specifies "A truncated action message with a tooltip for full text", but this implementation uses break-all to show the full message. This is a valid UX choice but could make cards tall with long messages.

If truncation with tooltip is preferred per the original requirements:

πŸ’‘ Optional: Restore truncation with tooltip
-          <div className={cn("mt-2 flex items-start gap-1.5 rounded-md px-2 py-1.5 text-xs", liveActionStatus.colorClass)}>
-            <liveActionStatus.icon className="h-3.5 w-3.5 shrink-0 animate-pulse mt-0.5" />
-            <span className="break-all leading-tight">
+          <div className={cn("mt-2 flex items-center gap-1.5 rounded-md px-2 py-1 text-[10px]", liveActionStatus.colorClass)}>
+            <liveActionStatus.icon className="h-3 w-3 shrink-0 animate-pulse" />
+            <span className="truncate" title={liveActionStatus.message}>
               {liveActionStatus.message}
             </span>
           </div>
πŸ€– Prompt for AI Agents
In `@apps/frontend/src/renderer/components/TaskCard.tsx` around lines 532 - 540,
The live-action message currently uses "break-all" which displays full text; per
the PR objective change the span inside the liveActionStatus block to truncate
the message and show the full text in a tooltip: replace the span's "break-all
leading-tight" with truncation classes (e.g., "truncate" and a max-width or
container-constrained class) and wrap or attach a Tooltip component (or native
title attribute) that renders liveActionStatus.message for the full text; locate
the render block where isRunning && !isStuck && liveActionStatus and update the
span and tooltip handling accordingly so long messages stay one-line and the
tooltip shows the full content.


{/* Footer */}
<div className="mt-4 flex items-center justify-between">
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
Expand Down
10 changes: 10 additions & 0 deletions apps/frontend/src/renderer/hooks/useIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ function queueUpdate(taskId: string, update: BatchedUpdate): void {
}
}

// Live tool action messages bypass batching - apply immediately for real-time feedback
// These are short-lived status updates (Reading file..., Editing..., etc.)
if (update.progress?.message && storeActionsRef) {
const isToolActionMessage = /^(Reading|Editing|Writing|Searching|Running|Using)/.test(update.progress.message);
if (isToolActionMessage) {
storeActionsRef.updateExecutionProgress(taskId, update.progress);
return;
}
}

// For logs, accumulate rather than replace
let mergedLogs = existing.logs;
if (update.logs) {
Expand Down
4 changes: 4 additions & 0 deletions apps/frontend/src/renderer/stores/task-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ export const useTaskStore = create<TaskState>((set, get) => ({
// This prevents unnecessary re-renders from the memo comparator
const phaseChanged = progress.phase && progress.phase !== existingProgress.phase;

// DEBUG: Log message updates
if (progress.message) {
console.log('[Store] Updating executionProgress.message:', { taskId, message: progress.message, phase: progress.phase });
}
return {
...t,
executionProgress: {
Expand Down
Loading