-
Notifications
You must be signed in to change notification settings - Fork 3k
Expand file tree
/
Copy pathcompressCommand.ts
More file actions
214 lines (196 loc) · 6.2 KB
/
Copy pathcompressCommand.ts
File metadata and controls
214 lines (196 loc) · 6.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { HistoryItemCompression } from '../types.js';
import { MessageType } from '../types.js';
import type { SlashCommand } from './types.js';
import { CommandKind } from './types.js';
import { t } from '../../i18n/index.js';
// Cap user-supplied compression instructions. The compression side-query has
// no input-truncation retry today, so an unbounded instruction string would
// inflate the side-query prompt and risk a PTL the compaction path can't
// recover from. 2000 chars is generous for human-typed focus directives
// without exposing that failure mode.
const MAX_COMPRESS_INSTRUCTIONS_CHARS = 2000;
export const compressCommand: SlashCommand = {
name: 'compress',
altNames: ['summarize'],
get description() {
return t('Compresses the context by replacing it with a summary.');
},
kind: CommandKind.BUILT_IN,
supportedModes: ['interactive', 'non_interactive', 'acp'] as const,
action: async (context) => {
const { ui } = context;
const executionMode = context.executionMode ?? 'interactive';
const abortSignal = context.abortSignal;
if (executionMode === 'interactive' && ui.pendingItem) {
ui.addItem(
{
type: MessageType.ERROR,
text: t('Already compressing, wait for previous request to complete'),
},
Date.now(),
);
return;
}
const pendingMessage: HistoryItemCompression = {
type: MessageType.COMPRESSION,
compression: {
isPending: true,
originalTokenCount: null,
newTokenCount: null,
compressionStatus: null,
},
};
const config = context.services.config;
const geminiClient = config?.getGeminiClient();
if (!config || !geminiClient) {
return {
type: 'message',
messageType: 'error',
content: t('Config not loaded.'),
};
}
const rawArgs = context.invocation?.args?.trim() ?? '';
const wasTruncated = rawArgs.length > MAX_COMPRESS_INSTRUCTIONS_CHARS;
const customInstructions = rawArgs
? rawArgs.slice(0, MAX_COMPRESS_INSTRUCTIONS_CHARS)
: undefined;
// Surface the silent cap so a user pasting an over-long focus directive
// knows their instructions were clipped mid-text rather than silently
// changing the summary's behaviour.
const truncationNotice = wasTruncated
? t('Compression instructions were truncated to {{max}} characters.', {
max: String(MAX_COMPRESS_INSTRUCTIONS_CHARS),
})
: undefined;
const doCompress = async () => {
const promptId = `compress-${Date.now()}`;
return await geminiClient.tryCompressChat(
promptId,
true,
abortSignal,
customInstructions,
);
};
if (executionMode === 'acp') {
const messages = async function* () {
try {
if (truncationNotice) {
yield {
messageType: 'info' as const,
content: truncationNotice,
};
}
yield {
messageType: 'info' as const,
content: 'Compressing context...',
};
const compressed = await doCompress();
if (!compressed) {
yield {
messageType: 'error' as const,
content: t('Failed to compress chat history.'),
};
return;
}
yield {
messageType: 'info' as const,
content: `Context compressed (${compressed.originalTokenCount} -> ${compressed.newTokenCount}).`,
};
} catch (e) {
yield {
messageType: 'error' as const,
content: t('Failed to compress chat history: {{error}}', {
error: e instanceof Error ? e.message : String(e),
}),
};
}
};
return { type: 'stream_messages', messages: messages() };
}
try {
if (executionMode === 'interactive') {
if (truncationNotice) {
ui.addItem(
{ type: MessageType.INFO, text: truncationNotice },
Date.now(),
);
}
ui.setPendingItem(pendingMessage);
}
const compressed = await doCompress();
if (abortSignal?.aborted) {
return;
}
if (!compressed) {
if (executionMode === 'interactive') {
ui.addItem(
{
type: MessageType.ERROR,
text: t('Failed to compress chat history.'),
},
Date.now(),
);
return;
}
return {
type: 'message',
messageType: 'error',
content: t('Failed to compress chat history.'),
};
}
if (executionMode === 'interactive') {
ui.addItem(
{
type: MessageType.COMPRESSION,
compression: {
isPending: false,
originalTokenCount: compressed.originalTokenCount,
newTokenCount: compressed.newTokenCount,
compressionStatus: compressed.compressionStatus,
},
} as HistoryItemCompression,
Date.now(),
);
return;
}
return {
type: 'message',
messageType: 'info',
content: `${truncationNotice ? `${truncationNotice} ` : ''}Context compressed (${compressed.originalTokenCount} -> ${compressed.newTokenCount}).`,
};
} catch (e) {
// If cancelled via ESC, don't show error — cancelSlashCommand already handled UI
if (abortSignal?.aborted) {
return;
}
if (executionMode === 'interactive') {
ui.addItem(
{
type: MessageType.ERROR,
text: t('Failed to compress chat history: {{error}}', {
error: e instanceof Error ? e.message : String(e),
}),
},
Date.now(),
);
return;
}
return {
type: 'message',
messageType: 'error',
content: t('Failed to compress chat history: {{error}}', {
error: e instanceof Error ? e.message : String(e),
}),
};
} finally {
if (executionMode === 'interactive') {
ui.setPendingItem(null);
}
}
},
};