Skip to content

Commit 58963d5

Browse files
authored
feat: autocompact working (#586)
1 parent 4f46438 commit 58963d5

File tree

16 files changed

+539
-94
lines changed

16 files changed

+539
-94
lines changed

apps/twig/src/main/services/agent/service.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,8 @@ export class AgentService extends TypedEventEmitter<AgentServiceEvents> {
367367
});
368368

369369
try {
370-
const { clientStreams } = await agent.run(taskId, taskRunId);
370+
const acpConnection = await agent.run(taskId, taskRunId);
371+
const { clientStreams } = acpConnection;
371372

372373
const connection = this.createClientConnection(
373374
taskRunId,
@@ -956,6 +957,30 @@ For git operations while detached:
956957
log.info("SDK session ID captured", { sessionId, sdkSessionId });
957958
}
958959
}
960+
961+
// Forward extension notifications to the renderer as ACP messages
962+
// The extNotification callback doesn't write to the stream, so we need
963+
// to manually emit these to the renderer
964+
if (
965+
method === "_posthog/status" ||
966+
method === "_posthog/task_notification" ||
967+
method === "_posthog/compact_boundary"
968+
) {
969+
log.info("Forwarding extension notification to renderer", {
970+
method,
971+
taskRunId,
972+
});
973+
const acpMessage: AcpMessage = {
974+
type: "acp_message",
975+
ts: Date.now(),
976+
message: {
977+
jsonrpc: "2.0",
978+
method,
979+
params,
980+
} as AcpMessage["message"],
981+
};
982+
emitToRenderer(acpMessage);
983+
}
959984
},
960985
};
961986

apps/twig/src/renderer/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ErrorBoundary } from "@components/ErrorBoundary";
12
import { LoginTransition } from "@components/LoginTransition";
23
import { MainLayout } from "@components/MainLayout";
34
import { AuthScreen } from "@features/auth/components/AuthScreen";
@@ -138,7 +139,7 @@ function App() {
138139
}
139140

140141
return (
141-
<>
142+
<ErrorBoundary name="App">
142143
<AnimatePresence mode="wait">
143144
{!isAuthenticated ? (
144145
<motion.div
@@ -164,7 +165,7 @@ function App() {
164165
isAnimating={showTransition}
165166
onComplete={handleTransitionComplete}
166167
/>
167-
</>
168+
</ErrorBoundary>
168169
);
169170
}
170171

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { Warning } from "@phosphor-icons/react";
2+
import { Box, Button, Callout, Flex, Text } from "@radix-ui/themes";
3+
import { logger } from "@renderer/lib/logger";
4+
import { Component, type ErrorInfo, type ReactNode } from "react";
5+
6+
const log = logger.scope("error-boundary");
7+
8+
interface Props {
9+
children: ReactNode;
10+
fallback?: ReactNode;
11+
/** Optional name to identify which boundary caught the error */
12+
name?: string;
13+
}
14+
15+
interface State {
16+
hasError: boolean;
17+
error: Error | null;
18+
}
19+
20+
export class ErrorBoundary extends Component<Props, State> {
21+
constructor(props: Props) {
22+
super(props);
23+
this.state = { hasError: false, error: null };
24+
}
25+
26+
static getDerivedStateFromError(error: Error): State {
27+
return { hasError: true, error };
28+
}
29+
30+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
31+
log.error("Error caught by boundary", {
32+
name: this.props.name,
33+
error: error.message,
34+
stack: error.stack,
35+
componentStack: errorInfo.componentStack,
36+
});
37+
}
38+
39+
handleRetry = () => {
40+
this.setState({ hasError: false, error: null });
41+
};
42+
43+
render() {
44+
if (this.state.hasError) {
45+
if (this.props.fallback) {
46+
return this.props.fallback;
47+
}
48+
49+
return (
50+
<Box p="4">
51+
<Callout.Root color="red" size="2">
52+
<Callout.Icon>
53+
<Warning weight="fill" />
54+
</Callout.Icon>
55+
<Callout.Text>
56+
<Flex direction="column" gap="2">
57+
<Text weight="medium">Something went wrong</Text>
58+
<Text size="1" className="text-gray-11">
59+
{this.state.error?.message || "An unexpected error occurred"}
60+
</Text>
61+
<Flex gap="2" mt="2">
62+
<Button size="1" variant="soft" onClick={this.handleRetry}>
63+
Try again
64+
</Button>
65+
</Flex>
66+
</Flex>
67+
</Callout.Text>
68+
</Callout.Root>
69+
</Box>
70+
);
71+
}
72+
73+
return this.props.children;
74+
}
75+
}

apps/twig/src/renderer/features/message-editor/components/ModeIndicatorInput.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,29 +35,31 @@ export function ModeIndicatorInput({ mode }: ModeIndicatorInputProps) {
3535
const config = modeConfig[mode];
3636

3737
return (
38-
<Flex align="center" gap="1" py="1">
39-
<Text
40-
size="1"
41-
style={{
42-
color: config.colorVar,
43-
fontFamily: "monospace",
44-
display: "flex",
45-
alignItems: "center",
46-
gap: "4px",
47-
}}
48-
>
49-
{config.icon}
50-
{config.label}
51-
</Text>
52-
<Text
53-
size="1"
54-
style={{
55-
color: "var(--gray-9)",
56-
fontFamily: "monospace",
57-
}}
58-
>
59-
(shift+tab to cycle)
60-
</Text>
38+
<Flex align="center" justify="between" py="1">
39+
<Flex align="center" gap="1">
40+
<Text
41+
size="1"
42+
style={{
43+
color: config.colorVar,
44+
fontFamily: "monospace",
45+
display: "flex",
46+
alignItems: "center",
47+
gap: "4px",
48+
}}
49+
>
50+
{config.icon}
51+
{config.label}
52+
</Text>
53+
<Text
54+
size="1"
55+
style={{
56+
color: "var(--gray-9)",
57+
fontFamily: "monospace",
58+
}}
59+
>
60+
(shift+tab to cycle)
61+
</Text>
62+
</Flex>
6163
</Flex>
6264
);
6365
}

apps/twig/src/renderer/features/sessions/components/ConversationView.tsx

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,18 @@ const TurnView = memo(function TurnView({
188188

189189
const showUserMessage = turn.userContent.trim().length > 0;
190190

191+
// Check if a compacting status should show as complete
192+
// (complete if there are items after it in the turn)
193+
const isCompactingComplete = (index: number, item: RenderItem) => {
194+
if (
195+
item.sessionUpdate === "status" &&
196+
(item as { status?: string }).status === "compacting"
197+
) {
198+
return index < turn.items.length - 1;
199+
}
200+
return false;
201+
};
202+
191203
return (
192204
<Box className="flex flex-col gap-2">
193205
{showUserMessage &&
@@ -196,15 +208,23 @@ const TurnView = memo(function TurnView({
196208
) : (
197209
<UserMessage content={turn.userContent} />
198210
))}
199-
{turn.items.map((item, i) => (
200-
<SessionUpdateView
201-
key={`${item.sessionUpdate}-${i}`}
202-
item={item}
203-
toolCalls={turn.toolCalls}
204-
taskId={taskId}
205-
turnCancelled={wasCancelled}
206-
/>
207-
))}
211+
{turn.items.map((item, i) => {
212+
// For status items, compute isComplete at render time
213+
const renderItem =
214+
item.sessionUpdate === "status" &&
215+
(item as { status?: string }).status === "compacting"
216+
? { ...item, isComplete: isCompactingComplete(i, item) }
217+
: item;
218+
return (
219+
<SessionUpdateView
220+
key={`${item.sessionUpdate}-${i}`}
221+
item={renderItem}
222+
toolCalls={turn.toolCalls}
223+
taskId={taskId}
224+
turnCancelled={wasCancelled}
225+
/>
226+
);
227+
})}
208228
{showGitResult && repoPath && gitAction.actionType && (
209229
<GitActionResult
210230
actionType={gitAction.actionType}
@@ -321,6 +341,59 @@ function buildConversationItems(events: AcpMessage[]): ConversationItem[] {
321341
});
322342
}
323343
}
344+
345+
// Compact boundary messages
346+
if (
347+
isJsonRpcNotification(msg) &&
348+
msg.method === "_posthog/compact_boundary" &&
349+
currentTurn
350+
) {
351+
const params = msg.params as {
352+
trigger: "manual" | "auto";
353+
preTokens: number;
354+
};
355+
currentTurn.items.push({
356+
sessionUpdate: "compact_boundary",
357+
trigger: params.trigger,
358+
preTokens: params.preTokens,
359+
});
360+
}
361+
362+
// Status messages (e.g., compacting in progress)
363+
if (
364+
isJsonRpcNotification(msg) &&
365+
msg.method === "_posthog/status" &&
366+
currentTurn
367+
) {
368+
const params = msg.params as {
369+
status: string;
370+
};
371+
currentTurn.items.push({
372+
sessionUpdate: "status",
373+
status: params.status,
374+
});
375+
}
376+
377+
// Task notification messages (background task completion)
378+
if (
379+
isJsonRpcNotification(msg) &&
380+
msg.method === "_posthog/task_notification" &&
381+
currentTurn
382+
) {
383+
const params = msg.params as {
384+
taskId: string;
385+
status: "completed" | "failed" | "stopped";
386+
summary: string;
387+
outputFile: string;
388+
};
389+
currentTurn.items.push({
390+
sessionUpdate: "task_notification",
391+
taskId: params.taskId,
392+
status: params.status,
393+
summary: params.summary,
394+
outputFile: params.outputFile,
395+
});
396+
}
324397
}
325398

326399
return items;
@@ -391,6 +464,24 @@ function processSessionUpdate(turn: Turn, update: SessionUpdate) {
391464
case "current_mode_update":
392465
turn.items.push(update);
393466
break;
467+
468+
// Handle custom session updates
469+
default: {
470+
// Check for our custom session update types
471+
const customUpdate = update as unknown as {
472+
sessionUpdate: string;
473+
status?: string;
474+
errorType?: string;
475+
message?: string;
476+
};
477+
if (
478+
customUpdate.sessionUpdate === "status" ||
479+
customUpdate.sessionUpdate === "error"
480+
) {
481+
turn.items.push(customUpdate as unknown as SessionUpdate);
482+
}
483+
break;
484+
}
394485
}
395486
}
396487

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Lightning } from "@phosphor-icons/react";
2+
import { Badge, Box, Flex, Text } from "@radix-ui/themes";
3+
4+
interface CompactBoundaryViewProps {
5+
trigger: "manual" | "auto";
6+
preTokens: number;
7+
}
8+
9+
export function CompactBoundaryView({
10+
trigger,
11+
preTokens,
12+
}: CompactBoundaryViewProps) {
13+
const tokensK = Math.round(preTokens / 1000);
14+
15+
return (
16+
<Box className="my-1 border-blue-6 border-l-2 py-1 pl-3 dark:border-blue-8">
17+
<Flex align="center" gap="2">
18+
<Lightning size={14} weight="fill" className="text-blue-9" />
19+
<Text size="1" className="text-gray-11">
20+
Conversation compacted
21+
</Text>
22+
<Badge
23+
size="1"
24+
color={trigger === "auto" ? "orange" : "blue"}
25+
variant="soft"
26+
>
27+
{trigger}
28+
</Badge>
29+
<Text size="1" className="text-gray-9">
30+
(~{tokensK}K tokens summarized)
31+
</Text>
32+
</Flex>
33+
</Box>
34+
);
35+
}

0 commit comments

Comments
 (0)