diff --git a/packages/ui/src/components/chat/multi-task-chat.tsx b/packages/ui/src/components/chat/multi-task-chat.tsx
index 38fa1a6..3d85cd8 100644
--- a/packages/ui/src/components/chat/multi-task-chat.tsx
+++ b/packages/ui/src/components/chat/multi-task-chat.tsx
@@ -10,6 +10,7 @@ import { addToTaskQueue, getSoloState, setActiveTaskId, toggleAutonomous, toggle
import { getLogger } from "@/lib/logger";
import { clearCompactionSuggestion, getCompactionSuggestion } from "@/stores/session-compaction";
import { emitSessionSidebarRequest } from "@/lib/session-sidebar-events";
+import { detectAgentWorkingState, getAgentStatusMessage } from "@/lib/agent-status-detection";
import {
Command,
Plus,
@@ -216,7 +217,36 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
const store = messageStore();
const lastMsg = store.getMessage(ids[ids.length - 1]);
- return lastMsg?.role === "assistant" && (lastMsg.status === "streaming" || lastMsg.status === "sending");
+
+ // Basic check: streaming or sending status
+ if (lastMsg?.role === "assistant" && (lastMsg.status === "streaming" || lastMsg.status === "sending")) {
+ return true;
+ }
+
+ // Enhanced check: semantic detection for "standby", "processing" messages
+ // This catches Ollama models that output status messages and pause
+ if (lastMsg?.role === "assistant") {
+ const workingState = detectAgentWorkingState(lastMsg);
+ return workingState.isWorking;
+ }
+
+ return false;
+ });
+
+ // Get dynamic status message for display
+ const agentStatusMessage = createMemo(() => {
+ const ids = filteredMessageIds();
+ if (ids.length === 0) return "THINKING";
+
+ const store = messageStore();
+ const lastMsg = store.getMessage(ids[ids.length - 1]);
+
+ if (!lastMsg || lastMsg.role !== "assistant") {
+ return "THINKING";
+ }
+
+ const statusMsg = getAgentStatusMessage(lastMsg);
+ return statusMsg?.toUpperCase() || "THINKING";
});
// Auto-scroll during streaming - DISABLED for performance testing
@@ -539,7 +569,7 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
- Streaming
+ {agentStatusMessage()}
{formatTokenTotal(tokenStats().used)}
@@ -846,7 +876,7 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
- {isAgentThinking() ? "THINKING" : "SENDING"}
+ {isSending() ? "SENDING" : agentStatusMessage()}
diff --git a/packages/ui/src/lib/agent-status-detection.ts b/packages/ui/src/lib/agent-status-detection.ts
new file mode 100644
index 0000000..27e82f9
--- /dev/null
+++ b/packages/ui/src/lib/agent-status-detection.ts
@@ -0,0 +1,249 @@
+/**
+ * Agent Status Detection Module
+ *
+ * Provides intelligent detection of when an agent is still "working" even after
+ * streaming has technically completed. This handles cases where:
+ * 1. Agent outputs "standby", "processing", "working" messages
+ * 2. Agent is in multi-step reasoning mode
+ * 3. Ollama models pause between thinking and output phases
+ */
+
+import type { MessageRecord } from "../stores/message-v2/types"
+
+// Keywords that indicate the agent is still processing
+const WORKING_KEYWORDS = [
+ "standby",
+ "stand by",
+ "processing",
+ "please wait",
+ "working on",
+ "analyzing",
+ "thinking",
+ "computing",
+ "calculating",
+ "evaluating",
+ "generating",
+ "preparing",
+ "loading",
+ "fetching",
+ "retrieving",
+ "in progress",
+ "one moment",
+ "hold on",
+ "just a sec",
+ "give me a moment",
+ "let me",
+ "i'll",
+ "i will",
+ "checking",
+ "scanning",
+ "searching",
+ "looking",
+ "finding"
+] as const
+
+// Keywords that indicate the agent has finished
+const COMPLETION_KEYWORDS = [
+ "here is",
+ "here's",
+ "here are",
+ "done",
+ "complete",
+ "finished",
+ "result",
+ "solution",
+ "answer",
+ "output",
+ "summary",
+ "conclusion",
+ "final",
+ "successfully",
+ "implemented",
+ "fixed",
+ "resolved",
+ "created",
+ "updated"
+] as const
+
+// Patterns that strongly indicate agent is still working
+const WORKING_PATTERNS = [
+ /stand\s*by/i,
+ /processing\s*(complete\s*)?data/i,
+ /please\s+wait/i,
+ /working\s+on/i,
+ /analyzing/i,
+ /\bwait\b/i,
+ /\bone\s+moment\b/i,
+ /\bhold\s+on\b/i,
+ /\.\.\.\s*$/, // Ends with ellipsis
+ /…\s*$/, // Ends with unicode ellipsis
+] as const
+
+/**
+ * Extracts text content from a message's parts
+ */
+function extractMessageText(message: MessageRecord): string {
+ const textParts: string[] = []
+
+ for (const partId of message.partIds) {
+ const part = message.parts[partId]
+ if (part?.data) {
+ const data = part.data as Record
+ if (data.type === "text" && typeof data.text === "string") {
+ textParts.push(data.text)
+ }
+ }
+ }
+
+ return textParts.join("\n")
+}
+
+/**
+ * Gets the last N characters of a message for keyword detection
+ */
+function getRecentContent(message: MessageRecord, charLimit = 500): string {
+ const fullText = extractMessageText(message)
+ if (fullText.length <= charLimit) {
+ return fullText.toLowerCase()
+ }
+ return fullText.slice(-charLimit).toLowerCase()
+}
+
+/**
+ * Checks if the message content indicates the agent is still working
+ */
+export function detectAgentWorkingState(message: MessageRecord | null | undefined): {
+ isWorking: boolean
+ reason?: string
+ confidence: "high" | "medium" | "low"
+} {
+ if (!message) {
+ return { isWorking: false, confidence: "high" }
+ }
+
+ // If message status is streaming or sending, definitely working
+ if (message.status === "streaming" || message.status === "sending") {
+ return { isWorking: true, reason: "Active streaming", confidence: "high" }
+ }
+
+ // Get recent content to analyze
+ const recentContent = getRecentContent(message)
+
+ if (!recentContent) {
+ return { isWorking: false, confidence: "high" }
+ }
+
+ // Check for working patterns with high confidence
+ for (const pattern of WORKING_PATTERNS) {
+ if (pattern.test(recentContent)) {
+ return {
+ isWorking: true,
+ reason: `Pattern match: ${pattern.source}`,
+ confidence: "high"
+ }
+ }
+ }
+
+ // Check if recent content ends with working keywords
+ const lastLine = recentContent.split("\n").pop()?.trim() || ""
+
+ for (const keyword of WORKING_KEYWORDS) {
+ if (lastLine.includes(keyword)) {
+ // Check if there's also a completion keyword nearby
+ const hasCompletionNearby = COMPLETION_KEYWORDS.some(ck =>
+ recentContent.slice(-200).includes(ck)
+ )
+
+ if (!hasCompletionNearby) {
+ return {
+ isWorking: true,
+ reason: `Working keyword: "${keyword}"`,
+ confidence: "medium"
+ }
+ }
+ }
+ }
+
+ // Check message age - if very recent and short, might still be working
+ const now = Date.now()
+ const messageAge = now - message.updatedAt
+ const contentLength = extractMessageText(message).length
+
+ // If message was updated very recently (< 2s) and content is short
+ if (messageAge < 2000 && contentLength < 100) {
+ return {
+ isWorking: true,
+ reason: "Recently updated with short content",
+ confidence: "low"
+ }
+ }
+
+ return { isWorking: false, confidence: "high" }
+}
+
+/**
+ * Check if the last assistant message indicates agent is still conceptually working
+ */
+export function isAgentConceptuallyThinking(
+ messages: MessageRecord[],
+ lastAssistantMessage: MessageRecord | null | undefined
+): boolean {
+ if (!lastAssistantMessage) {
+ return false
+ }
+
+ // Check if message status indicates active work
+ if (lastAssistantMessage.status === "streaming" ||
+ lastAssistantMessage.status === "sending") {
+ return true
+ }
+
+ // Use semantic detection
+ const workingState = detectAgentWorkingState(lastAssistantMessage)
+ return workingState.isWorking
+}
+
+/**
+ * Get a user-friendly status message for the current agent state
+ */
+export function getAgentStatusMessage(
+ message: MessageRecord | null | undefined
+): string | null {
+ if (!message) {
+ return null
+ }
+
+ const workingState = detectAgentWorkingState(message)
+
+ if (!workingState.isWorking) {
+ return null
+ }
+
+ if (message.status === "streaming") {
+ return "Streaming..."
+ }
+
+ if (message.status === "sending") {
+ return "Sending..."
+ }
+
+ // Based on reason
+ if (workingState.reason?.includes("standby") ||
+ workingState.reason?.includes("stand by")) {
+ return "Agent processing..."
+ }
+
+ if (workingState.reason?.includes("processing")) {
+ return "Processing..."
+ }
+
+ if (workingState.reason?.includes("analyzing")) {
+ return "Analyzing..."
+ }
+
+ if (workingState.reason?.includes("ellipsis")) {
+ return "Thinking..."
+ }
+
+ return "Working..."
+}
diff --git a/packages/ui/src/stores/session-status.ts b/packages/ui/src/stores/session-status.ts
index 770779c..108d399 100644
--- a/packages/ui/src/stores/session-status.ts
+++ b/packages/ui/src/stores/session-status.ts
@@ -4,6 +4,7 @@ import type { MessageRecord } from "./message-v2/types"
import { sessions } from "./sessions"
import { getSessionCompactionState } from "./session-compaction"
import { messageStoreBus } from "./message-v2/bus"
+import { detectAgentWorkingState } from "../lib/agent-status-detection"
function getSession(instanceId: string, sessionId: string): Session | null {
const instanceSessions = sessions().get(instanceId)
@@ -159,6 +160,15 @@ export function getSessionStatus(instanceId: string, sessionId: string): Session
return "working"
}
+ // Enhanced: Check if last assistant message content suggests agent is still working
+ // This catches Ollama models that output "standby", "processing" messages and pause
+ if (lastRecord && lastRecord.role === "assistant") {
+ const workingState = detectAgentWorkingState(lastRecord)
+ if (workingState.isWorking && workingState.confidence !== "low") {
+ return "working"
+ }
+ }
+
return "idle"
}