/ src / commands / compact.ts
compact.ts
 1  import { Command } from '../commands.js'
 2  import { getContext } from '../context.js'
 3  import { getMessagesGetter, getMessagesSetter } from '../messages.js'
 4  import { API_ERROR_MESSAGE_PREFIX, querySonnet } from '../services/claude.js'
 5  import {
 6    createUserMessage,
 7    normalizeMessagesForAPI,
 8  } from '../utils/messages.js'
 9  import { getCodeStyle } from '../utils/style.js'
10  import { clearTerminal } from '../utils/terminal.js'
11  
12  const compact = {
13    type: 'local',
14    name: 'compact',
15    description: 'Clear conversation history but keep a summary in context',
16    isEnabled: true,
17    isHidden: false,
18    async call(
19      _,
20      {
21        options: { tools, slowAndCapableModel },
22        abortController,
23        setForkConvoWithMessagesOnTheNextRender,
24      },
25    ) {
26      // Get existing messages before clearing
27      const messages = getMessagesGetter()()
28  
29      // Add summary request as a new message
30      const summaryRequest = createUserMessage(
31        "Provide a detailed but concise summary of our conversation above. Focus on information that would be helpful for continuing the conversation, including what we did, what we're doing, which files we're working on, and what we're going to do next.",
32      )
33  
34      const summaryResponse = await querySonnet(
35        normalizeMessagesForAPI([...messages, summaryRequest]),
36        ['You are a helpful AI assistant tasked with summarizing conversations.'],
37        0,
38        tools,
39        abortController.signal,
40        {
41          dangerouslySkipPermissions: false,
42          model: slowAndCapableModel,
43          prependCLISysprompt: true,
44        },
45      )
46  
47      // Extract summary from response, throw if we can't get it
48      const content = summaryResponse.message.content
49      const summary =
50        typeof content === 'string'
51          ? content
52          : content.length > 0 && content[0]?.type === 'text'
53            ? content[0].text
54            : null
55  
56      if (!summary) {
57        throw new Error(
58          `Failed to generate conversation summary - response did not contain valid text content - ${summaryResponse}`,
59        )
60      } else if (summary.startsWith(API_ERROR_MESSAGE_PREFIX)) {
61        throw new Error(summary)
62      }
63  
64      // Substitute low token usage info so that the context-size UI warning goes
65      // away. The actual numbers don't matter too much: `countTokens` checks the
66      // most recent assistant message for usage numbers, so this estimate will
67      // be overridden quickly.
68      summaryResponse.message.usage = {
69        input_tokens: 0,
70        output_tokens: summaryResponse.message.usage.output_tokens,
71        cache_creation_input_tokens: 0,
72        cache_read_input_tokens: 0,
73      }
74  
75      // Clear screen and messages
76      await clearTerminal()
77      getMessagesSetter()([])
78      setForkConvoWithMessagesOnTheNextRender([
79        createUserMessage(
80          `Use the /compact command to clear the conversation history, and start a new conversation with the summary in context.`,
81        ),
82        summaryResponse,
83      ])
84      getContext.cache.clear?.()
85      getCodeStyle.cache.clear?.()
86  
87      return '' // not used, just for typesafety. TODO: avoid this hack
88    },
89    userFacingName() {
90      return 'compact'
91    },
92  } satisfies Command
93  
94  export default compact