diff --git a/src/hooks/useAutoCompactContinue.ts b/src/hooks/useAutoCompactContinue.ts index b23533792..10f20bf4b 100644 --- a/src/hooks/useAutoCompactContinue.ts +++ b/src/hooks/useAutoCompactContinue.ts @@ -41,10 +41,12 @@ export function useAutoCompactContinue() { // Check all workspaces for completed compaction for (const [workspaceId, state] of newStates) { // Detect if workspace is in "single compacted message" state + // Skip workspace-init messages since they're UI-only metadata + const cmuxMessages = state.messages.filter((m) => m.type !== "workspace-init"); const isSingleCompacted = - state.messages.length === 1 && - state.messages[0].type === "assistant" && - state.messages[0].isCompacted === true; + cmuxMessages.length === 1 && + cmuxMessages[0]?.type === "assistant" && + cmuxMessages[0].isCompacted === true; if (!isSingleCompacted) { // Workspace no longer in compacted state - no action needed @@ -74,11 +76,6 @@ export function useAutoCompactContinue() { // Mark THIS RESULT as processed before sending to prevent duplicates processedMessageIds.current.add(idForGuard); - console.log( - `[useAutoCompactContinue] Sending continue message for ${workspaceId}:`, - continueMessage - ); - // Build options and send message directly const options = buildSendMessageOptions(workspaceId); void (async () => { diff --git a/src/stores/WorkspaceStore.ts b/src/stores/WorkspaceStore.ts index 99a368928..6a77b54cb 100644 --- a/src/stores/WorkspaceStore.ts +++ b/src/stores/WorkspaceStore.ts @@ -9,7 +9,7 @@ import { updatePersistedState } from "@/hooks/usePersistedState"; import { getRetryStateKey } from "@/constants/storage"; import { CUSTOM_EVENTS } from "@/constants/events"; import { useSyncExternalStore } from "react"; -import { isCaughtUpMessage, isStreamError, isDeleteMessage } from "@/types/ipc"; +import { isCaughtUpMessage, isStreamError, isDeleteMessage, isCmuxMessage } from "@/types/ipc"; import { MapStore } from "./MapStore"; import { createDisplayUsage } from "@/utils/tokens/displayUsage"; import { WorkspaceConsumerManager } from "./WorkspaceConsumerManager"; @@ -20,6 +20,7 @@ import { getCancelledCompactionKey } from "@/constants/storage"; import { isCompactingStream, findCompactionRequestMessage } from "@/utils/compaction/handler"; export interface WorkspaceState { + name: string; // User-facing workspace name (e.g., "feature-branch") messages: DisplayedMessage[]; canInterrupt: boolean; isCompacting: boolean; @@ -108,6 +109,7 @@ export class WorkspaceStore { private caughtUp = new Map(); private historicalMessages = new Map(); private pendingStreamEvents = new Map(); + private workspaceMetadata = new Map(); // Store metadata for name lookup /** * Map of event types to their handlers. This is the single source of truth for: @@ -335,8 +337,10 @@ export class WorkspaceStore { const isCaughtUp = this.caughtUp.get(workspaceId) ?? false; const activeStreams = aggregator.getActiveStreams(); const messages = aggregator.getAllMessages(); + const metadata = this.workspaceMetadata.get(workspaceId); return { + name: metadata?.name ?? workspaceId, // Fall back to ID if metadata missing messages: aggregator.getDisplayedMessages(), canInterrupt: activeStreams.length > 0, isCompacting: aggregator.isCompacting(), @@ -730,6 +734,9 @@ export class WorkspaceStore { return; } + // Store metadata for name lookup + this.workspaceMetadata.set(workspaceId, metadata); + const aggregator = this.getOrCreateAggregator(workspaceId, metadata.createdAt); // Initialize recency cache and bump derived store immediately @@ -958,23 +965,26 @@ export class WorkspaceStore { } // Regular messages (CmuxMessage without type field) - const isCaughtUp = this.caughtUp.get(workspaceId) ?? false; - if (!isCaughtUp && "role" in data && !("type" in data)) { - // Buffer historical CmuxMessages - const historicalMsgs = this.historicalMessages.get(workspaceId) ?? []; - historicalMsgs.push(data); - this.historicalMessages.set(workspaceId, historicalMsgs); - } else if (isCaughtUp && "role" in data) { - // Process live events immediately (after history loaded) - // Check for role field to ensure this is a CmuxMessage - aggregator.handleMessage(data); - this.states.bump(workspaceId); - this.checkAndBumpRecencyIfChanged(); - } else if ("role" in data || "type" in data) { - // Unexpected: message with role/type field didn't match any condition - console.error("[WorkspaceStore] Message not processed - unexpected state", { + if (isCmuxMessage(data)) { + const isCaughtUp = this.caughtUp.get(workspaceId) ?? false; + if (!isCaughtUp) { + // Buffer historical CmuxMessages + const historicalMsgs = this.historicalMessages.get(workspaceId) ?? []; + historicalMsgs.push(data); + this.historicalMessages.set(workspaceId, historicalMsgs); + } else { + // Process live events immediately (after history loaded) + aggregator.handleMessage(data); + this.states.bump(workspaceId); + this.checkAndBumpRecencyIfChanged(); + } + return; + } + + // If we reach here, unknown message type - log for debugging + if ("role" in data || "type" in data) { + console.error("[WorkspaceStore] Unknown message type - not processed", { workspaceId, - isCaughtUp, hasRole: "role" in data, hasType: "type" in data, type: "type" in data ? (data as { type: string }).type : undefined, diff --git a/src/types/ipc.ts b/src/types/ipc.ts index 9a50745f3..498ceb940 100644 --- a/src/types/ipc.ts +++ b/src/types/ipc.ts @@ -149,6 +149,11 @@ export function isReasoningEnd(msg: WorkspaceChatMessage): msg is ReasoningEndEv return "type" in msg && msg.type === "reasoning-end"; } +// Type guard for CmuxMessage (messages with role but no type field) +export function isCmuxMessage(msg: WorkspaceChatMessage): msg is CmuxMessage { + return "role" in msg && !("type" in msg); +} + // Type guards for init events export function isInitStart( msg: WorkspaceChatMessage diff --git a/src/utils/messages/StreamingMessageAggregator.ts b/src/utils/messages/StreamingMessageAggregator.ts index d8ab95b05..81a2f3c09 100644 --- a/src/utils/messages/StreamingMessageAggregator.ts +++ b/src/utils/messages/StreamingMessageAggregator.ts @@ -14,7 +14,7 @@ import type { import type { TodoItem } from "@/types/tools"; import type { WorkspaceChatMessage, StreamErrorMessage, DeleteMessage } from "@/types/ipc"; -import { isInitStart, isInitOutput, isInitEnd } from "@/types/ipc"; +import { isInitStart, isInitOutput, isInitEnd, isCmuxMessage } from "@/types/ipc"; import type { DynamicToolPart, DynamicToolPartPending, @@ -543,7 +543,7 @@ export class StreamingMessageAggregator { // Handle regular messages (user messages, historical messages) // Check if it's a CmuxMessage (has role property but no type) - if ("role" in data && !("type" in data)) { + if (isCmuxMessage(data)) { const incomingMessage = data; // Smart replacement logic for edits: