Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
5df7ade
🤖 Simplify compaction at protocol level
ammar-agent Oct 17, 2025
1b0779d
Fix formatting
ammar-agent Oct 17, 2025
8f5938e
Update E2E test for new compaction flow
ammar-agent Oct 17, 2025
58d0695
Fix formatting
ammar-agent Oct 17, 2025
da41f63
🤖 Style compaction streaming with visual effects
ammar-agent Oct 18, 2025
d371922
Fix formatting
ammar-agent Oct 18, 2025
8b16634
🤖 Fix compaction background to cover full message area
ammar-agent Oct 18, 2025
9ce7bed
🤖 Change compaction effect to green laser scanning
ammar-agent Oct 18, 2025
a86085f
🤖 Refine compaction effect to subtle gradient wave
ammar-agent Oct 18, 2025
ea7eb23
Make compaction gradient more prominent
ammar-agent Oct 18, 2025
5380f7c
🤖 Polish compaction gradient with layered effects
ammar-agent Oct 18, 2025
0be450f
🤖 Make compaction gradient sweep continuously
ammar-agent Oct 18, 2025
a42c9d7
🤖 Enhance compaction background with shimmer and particle effects
ammar-agent Oct 18, 2025
4eed689
🤖 Add [truncated] sentinel for interrupted compaction
ammar-agent Oct 18, 2025
f5c940f
🤖 Reduce shimmer intensity and remove particles
ammar-agent Oct 18, 2025
b8c10d8
🤖 Use plan-mode color for shimmer instead of white
ammar-agent Oct 18, 2025
6a10963
🤖 Fix formatting
ammar-agent Oct 18, 2025
56cbac7
🤖 Remove test-temp submodule and add .gitignore exclusion
ammar-agent Oct 18, 2025
765a029
Replace scrolling with fade effect in compacting messages
ammar-agent Oct 18, 2025
a4381e9
Fix compacting message issues
ammar-agent Oct 18, 2025
c9d45bb
Add Ctrl+C/Ctrl+A compaction interrupt handling
ammar-agent Oct 18, 2025
98bbcc2
🤖 Fix Ctrl+C compaction cancel bug with flag-based approach
ammar-agent Oct 18, 2025
b058c26
🤖 Fix Ctrl+C compaction cancel with localStorage (reload-safe)
ammar-agent Oct 18, 2025
e2037c8
Add debug logging for compaction cancel flow
ammar-agent Oct 18, 2025
d21d577
Use compaction-request user message ID (stable across retries)
ammar-agent Oct 18, 2025
152b3d8
🤖 Add abandonPartial flag to interruptStream for clean cancellation
ammar-agent Oct 18, 2025
dd1353c
Simplify: keep compaction-request in history on cancel
ammar-agent Oct 18, 2025
4043621
Fix type errors in preload.ts
ammar-agent Oct 18, 2025
fc7851f
Fix interruptStream type declaration in ipc.ts
ammar-agent Oct 18, 2025
3fbd600
Enter edit mode on compaction-request after Ctrl+C cancel
ammar-agent Oct 18, 2025
f799239
Add startEditing method to ChatInputAPI
ammar-agent Oct 18, 2025
3611335
Use setEditingMessage directly instead of ChatInputAPI
ammar-agent Oct 18, 2025
6a8afc1
Revert ChatInput API changes - not needed for edit mode approach
ammar-agent Oct 18, 2025
6e117d6
Pass setEditingMessage to useAIViewKeybinds
ammar-agent Oct 18, 2025
fc4d0f3
Remove debug logging from WorkspaceStore
ammar-agent Oct 18, 2025
bebbc41
🤖 Refactor: Extract compaction helper functions to reduce duplication
ammar-agent Oct 18, 2025
93755a6
🤖 Fix ESLint error: type JSON.parse result in cancellation check
ammar-agent Oct 18, 2025
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ CODE_CHANGES.md
README_COMPACT_HERE.md
artifacts/
tests/e2e/tmp/

# Test temporary directories
src/test-temp-*/
tests/**/test-temp-*/

runs/

# Python
Expand Down
8 changes: 7 additions & 1 deletion src/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@ const AIViewInner: React.FC<AIViewProps> = ({
chatInputAPI,
jumpToBottom,
handleOpenTerminal,
aggregator,
setEditingMessage,
});

// Clear editing state if the message being edited no longer exists
Expand Down Expand Up @@ -523,7 +525,11 @@ const AIViewInner: React.FC<AIViewProps> = ({
? `${getModelName(currentModel)} streaming...`
: "streaming..."
}
cancelText={`hit ${formatKeybind(KEYBINDS.INTERRUPT_STREAM)} to cancel`}
cancelText={
isCompacting
? `${formatKeybind(KEYBINDS.INTERRUPT_STREAM)} cancel | ${formatKeybind(KEYBINDS.ACCEPT_EARLY_COMPACTION)} accept early`
: `hit ${formatKeybind(KEYBINDS.INTERRUPT_STREAM)} to cancel`
}
tokenCount={
activeStreamMessageId
? aggregator.getStreamingTokenCount(activeStreamMessageId)
Expand Down
19 changes: 16 additions & 3 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ const ModelDisplayWrapper = styled.div`

export interface ChatInputAPI {
focus: () => void;
restoreText: (text: string) => void;
}

export interface ChatInputProps {
Expand Down Expand Up @@ -430,12 +431,24 @@ export const ChatInput: React.FC<ChatInputProps> = ({
});
}, []);

// Method to restore text to input (used by compaction cancel)
const restoreText = useCallback(
(text: string) => {
setInput(text);
focusMessageInput();
},
[focusMessageInput]
);

// Provide API to parent via callback
useEffect(() => {
if (onReady) {
onReady({ focus: focusMessageInput });
onReady({
focus: focusMessageInput,
restoreText,
});
}
}, [onReady, focusMessageInput]);
}, [onReady, focusMessageInput, restoreText]);

useEffect(() => {
const handleGlobalKeyDown = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -948,7 +961,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({
return `Edit your message... (${formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel, ${formatKeybind(KEYBINDS.SEND_MESSAGE)} to send)`;
}
if (isCompacting) {
return `Compacting... (${formatKeybind(KEYBINDS.INTERRUPT_STREAM)} to cancel)`;
return `Compacting... (${formatKeybind(KEYBINDS.INTERRUPT_STREAM)} cancel | ${formatKeybind(KEYBINDS.ACCEPT_EARLY_COMPACTION)} accept early)`;
}

// Build hints for normal input
Expand Down
15 changes: 14 additions & 1 deletion src/components/Messages/AssistantMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { MessageWindow } from "./MessageWindow";
import { useStartHere } from "@/hooks/useStartHere";
import { COMPACTED_EMOJI } from "@/constants/ui";
import { ModelDisplay } from "./ModelDisplay";
import { CompactingMessageContent } from "./CompactingMessageContent";
import { CompactionBackground } from "./CompactionBackground";

const RawContent = styled.pre`
font-family: var(--font-monospace);
Expand Down Expand Up @@ -49,13 +51,15 @@ interface AssistantMessageProps {
message: DisplayedMessage & { type: "assistant" };
className?: string;
workspaceId?: string;
isCompacting?: boolean;
clipboardWriteText?: (data: string) => Promise<void>;
}

export const AssistantMessage: React.FC<AssistantMessageProps> = ({
message,
className,
workspaceId,
isCompacting = false,
clipboardWriteText = (data: string) => navigator.clipboard.writeText(data),
}) => {
const [showRaw, setShowRaw] = useState(false);
Expand All @@ -64,6 +68,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
const content = message.content;
const isStreaming = message.isStreaming;
const isCompacted = message.isCompacted;
const isStreamingCompaction = isStreaming && isCompacting;

// Use Start Here hook for final assistant messages
const {
Expand Down Expand Up @@ -120,7 +125,14 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({

// Streaming text gets typewriter effect
if (isStreaming) {
return <TypewriterMarkdown deltas={[content]} isComplete={false} />;
const contentElement = <TypewriterMarkdown deltas={[content]} isComplete={false} />;

// Wrap streaming compaction in special container
if (isStreamingCompaction) {
return <CompactingMessageContent>{contentElement}</CompactingMessageContent>;
}

return contentElement;
}

// Completed text renders as static content
Expand Down Expand Up @@ -154,6 +166,7 @@ export const AssistantMessage: React.FC<AssistantMessageProps> = ({
message={message}
buttons={buttons}
className={className}
backgroundEffect={isStreamingCompaction ? <CompactionBackground /> : undefined}
>
{renderContent()}
</MessageWindow>
Expand Down
43 changes: 43 additions & 0 deletions src/components/Messages/CompactingMessageContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";
import styled from "@emotion/styled";

/**
* Wrapper for compaction streaming content
* Provides max-height constraint with fade effect to imply content above
* No scrolling - content stays anchored to bottom, older content fades at top
*/

const Container = styled.div`
max-height: 300px;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-end; /* Anchor content to bottom */

/* Fade effect: content fades progressively from top to bottom */
mask-image: linear-gradient(
to bottom,
transparent 0%,
rgba(0, 0, 0, 0.3) 5%,
rgba(0, 0, 0, 0.6) 10%,
rgba(0, 0, 0, 0.85) 15%,
black 20%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
rgba(0, 0, 0, 0.3) 5%,
rgba(0, 0, 0, 0.6) 10%,
rgba(0, 0, 0, 0.85) 15%,
black 20%
);
`;

interface CompactingMessageContentProps {
children: React.ReactNode;
}

export const CompactingMessageContent: React.FC<CompactingMessageContentProps> = ({ children }) => {
return <Container>{children}</Container>;
};
85 changes: 85 additions & 0 deletions src/components/Messages/CompactionBackground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from "react";
import styled from "@emotion/styled";
import { keyframes } from "@emotion/react";

/**
* Animated background for compaction streaming
* Shimmer effect with moving gradient and particles for dynamic appearance
*/

const shimmer = keyframes`
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
`;

const gradientMove = keyframes`
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
`;

const Container = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
pointer-events: none;
border-radius: 6px;
`;

const AnimatedGradient = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
-45deg,
var(--color-plan-mode-alpha),
color-mix(in srgb, var(--color-plan-mode) 30%, transparent),
var(--color-plan-mode-alpha),
color-mix(in srgb, var(--color-plan-mode) 25%, transparent)
);
background-size: 400% 400%;
animation: ${gradientMove} 8s ease infinite;
opacity: 0.4;
`;

const ShimmerLayer = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
90deg,
transparent 0%,
transparent 40%,
var(--color-plan-mode-alpha) 50%,
transparent 60%,
transparent 100%
);
background-size: 1000px 100%;
animation: ${shimmer} 3s infinite linear;
`;

export const CompactionBackground: React.FC = () => {
return (
<Container>
<AnimatedGradient />
<ShimmerLayer />
</Container>
);
};
7 changes: 6 additions & 1 deletion src/components/Messages/MessageRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export const MessageRenderer = React.memo<MessageRendererProps>(
);
case "assistant":
return (
<AssistantMessage message={message} className={className} workspaceId={workspaceId} />
<AssistantMessage
message={message}
className={className}
workspaceId={workspaceId}
isCompacting={isCompacting}
/>
);
case "tool":
return <ToolMessage message={message} className={className} workspaceId={workspaceId} />;
Expand Down
8 changes: 8 additions & 0 deletions src/components/Messages/MessageWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { formatTimestamp } from "@/utils/ui/dateTime";
import { TooltipWrapper, Tooltip } from "../Tooltip";

const MessageBlock = styled.div<{ borderColor: string; backgroundColor?: string }>`
position: relative;
margin-bottom: 15px;
margin-top: 15px;
background: ${(props) => props.backgroundColor ?? "#1e1e1e"};
Expand All @@ -16,6 +17,8 @@ const MessageBlock = styled.div<{ borderColor: string; backgroundColor?: string
`;

const MessageHeader = styled.div`
position: relative;
z-index: 1;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.05);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
Expand Down Expand Up @@ -51,6 +54,8 @@ const ButtonGroup = styled.div`
`;

const MessageContent = styled.div`
position: relative;
z-index: 1;
padding: 12px;
`;

Expand Down Expand Up @@ -85,6 +90,7 @@ interface MessageWindowProps {
children: ReactNode;
className?: string;
rightLabel?: ReactNode;
backgroundEffect?: ReactNode; // Optional background effect (e.g., animation)
}

export const MessageWindow: React.FC<MessageWindowProps> = ({
Expand All @@ -96,6 +102,7 @@ export const MessageWindow: React.FC<MessageWindowProps> = ({
children,
className,
rightLabel,
backgroundEffect,
}) => {
const [showJson, setShowJson] = useState(false);

Expand All @@ -111,6 +118,7 @@ export const MessageWindow: React.FC<MessageWindowProps> = ({

return (
<MessageBlock borderColor={borderColor} backgroundColor={backgroundColor} className={className}>
{backgroundEffect}
<MessageHeader>
<LeftSection>
<MessageTypeLabel>{label}</MessageTypeLabel>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Messages/UserMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({
onClick: handleEdit,
disabled: isCompacting,
tooltip: isCompacting
? `Cannot edit while compacting (press ${formatKeybind(KEYBINDS.INTERRUPT_STREAM)} to cancel)`
? `Cannot edit while compacting (${formatKeybind(KEYBINDS.INTERRUPT_STREAM)} to cancel)`
: undefined,
},
]
Expand Down
8 changes: 8 additions & 0 deletions src/constants/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export function getLastThinkingByModelKey(modelName: string): string {
return `lastThinkingByModel:${modelName}`;
}

/**
* Get storage key for cancelled compaction tracking.
* Stores compaction-request user message ID to verify freshness across reloads.
*/
export function getCancelledCompactionKey(workspaceId: string): string {
return `workspace:${workspaceId}:cancelled-compaction`;
}

/**
* Get the localStorage key for the UI mode for a workspace
* Format: "mode:{workspaceId}"
Expand Down
Loading