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
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const ChatInput = observer(({
return () => window.removeEventListener('keydown', handleGlobalKeyDown, true);
}, []);

const disabled = isStreaming
const disabled = false; // Allow input while streaming
const inputEmpty = !inputValue || inputValue.trim().length === 0;

function handleInput(e: React.ChangeEvent<HTMLTextAreaElement>) {
Expand Down Expand Up @@ -136,22 +136,33 @@ export const ChatInput = observer(({
console.warn('Empty message');
return;
}
if (isStreaming) {
console.warn('Already waiting for response');
return;
}

const savedInput = inputValue.trim();

try {
await onSendMessage(savedInput, chatMode);
if (isStreaming) {
// Queue the message if streaming
const queuedMessage = editorEngine.chat.queue.enqueue(savedInput, chatMode);
console.log('Message queued:', queuedMessage);
} else {
// Send immediately if not streaming
await onSendMessage(savedInput, chatMode);
}
// Clear input
setInputValue('');
} catch (error) {
console.error('Error sending message', error);
toast.error('Failed to send message. Please try again.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handler no longer restores the input value on send failure. Consider whether you want to recover the user's input to prevent accidental text loss.

setInputValue(savedInput);
}
}

const getPlaceholderText = () => {
if (isStreaming && editorEngine.chat.queue.length > 0) {
return `${editorEngine.chat.queue.length} message${editorEngine.chat.queue.length > 1 ? 's' : ''} queued - type to add more...`;
}
if (isStreaming) {
return 'Type to queue message while AI responds...';
}
Comment on lines 157 to +165
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Localize new placeholders (next-intl).

Avoid hardcoded user-facing strings; add keys with pluralization.

-        if (isStreaming && editorEngine.chat.queue.length > 0) {
-            return `${editorEngine.chat.queue.length} message${editorEngine.chat.queue.length > 1 ? 's' : ''} queued - type to add more...`;
-        }
-        if (isStreaming) {
-            return 'Type to queue message while AI responds...';
-        }
+        if (isStreaming && editorEngine.chat.queue.length > 0) {
+            return t(transKeys.editor.chat.input.placeholder.streamingQueued, {
+                count: editorEngine.chat.queue.length,
+            });
+        }
+        if (isStreaming) {
+            return t(transKeys.editor.chat.input.placeholder.streaming);
+        }

Please add the corresponding keys to your messages file.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getPlaceholderText = () => {
if (isStreaming && editorEngine.chat.queue.length > 0) {
return `${editorEngine.chat.queue.length} message${editorEngine.chat.queue.length > 1 ? 's' : ''} queued - type to add more...`;
}
if (isStreaming) {
return 'Type to queue message while AI responds...';
}
const getPlaceholderText = () => {
if (isStreaming && editorEngine.chat.queue.length > 0) {
return t(transKeys.editor.chat.input.placeholder.streamingQueued, {
count: editorEngine.chat.queue.length,
});
}
if (isStreaming) {
return t(transKeys.editor.chat.input.placeholder.streaming);
}
🤖 Prompt for AI Agents
In
apps/web/client/src/app/project/[id]/_components/right-panel/chat-tab/chat-input/index.tsx
around lines 159 to 165, the placeholder strings are hardcoded; replace them
with next-intl message lookups (use intl.formatMessage or <FormattedMessage>)
and add pluralization for the queued count (e.g., a plural key like
"chat.input.queued" with forms for 0/one/other). Add corresponding keys to the
project messages file(s) (including plural variants and the "typing while
responding" key) and update the component to use those keys instead of literal
text so translations can be provided.

if (chatMode === ChatType.ASK) {
return 'Ask a question about your project...';
}
Expand Down Expand Up @@ -422,10 +433,11 @@ export const ChatInput = observer(({
size={'icon'}
variant={'secondary'}
className="text-smallPlus w-fit h-full py-0.5 px-2.5 text-primary"
disabled={inputEmpty || disabled}
disabled={inputEmpty}
onClick={() => void sendMessage()}
title={isStreaming ? 'Queue message' : 'Send message'}
>
<Icons.ArrowRight />
{isStreaming ? <Icons.CounterClockwiseClock /> : <Icons.ArrowRight />}
</Button>
)}
</div>
Expand Down
11 changes: 8 additions & 3 deletions apps/web/client/src/app/project/[id]/_hooks/use-chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,15 +208,20 @@ export function useChat({ conversationId, projectId, initialMessages }: UseChatP
);
};

const cleanupContext = async () => {
await editorEngine.chat.context.clearImagesFromContext();
const cleanupContext = () => {
editorEngine.chat.context.clearImagesFromContext();
};

void cleanupContext();
void fetchSuggestions();
void applyCommit();

// Process any queued messages
setTimeout(() => {
void editorEngine.chat.queue.processNext();
}, 500);
}
}, [finishReason, conversationId]);
}, [finishReason, conversationId, editorEngine.chat.queue, editorEngine.chat.context, editorEngine.versions, setMessages]);

useEffect(() => {
editorEngine.chat.conversation.setConversationLength(messages.length);
Expand Down
4 changes: 4 additions & 0 deletions apps/web/client/src/components/store/editor/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { makeAutoObservable } from 'mobx';
import type { EditorEngine } from '../engine';
import { ChatContext } from './context';
import { ConversationManager } from './conversation';
import { MessageQueue } from './queue';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use path alias for internal imports.

Align with configured @/* aliases.

-import { MessageQueue } from './queue';
+import { MessageQueue } from '@/components/store/editor/chat/queue';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { MessageQueue } from './queue';
import { MessageQueue } from '@/components/store/editor/chat/queue';
🤖 Prompt for AI Agents
In apps/web/client/src/components/store/editor/chat/index.ts around line 7, the
import uses a relative path "import { MessageQueue } from './queue';" but
project prefers the configured @/* path aliases; update the import to use the
alias (e.g. import { MessageQueue } from '@/components/store/editor/chat/queue')
matching your tsconfig/webpack alias and adjust the path to the correct aliased
location so linting/building uses the internal @/ alias.


export const FOCUS_CHAT_INPUT_EVENT = 'focus-chat-input';
export class ChatManager {
conversation: ConversationManager;
context: ChatContext;
queue: MessageQueue;

// Content sent from useChat hook
_sendMessageAction: SendMessage | null = null;
Expand All @@ -17,6 +19,7 @@ export class ChatManager {
constructor(private editorEngine: EditorEngine) {
this.context = new ChatContext(this.editorEngine);
this.conversation = new ConversationManager(this.editorEngine);
this.queue = new MessageQueue(this.editorEngine);
makeAutoObservable(this);
}

Expand Down Expand Up @@ -51,5 +54,6 @@ export class ChatManager {
clear() {
this.context.clear();
this.conversation.clear();
this.queue.clear();
}
}
93 changes: 93 additions & 0 deletions apps/web/client/src/components/store/editor/chat/queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { type ChatType } from '@onlook/models';
import { makeAutoObservable } from 'mobx';
import type { EditorEngine } from '../engine';
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use path alias for internal imports.

Replace relative import with the configured alias per guidelines.

-import type { EditorEngine } from '../engine';
+import type { EditorEngine } from '@/components/store/editor/engine';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import type { EditorEngine } from '../engine';
import type { EditorEngine } from '@/components/store/editor/engine';
🤖 Prompt for AI Agents
In apps/web/client/src/components/store/editor/chat/queue.ts around line 3,
replace the relative import "import type { EditorEngine } from '../engine';"
with the project's configured path alias (e.g. use the alias configured in
tsconfig like "@/components/store/editor/engine" or the appropriate alias for
this repo) so internal imports follow the guidelines; update the import path to
the aliased module and ensure TypeScript resolves the alias by using the exact
alias name used in the repo.


export interface QueuedMessage {
content: string;
type: ChatType;
id: string;
timestamp: number;
}

export class MessageQueue {
private _queue: QueuedMessage[] = [];
private _isProcessing = false;

constructor(private editorEngine: EditorEngine) {
makeAutoObservable(this);
}
Comment on lines +16 to +18
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Don’t make EditorEngine observable.

Avoid deep observability/memory churn by excluding heavy refs from MobX.

-    constructor(private editorEngine: EditorEngine) {
-        makeAutoObservable(this);
-    }
+    constructor(private editorEngine: EditorEngine) {
+        makeAutoObservable(this, { editorEngine: false });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(private editorEngine: EditorEngine) {
makeAutoObservable(this);
}
constructor(private editorEngine: EditorEngine) {
makeAutoObservable(this, { editorEngine: false });
}
🤖 Prompt for AI Agents
In apps/web/client/src/components/store/editor/chat/queue.ts around lines 16 to
18, the constructor currently calls makeAutoObservable(this) which makes the
heavy EditorEngine reference fully observable; change the call to exclude that
property from observability by calling makeAutoObservable(this, { editorEngine:
false }) (or the equivalent makeObservable wiring) so EditorEngine remains a
plain non-observable field and avoid deep observability/memory churn.


get queue(): QueuedMessage[] {
return [...this._queue];
}

get length(): number {
return this._queue.length;
}

get isProcessing(): boolean {
return this._isProcessing;
}

get isEmpty(): boolean {
return this._queue.length === 0;
}

enqueue(content: string, type: ChatType): QueuedMessage {
const message: QueuedMessage = {
content: content.trim(),
type,
id: `queued_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
Comment on lines +40 to +41
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace deprecated substr().

Use slice() to avoid deprecation warnings.

-            id: `queued_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+            id: `queued_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id: `queued_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
id: `queued_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,
timestamp: Date.now(),
🤖 Prompt for AI Agents
In apps/web/client/src/components/store/editor/chat/queue.ts around lines 40-41,
the id generation uses the deprecated Math.random().toString(36).substr(2, 9);
replace substr with slice to avoid deprecation warnings and preserve the same
9-character output by using Math.random().toString(36).slice(2, 11). Update the
id line accordingly so it still concatenates Date.now() with a 9-char random
string but uses slice instead of substr.

};

this._queue.push(message);
return message;
}

dequeue(): QueuedMessage | null {
return this._queue.shift() || null;
}

removeMessage(id: string): boolean {
const index = this._queue.findIndex(msg => msg.id === id);
if (index !== -1) {
this._queue.splice(index, 1);
return true;
}
return false;
}

clear(): void {
this._queue = [];
this._isProcessing = false;
}

async processNext(): Promise<void> {
if (this._isProcessing || this.isEmpty || this.editorEngine.chat.isStreaming) {
return;
}

this._isProcessing = true;

const nextMessage = this.dequeue();
if (!nextMessage) {
this._isProcessing = false;
return;
}

try {
await this.editorEngine.chat.sendMessage(nextMessage.content, nextMessage.type);
} catch (error) {
console.error('Error processing queued message:', error);
} finally {
this._isProcessing = false;
}
}

async processAll(): Promise<void> {
while (!this.isEmpty && !this.editorEngine.chat.isStreaming) {
await this.processNext();
}
}
}