Skip to content

Commit 468df56

Browse files
committed
UI update
1 parent 6dc0787 commit 468df56

File tree

6 files changed

+196
-33
lines changed

6 files changed

+196
-33
lines changed

src/assets/styles/_base.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
:root {
2-
font-family: "Roboto", Helvetica, Arial, sans-serif;
2+
font-family: var(--font-family, "Roboto", Helvetica, Arial, sans-serif);
33
}
44

55
* {

src/components/ChatInterface.vue

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@
99
</div>
1010
<h1 class="plugin-title">AI Shell</h1>
1111
<div class="chat-messages">
12-
<message v-for="(message, index) in messages" :key="message.id" :message="message"
13-
:pending-tool-call-ids="pendingToolCallIds"
14-
:status="index === messages.length - 1 ? (status === 'ready' || status === 'error' ? 'ready' : 'processing') : 'ready'"
15-
@accept-permission="acceptPermission" @reject-permission="handleRejectPermission" />
12+
<template v-for="(message, index) in messages" :key="message.id">
13+
<message
14+
v-if="
15+
!(message.role === 'assistant' && message.parts.find((p) => p.type === 'data-userEditedToolCall'))
16+
&& !(message.role === 'user' && message.parts.find((p) => p.type === 'data-editedQuery'))
17+
"
18+
:message="message"
19+
:pending-tool-call-ids="pendingToolCallIds"
20+
:status="index === messages.length - 1 ? (status === 'ready' || status === 'error' ? 'ready' : 'processing') : 'ready'"
21+
@accept-permission="acceptPermission" @reject-permission="handleRejectPermission" />
22+
</template>
1623
<div class="message error" v-if="isUnexpectedError">
1724
<div class="message-content">
1825
Something went wrong.
@@ -255,7 +262,7 @@ export default {
255262
256263
handleRejectPermission(options: {
257264
toolCallId: string;
258-
userEditedCode?: string;
265+
editedQuery?: string;
259266
}) {
260267
this.rejectPermission({
261268
...options,

src/components/messages/Message.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
</template>
1010
<tool-message
1111
v-else-if="isToolUIPart(part)"
12+
:message="message"
1213
:toolCall="part"
1314
:askingPermission="pendingToolCallIds.includes(part.toolCallId)"
1415
@accept="$emit('accept-permission', part.toolCallId)"
@@ -37,11 +38,12 @@
3738

3839
<script lang="ts">
3940
import { PropType } from "vue";
40-
import { isToolUIPart, UIMessage } from "ai";
41+
import { isToolUIPart } from "ai";
4142
import Markdown from "@/components/messages/Markdown.vue";
4243
import ToolMessage from "@/components/messages/ToolMessage.vue";
4344
import { clipboard } from "@beekeeperstudio/plugin";
4445
import { isEmptyUIMessage } from "@/utils";
46+
import { UIMessage } from "@/types";
4547
4648
export default {
4749
name: "Message",

src/components/messages/ToolMessage.vue

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
<template>
22
<div class="tool" :data-tool-name="name" :data-tool-state="toolCall.state" :data-tool-empty-result="isEmptyResult"
33
:data-tool-error="!!error">
4-
<div class="tool-name">{{ displayName }}</div>
4+
<div class="tool-name">
5+
{{ displayName }}
6+
<span
7+
v-if="message.parts.find((p) => p.type === 'data-toolReplacement')"
8+
class="edited-badge"
9+
title="You edited the AI's suggestion"
10+
>Edited</span>
11+
</div>
512
<div class="tool-input-container" :style="{ opacity: editing ? 0.5 : 1 }">
613
<markdown v-if="name === 'run_query'" :content="'```sql\n' +
714
(toolCall.input?.query ||
@@ -21,7 +28,7 @@
2128
<button class="btn btn-flat" @click="cancelEdit">Cancel</button>
2229
</div>
2330
</div>
24-
<div v-if="askingPermission && !editing">
31+
<div v-if="askingPermission && !editing" class="tool-permission">
2532
{{
2633
name === "run_query"
2734
? "Do you want to run this query?"
@@ -68,11 +75,16 @@ import { safeJSONStringify } from "@/utils";
6875
import RunQueryResult from "@/components/messages/tool/RunQueryResult.vue";
6976
import { isErrorContent, parseErrorContent } from "@/utils";
7077
import _ from "lodash";
78+
import { UIMessage } from "@/types";
7179
7280
export default {
7381
components: { Markdown, RunQueryResult },
7482
props: {
7583
askingPermission: Boolean,
84+
message: {
85+
type: Object as PropType<UIMessage>,
86+
required: true,
87+
},
7688
toolCall: {
7789
type: Object as PropType<ToolUIPart>,
7890
required: true,
@@ -164,7 +176,7 @@ export default {
164176
},
165177
saveEdit() {
166178
this.editing = false;
167-
this.$emit("reject", { userEditedCode: this.queryEditorValue })
179+
this.$emit("reject", { editedQuery: this.queryEditorValue })
168180
this.initialQueryEditorValue = this.queryEditorValue;
169181
},
170182
cancelEdit() {
@@ -176,8 +188,8 @@ export default {
176188
</script>
177189

178190
<style scoped>
179-
.tool-input-container {
180-
margin-bottom: 0.5rem;
191+
.tool-error {
192+
margin-top: 0.5rem;
181193
}
182194
183195
.tool-input-edit {
@@ -201,6 +213,20 @@ export default {
201213
gap: 1rem;
202214
margin-top: 1rem;
203215
}
216+
}
217+
218+
.tool-permission {
219+
margin-top: 0.5rem;
220+
}
204221
222+
.edited-badge {
223+
border: 1px solid var(--border-color);
224+
border-radius: 8px;
225+
font-size: 0.75rem;
226+
margin-left: 0.25rem;
227+
padding-inline: 0.25rem;
228+
padding-block: 0.1rem;
229+
color: var(--text-light);
230+
cursor: default;
205231
}
206232
</style>

src/composables/ai.ts

Lines changed: 121 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Chat } from "@ai-sdk/vue";
2-
import { computed, ComputedRef, watch } from "vue";
2+
import { computed, ComputedRef, ref, Ref, watch } from "vue";
33
import { AvailableProviders, AvailableModels } from "@/config";
44
import { tools, userRejectedToolCall } from "@/tools";
55
import {
@@ -52,6 +52,8 @@ class AIShellChat {
5252
readonly pendingToolCallIds: ComputedRef<string[]>;
5353
readonly askingPermission: ComputedRef<boolean>;
5454

55+
/** Force the `status` if not `null` */
56+
private forceStatus = ref<ChatStatus | null>();
5557
private chat: Chat<UIMessage>;
5658
private emitter = mitt<{
5759
finish: void;
@@ -71,7 +73,7 @@ class AIShellChat {
7173

7274
this.messages = computed(() => this.chat.messages);
7375
this.error = computed(() => this.chat.error);
74-
this.status = computed(() => this.chat.status);
76+
this.status = computed(() => this.forceStatus.value ?? this.chat.status);
7577
this.pendingToolCallIds = computed(() =>
7678
this.pendingToolCalls.map((t) => t.toolCallId),
7779
);
@@ -80,11 +82,11 @@ class AIShellChat {
8082
);
8183
}
8284

83-
async send(message: string | UIMessage['parts'], options: SendOptions) {
84-
await this.chat.sendMessage(
85-
typeof message === "string"
86-
? { text: message }
87-
: { parts: message },
85+
/** Pass `undefined` to trigger the API without sending the message */
86+
async send(message: string | undefined, options: SendOptions) {
87+
await this.chat.sendMessage(message
88+
? { text: message }
89+
: undefined,
8890
{
8991
body: {
9092
sendOptions: options,
@@ -124,7 +126,7 @@ class AIShellChat {
124126
**/
125127
rejectPermission(options?:
126128
{ toolCallId: string; }
127-
| { toolCallId: string; userEditedCode: string; sendOptions: SendOptions }
129+
| { toolCallId: string; editedQuery: string; sendOptions: SendOptions }
128130
) {
129131
if (!options) {
130132
this.pendingToolCalls.forEach((t) => {
@@ -138,16 +140,118 @@ class AIShellChat {
138140
throw new Error(`Tool call with id ${options.toolCallId} not found`);
139141
}
140142

141-
if ('userEditedCode' in options) {
143+
if ('editedQuery' in options) {
142144
const sendFollowupMessage = async () => {
143145
this.emitter.off("finish", sendFollowupMessage);
144-
this.send(
145-
[{
146-
type: "data-userEditedCode",
147-
data: { code: options.userEditedCode }
148-
}],
149-
options.sendOptions
146+
147+
const assistantMessageId = this.chat.generateId();
148+
const replacementToolCallId = this.chat.generateId();
149+
150+
this.forceStatus.value = "streaming";
151+
152+
this.chat.messages = [
153+
...this.chat.messages.slice(0, -1),
154+
{
155+
...this.chat.lastMessage!,
156+
parts: [
157+
...this.chat.lastMessage!.parts,
158+
{
159+
type: "data-userEditedToolCall",
160+
data: { replacementToolCallId },
161+
}
162+
],
163+
},
164+
{
165+
id: this.chat.generateId(),
166+
role: "user",
167+
parts: [{
168+
// We use data so it's not shown in the UI
169+
type: "data-editedQuery",
170+
data: {
171+
query: options.editedQuery,
172+
targetToolCallId: options.toolCallId,
173+
},
174+
}],
175+
},
176+
{
177+
id: assistantMessageId,
178+
role: "assistant",
179+
parts: [
180+
{ type: "step-start" },
181+
{
182+
type: "data-toolReplacement",
183+
data: { targetToolCallId: options.toolCallId },
184+
},
185+
{
186+
type: "tool-run_query",
187+
state: "input-available",
188+
toolCallId: replacementToolCallId,
189+
input: { query: options.editedQuery },
190+
},
191+
],
192+
},
193+
];
194+
195+
const runQueryOutput = await this.createRunQueryToolOutput(
196+
replacementToolCallId,
197+
options.editedQuery
150198
);
199+
200+
if (runQueryOutput.state === "output-error") {
201+
this.chat.messages = [
202+
...this.chat.messages.slice(0, -1),
203+
{
204+
id: assistantMessageId,
205+
role: "assistant",
206+
parts: [
207+
{ type: "step-start" },
208+
{
209+
type: "data-toolReplacement",
210+
data: { targetToolCallId: options.toolCallId },
211+
},
212+
{
213+
type: "tool-run_query",
214+
state: "output-error",
215+
toolCallId: replacementToolCallId,
216+
input: { query: options.editedQuery },
217+
errorText: runQueryOutput.errorText,
218+
},
219+
],
220+
},
221+
];
222+
} else {
223+
this.chat.messages = [
224+
...this.chat.messages.slice(0, -1),
225+
{
226+
id: assistantMessageId,
227+
role: "assistant",
228+
parts: [
229+
{ type: "step-start" },
230+
{
231+
type: "data-toolReplacement",
232+
data: { targetToolCallId: options.toolCallId },
233+
},
234+
{
235+
type: "tool-run_query",
236+
state: "output-available",
237+
toolCallId: replacementToolCallId,
238+
input: { query: options.editedQuery },
239+
// @ts-expect-error ts doesnt like this because the execute()
240+
// method for run_query (see tools/index.ts) is not defined.
241+
// It can be fixed:
242+
// 1. Upgrading to AI SDK v6
243+
// 2. Use the new API for user permission check
244+
// 3. define execute() method
245+
output: runQueryOutput.output,
246+
},
247+
],
248+
},
249+
];
250+
}
251+
252+
this.forceStatus.value = null;
253+
254+
this.send(undefined, options.sendOptions);
151255
};
152256
this.emitter.on("finish", sendFollowupMessage);
153257
}
@@ -264,11 +368,11 @@ class AIShellChat {
264368
modelId: sendOptions.modelId,
265369
messages: convertToModelMessages<UIMessage>(m.messages, {
266370
convertDataPart(part) {
267-
if (part.type === "data-userEditedCode") {
371+
if (part.type === "data-editedQuery") {
268372
return {
269373
type: "text",
270374
text: "Please run the following code instead:\n```\n"
271-
+ part.data.code
375+
+ part.data.query
272376
+ "\n```",
273377
};
274378
}

src/types.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,34 @@ import { UIMessage as AIUIMessage, InferUITools } from "ai";
22
import { tools } from "./tools";
33

44
export type UIDataTypes = {
5-
/** The code that the user provided. If defined, it will run the `code` and
6-
* give the result to the AI.*/
7-
userEditedCode: {
8-
code: string;
5+
/** Marks the original tool call that was edited by the user.
6+
* The UI should show this tool call as replaced/superseded. */
7+
userEditedToolCall: {
8+
/** The ID of the new tool call that replaces this one. */
9+
replacementToolCallId: string;
10+
};
11+
12+
/**
13+
* The query or code that the user provided. If defined, AI Shell should run
14+
* the `query` and give the result to the AI.
15+
*
16+
* This data is used by user messages.
17+
**/
18+
editedQuery: {
19+
query: string;
20+
/** The tool call that the user rejected. */
21+
targetToolCallId: string;
22+
};
23+
24+
/**
25+
* If defined, AI Shell should replace the UI of `targetToolCallId` with
26+
* this tool or message.
27+
*
28+
* This data is used by assistant messages.
29+
**/
30+
toolReplacement: {
31+
/** The ID of the original tool call that this replaces. */
32+
targetToolCallId: string;
933
};
1034
}
1135

0 commit comments

Comments
 (0)