11<template >
2- <div class =" chat-container" :class =" { 'empty-chat': messages.length === 0 }" :data-status =" status" >
2+ <div
3+ class =" chat-container"
4+ :class =" { 'empty-chat': messages.length === 0 }"
5+ :data-status =" status"
6+ >
37 <div class =" scroll-container" ref =" scrollContainerRef" >
48 <div class =" header" >
5- <button class =" btn btn-flat-2 settings-btn" @click =" $emit('open-configuration')" >
9+ <button
10+ class =" btn btn-flat-2 settings-btn"
11+ @click =" $emit('open-configuration')"
12+ >
613 <span class =" material-symbols-outlined" >settings</span >
714 <span class =" title-popup" >Settings</span >
815 </button >
1219 <p >
1320 The AI Shell can see your table schemas, and (with your permission)
1421 run {{ sqlOrCode }} to answer your questions.
15- <ExternalLink href =" https://docs.beekeeperstudio.io/user_guide/sql-ai-shell/" >Learn more</ExternalLink >.
22+ <ExternalLink
23+ href =" https://docs.beekeeperstudio.io/user_guide/sql-ai-shell/"
24+ >Learn more</ExternalLink
25+ >.
1626 </p >
1727 </div >
1828 <div class =" chat-messages" >
1929 <template v-for =" (message , index ) in messages " :key =" message .id " >
2030 <message
2131 v-if ="
22- !(message.role === 'assistant' && message.parts.find((p) => p.type === 'data-userEditedToolCall'))
23- && !(message.role === 'user' && message.parts.find((p) => p.type === 'data-editedQuery'))
24- "
32+ !(
33+ message.role === 'assistant' &&
34+ message.parts.find((p) => p.type === 'data-userEditedToolCall')
35+ ) &&
36+ !(
37+ message.role === 'user' &&
38+ message.parts.find((p) => p.type === 'data-editedQuery')
39+ )
40+ "
2541 :message =" message"
26- :status =" index === messages.length - 1 ? (status === 'ready' || status === 'error' ? 'ready' : 'processing') : 'ready'"
27- @accept-permission =" acceptPermission" @reject-permission =" handleRejectPermission" />
42+ :status ="
43+ index === messages.length - 1
44+ ? status === 'ready' || status === 'error'
45+ ? 'ready'
46+ : 'processing'
47+ : 'ready'
48+ "
49+ @accept-permission =" acceptPermission"
50+ @reject-permission =" handleRejectPermission"
51+ />
2852 </template >
2953 <div class =" message error" v-if =" isUnexpectedError" >
3054 <div class =" message-content" >
3155 Something went wrong.
3256 <div v-if =" isOllamaToolError" class =" error-hint" >
33- 💡 <strong >Hint:</strong > This might be because your Ollama model doesn't support tools. Try using a
34- different model, or switch to a different provider .
57+ 💡 <strong >Hint:</strong > This might be because your Ollama model
58+ doesn't support tools. Try using a different model .
3559 </div >
3660 <pre v-if =" !isErrorTruncated || showFullError" v-text =" error" />
3761 <pre v-else v-text =" truncatedError" />
38- <button v-if =" isErrorTruncated" @click =" showFullError = !showFullError" class =" btn show-more-btn" >
62+ <button
63+ v-if =" isErrorTruncated"
64+ @click =" showFullError = !showFullError"
65+ class =" btn show-more-btn"
66+ >
3967 {{ showFullError ? "Show less" : "Show more" }}
4068 </button >
4169 <button class =" btn" @click =" () => reload()" >
4775 <div class =" message error" v-if =" noModelError" >
4876 <div class =" message-content" >No model selected</div >
4977 </div >
50- <div class =" spinner-container" :style =" { visibility: showSpinner ? 'visible' : 'hidden' }" >
78+ <div
79+ class =" spinner-container"
80+ :style =" { visibility: showSpinner ? 'visible' : 'hidden' }"
81+ >
5182 <span class =" spinner" />
83+ <span class =" label" v-if =" compacting" >Compacting</span >
5284 </div >
5385 </div >
54- <button v-if =" !isAtBottom" @click =" scrollToBottom({ smooth: true })" class =" btn scroll-down-btn"
55- title =" Scroll to bottom" >
86+ <button
87+ v-if =" !isAtBottom"
88+ @click =" scrollToBottom({ smooth: true })"
89+ class =" btn scroll-down-btn"
90+ title =" Scroll to bottom"
91+ >
5692 <span class =" material-symbols-outlined" >keyboard_arrow_down</span >
5793 </button >
5894 </div >
5995 <div class =" chat-input-container-container" >
60- <PromptInput ref =" promptInput" storage-key =" inputHistory" :processing =" processing" :selected-model =" model"
61- @select-model =" selectModel" @manage-models =" $emit('manage-models')" @submit =" submit" @stop =" stop" />
96+ <PromptInput
97+ ref =" promptInput"
98+ storage-key =" inputHistory"
99+ :processing =" processing"
100+ :selected-model =" model"
101+ @select-model =" selectModel"
102+ @manage-models =" $emit('manage-models')"
103+ @submit =" submit"
104+ @stop =" stop"
105+ />
106+ <div
107+ v-if =" enableAutoCompact && contextLeftUntilAutoCompact <= 15"
108+ class =" auto-compact-notice"
109+ >
110+ <template v-if =" contextOverflow " >
111+ Auto-compacting on next message
112+ </template >
113+ <template v-else >
114+ Context left until auto-compact:
115+ {{ contextLeftUntilAutoCompact.toFixed(1) }}%
116+ </template >
117+ </div >
62118 </div >
63119 </div >
64120</template >
@@ -79,6 +135,7 @@ import PromptInput from "@/components/common/PromptInput.vue";
79135import { getConnectionInfo } from " @beekeeperstudio/plugin" ;
80136import ExternalLink from " @/components/common/ExternalLink.vue" ;
81137import { log } from " @beekeeperstudio/plugin" ;
138+ import { useConfigurationStore } from " @/stores/configuration" ;
82139
83140export default {
84141 name: " ChatInterface" ,
@@ -101,20 +158,7 @@ export default {
101158 },
102159
103160 setup(props ) {
104- const ai = useAI ({ initialMessages: props .initialMessages });
105-
106- return {
107- send: ai .send ,
108- abort: ai .abort ,
109- messages: ai .messages ,
110- error: ai .error ,
111- status: ai .status ,
112- acceptPermission: ai .acceptPermission ,
113- rejectPermission: ai .rejectPermission ,
114- rejectAllPendingApprovals: ai .rejectAllPendingApprovals ,
115- hasPendingApprovals: ai .hasPendingApprovals ,
116- retry: ai .retry ,
117- };
161+ return useAI ({ initialMessages: props .initialMessages });
118162 },
119163
120164 data() {
@@ -127,13 +171,22 @@ export default {
127171 },
128172
129173 computed: {
130- ... mapGetters (useChatStore , [" systemPrompt" ]),
174+ ... mapGetters (useConfigurationStore , [" enableAutoCompact" ]),
175+ ... mapGetters (useChatStore , [
176+ " systemPrompt" ,
177+ " contextOverflow" ,
178+ " contextLeftUntilAutoCompact" ,
179+ ]),
131180 ... mapWritableState (useChatStore , [" model" ]),
132181 processing() {
133182 if (this .hasPendingApprovals ) return false ;
134183 return this .status !== " ready" && this .status !== " error" ;
135184 },
136185 showSpinner() {
186+ if (this .compacting ) {
187+ return true ;
188+ }
189+
137190 return (
138191 ! this .hasPendingApprovals &&
139192 (this .status === " submitted" || this .status === " streaming" )
@@ -148,12 +201,12 @@ export default {
148201 return true ;
149202 }
150203
151- if (this .error .message .includes (' User rejected tool call' )) {
204+ if (this .error .message .includes (" User rejected tool call" )) {
152205 return false ;
153206 }
154207
155208 // User aborted request before AI got a chance to respond
156- if (this .error .message .includes (' aborted without reason' )) {
209+ if (this .error .message .includes (" aborted without reason" )) {
157210 return false ;
158211 }
159212
@@ -168,8 +221,8 @@ export default {
168221 isOllamaToolError() {
169222 if (! this .error || ! this .model ) return false ;
170223 const errorStr = this .error .toString ().toLowerCase ();
171- const isOllama = this .model .provider === ' ollama' ;
172- const hasToolError = errorStr .includes (' bad request' );
224+ const isOllama = this .model .provider === " ollama" ;
225+ const hasToolError = errorStr .includes (" bad request" );
173226 return isOllama && hasToolError ;
174227 },
175228
@@ -184,7 +237,7 @@ export default {
184237 }
185238 },
186239 },
187- ]
240+ ];
188241 },
189242 },
190243
@@ -207,9 +260,11 @@ export default {
207260
208261 async mounted() {
209262 getConnectionInfo ().then ((connection ) => {
210- if (connection .databaseType === " mongodb"
211- || connection .connectionType === " surrealdb"
212- || connection .connectionType === " redis" ) {
263+ if (
264+ connection .databaseType === " mongodb" ||
265+ connection .connectionType === " surrealdb" ||
266+ connection .connectionType === " redis"
267+ ) {
213268 this .sqlOrCode = " Code" ;
214269 }
215270 });
@@ -219,8 +274,8 @@ export default {
219274 // Calculate if we're near bottom (within 50px of bottom)
220275 const isNearBottom =
221276 scrollContainer .scrollHeight -
222- scrollContainer .scrollTop -
223- scrollContainer .clientHeight <
277+ scrollContainer .scrollTop -
278+ scrollContainer .clientHeight <
224279 50 ;
225280
226281 this .isAtBottom = isNearBottom ;
@@ -233,7 +288,7 @@ export default {
233288 methods: {
234289 ... mapActions (useInternalDataStore , [" setInternal" ]),
235290
236- submit(input : string ) {
291+ async submit(input : string ) {
237292 if (! this .model ) {
238293 // FIXME we should catch this and show it on screen
239294 this .noModelError = true ;
@@ -246,7 +301,11 @@ export default {
246301 this .rejectAllPendingApprovals ();
247302 }
248303
249- this .send (input );
304+ if (this .contextOverflow ) {
305+ await this .compact (input );
306+ } else {
307+ await this .send (input );
308+ }
250309 },
251310
252311 async reload() {
@@ -268,7 +327,7 @@ export default {
268327 if (options ?.smooth ) {
269328 this .$refs .scrollContainerRef .scrollTo ({
270329 top: this .$refs .scrollContainerRef .scrollHeight ,
271- behavior: ' smooth'
330+ behavior: " smooth" ,
272331 });
273332 } else {
274333 this .$refs .scrollContainerRef .scrollTop =
@@ -299,3 +358,15 @@ export default {
299358 },
300359};
301360 </script >
361+
362+ <style scoped>
363+ .spinner-container .label {
364+ padding-left : 1ch ;
365+ }
366+ .auto-compact-notice {
367+ width : 100% ;
368+ margin-top : 0.5rem ;
369+ text-align : right ;
370+ color : var (--text );
371+ }
372+ </style >
0 commit comments