Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/slow-lights-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"kilo-code": minor
---

Message queuing and interjections

Now, if the agent is busy when you hit enter, the message will become "queued" and will be sent the next time the agent is idle.
Also, hitting alt + enter will now cancel the existing task and send the queued message (interjection)
94 changes: 94 additions & 0 deletions apps/storybook/src/components/chat/QueuedMessageListExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// kilocode_change - moved from webview-ui for Storybook-only usage
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { QueuedMessageList } from "@/components/chat/QueuedMessageList"
import { createSampleMessage, QueuedMessage } from "@/components/chat/hooks/useQueuedMessages"

interface QueuedMessageListExampleProps {
initialMessages?: QueuedMessage[]
className?: string
}

export function QueuedMessageListExample({ initialMessages = [], className }: QueuedMessageListExampleProps) {
const [messages, setMessages] = useState<QueuedMessage[]>(initialMessages)
const [isQueuePaused, setIsQueuePaused] = useState(false)

const handleRemoveMessage = (messageId: string) => {
setMessages((prev) => prev.filter((msg) => msg.id !== messageId))
}

const handlePauseQueue = () => {
setIsQueuePaused(true)
}

const handleResumeQueue = () => {
setIsQueuePaused(false)
}

const sampleTexts = [
"Write a function to calculate fibonacci numbers",
"Add error handling to the previous code",
"Refactor this component to use TypeScript",
"Create unit tests for the authentication service",
"Optimize the database query performance",
"Add responsive design to the dashboard",
]

const sampleImageMessages = [
{ text: "Fix the layout issue shown in this screenshot", images: ["screenshot.png"] },
{ text: "Implement the design from these mockups", images: ["design1.png", "design2.png"] },
{ text: "", images: ["error-log.png"] },
{
text: "Update the UI based on these wireframes",
images: ["wireframe1.jpg", "wireframe2.jpg", "wireframe3.jpg"],
},
]

const addSampleMessage = (messageData: { text: string; images?: string[] }) => {
const newMessage = createSampleMessage(messageData.text, messageData.images)
setMessages((prev) => [...prev, newMessage])
}

const addSampleTextMessage = () => {
const randomText = sampleTexts[Math.floor(Math.random() * sampleTexts.length)]
addSampleMessage({ text: randomText })
}

const addSampleImageMessage = () => {
const randomMessage = sampleImageMessages[Math.floor(Math.random() * sampleImageMessages.length)]
addSampleMessage({ text: randomMessage.text, images: randomMessage.images })
}

const addLongMessage = () => {
addSampleMessage({
text: "This is a very long message that demonstrates how the component handles text truncation and maintains proper layout even with extensive content that would normally overflow the container and cause layout issues in the user interface",
})
}

const clearAllMessages = () => {
setMessages([])
}

return (
<div className={`space-y-4 ${className || ""}`}>
<div className="flex flex-wrap gap-2">
<Button onClick={addSampleTextMessage}>Add Text Message</Button>
<Button onClick={addSampleImageMessage}>Add Image Message</Button>
<Button onClick={addLongMessage}>Add Long Message</Button>
<Button variant="secondary" onClick={clearAllMessages} disabled={messages.length === 0}>
Clear All
</Button>
</div>

<div className="text-sm text-vscode-descriptionForeground">Messages: {messages.length}</div>

<QueuedMessageList
messages={messages}
onRemoveMessage={handleRemoveMessage}
isQueuePaused={isQueuePaused}
onPauseQueue={handlePauseQueue}
onResumeQueue={handleResumeQueue}
/>
</div>
)
}
2 changes: 1 addition & 1 deletion apps/storybook/src/decorators/withExtensionState.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Decorator } from "@storybook/react"
import type { Decorator } from "@storybook/react-vite"
import { ExtensionStateContext } from "../../../../webview-ui/src/context/ExtensionStateContext"
import { createExtensionStateMock } from "../utils/createExtensionStateMock"

Expand Down
2 changes: 1 addition & 1 deletion apps/storybook/src/decorators/withFixedContainment.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Decorator } from "@storybook/react"
import type { Decorator } from "@storybook/react-vite"

// Wraps stories with an element that will "contain" any `position: fixed` elements
// the `translate-0` is a noop, but causes any children to be contained in this element.
Expand Down
2 changes: 1 addition & 1 deletion apps/storybook/src/decorators/withI18n.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect } from "react"
import type { Decorator } from "@storybook/react"
import type { Decorator } from "@storybook/react-vite"
import i18n from "../../../../webview-ui/src/i18n/setup"
import { loadTranslations } from "../../../../webview-ui/src/i18n/setup"
import TranslationProvider from "@/i18n/TranslationContext"
Expand Down
2 changes: 1 addition & 1 deletion apps/storybook/src/decorators/withQueryClient.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Decorator } from "@storybook/react"
import type { Decorator } from "@storybook/react-vite"
import React from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"

Expand Down
2 changes: 1 addition & 1 deletion apps/storybook/src/decorators/withTheme.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Decorator } from "@storybook/react"
import type { Decorator } from "@storybook/react-vite"
import { useEffect } from "react"

// Decorator to handle theme switching by applying the data-theme attribute to the body
Expand Down
2 changes: 1 addition & 1 deletion apps/storybook/src/decorators/withTooltipProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Decorator } from "@storybook/react"
import type { Decorator } from "@storybook/react-vite"
import { TooltipProvider } from "@/components/ui/tooltip"

export const withTooltipProvider: Decorator = (Story) => {
Expand Down
62 changes: 62 additions & 0 deletions apps/storybook/stories/QueuedMessageList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// kilocode_change - new file
import type { Meta, StoryObj } from "@storybook/react-vite"
import { createSampleMessage } from "@/components/chat/hooks/useQueuedMessages"
import { QueuedMessageListExample } from "../src/components/chat/QueuedMessageListExample"

const sampleMessages = [
createSampleMessage("Write a function to calculate fibonacci numbers"),
createSampleMessage("Add error handling to the previous code", ["screenshot.png"]),
createSampleMessage("", ["image1.jpg", "image2.png"]), // Image-only message
createSampleMessage(
"This is a very long message that should be truncated when displayed in the list to prevent layout issues and maintain readability",
),
]

const meta = {
title: "Component/QueuedMessageList",
component: QueuedMessageListExample,
tags: ["autodocs"],
args: {
initialMessages: [],
},
} satisfies Meta<typeof QueuedMessageListExample>

export default meta
type Story = StoryObj<typeof meta>

export const Default: Story = {
args: {
initialMessages: sampleMessages,
},
}

export const WithLongMessages: Story = {
args: {
initialMessages: [
createSampleMessage(
"This is a very long message that demonstrates how the component handles text truncation and maintains proper layout even with extensive content that would normally overflow the container",
),
createSampleMessage("Another long message with images", [
"very-long-filename-that-might-cause-issues.png",
"another-image.jpg",
]),
],
},
}

export const ManyMessages: Story = {
args: {
initialMessages: Array.from({ length: 10 }, (_, i) =>
createSampleMessage(`Queued message ${i + 1}`, i % 3 === 0 ? [`image${i}.png`] : []),
),
},
}

export const InteractivePlayground: Story = {
args: {
initialMessages: [
createSampleMessage("Welcome! Try the buttons above to add messages"),
createSampleMessage("You can also remove messages by clicking the trash icon", ["example.png"]),
],
},
}
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion webview-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"debounce": "^2.1.1",
"dompurify": "^3.2.6",
"fast-deep-equal": "^3.1.3",
"framer-motion": "^12.15.0",
"fzf": "^0.5.2",
"hast-util-to-jsx-runtime": "^2.3.6",
"i18next": "^25.0.0",
Expand Down Expand Up @@ -88,7 +89,6 @@
"@roo-code/config-typescript": "workspace:^",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/react": "^8.4.7",
"@storybook/react-vite": "^8.4.7",
"@storybook/testing-library": "^0.2.2",
"@testing-library/jest-dom": "^6.6.3",
Expand Down
56 changes: 50 additions & 6 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import {
insertSlashCommand,
validateSlashCommand,
} from "@/utils/slash-commands"
import { QueuedMessageList } from "./QueuedMessageList"
import { QueuedMessage } from "./hooks/useQueuedMessages"
// kilocode_change end

interface ChatTextAreaProps {
Expand All @@ -72,6 +74,15 @@ interface ChatTextAreaProps {
// Edit mode props
isEditMode?: boolean
onCancel?: () => void
// kilocode_change - start message queuing props
isQueuePaused?: boolean // kilocode_change
queuedMessages?: QueuedMessage[] // kilocode_change
onInterjection?: () => void // kilocode_change
onRemoveQueuedMessage?: (messageId: string) => void // kilocode_change
onEditQueuedMessage?: (messageId: string) => void // kilocode_change
onResumeQueue?: () => void // kilocode_change
onPauseQueue?: () => void // kilocode_change
// kilocode_change - end message queuing props
}

const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
Expand All @@ -93,6 +104,14 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
modeShortcutText,
isEditMode = false,
onCancel,
// kilocode_change - start message queuing props
onInterjection, // kilocode_change
queuedMessages, // kilocode_change
onRemoveQueuedMessage, // kilocode_change
isQueuePaused, // kilocode_change
onResumeQueue, // kilocode_change
onPauseQueue, // kilocode_change
// kilocode_change - end message queuing props
},
ref,
) => {
Expand Down Expand Up @@ -575,14 +594,27 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
return
}

// kilocode_change start: Handle Alt+Enter for interjection
if (event.key === "Enter" && event.altKey && !isComposing) {
event.preventDefault()
onInterjection?.()
return
}
// kilocode_change end: Handle Alt+Enter for interjection

if (event.key === "Enter" && !event.shiftKey && !isComposing) {
event.preventDefault()

if (!sendingDisabled) {
// Reset history navigation state when sending
resetHistoryNavigation()
onSend()
}
// kilocode_change start - Always call onSend
// ChatView's handleSendMessage will handle queuing when sendingDisabled
// if (!sendingDisabled) {
// // Reset history navigation state when sending
// resetHistoryNavigation()
// onSend()
// }
resetHistoryNavigation() // kilocode_change
onSend() // kilocode_change
// kilocode_change end - Always call onSend
}

if (event.key === "Backspace" && !isComposing) {
Expand Down Expand Up @@ -638,8 +670,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
handleSlashCommandsSelect,
selectedSlashCommandsIndex,
slashCommandsQuery,
onInterjection, // kilocode_change
// kilocode_change end
sendingDisabled,
// sendingDisabled,
onSend,
showContextMenu,
searchQuery,
Expand Down Expand Up @@ -1650,6 +1683,17 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
"mr-auto",
"box-border",
)}>
{/* kilocode_change start - QueuedMessageList */}
<div className="mb-[calc(var(--spacing)*-1-2px)]">
<QueuedMessageList
messages={queuedMessages || []}
onRemoveMessage={onRemoveQueuedMessage || (() => {})}
isQueuePaused={isQueuePaused || false}
onResumeQueue={onResumeQueue || (() => {})}
onPauseQueue={onPauseQueue || (() => {})}
/>
</div>
{/* kilocode_change end - QueuedMessageList */}
<div className="relative">
<div
className={cn("chat-text-area", "relative", "flex", "flex-col", "outline-none")}
Expand Down
Loading