mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 08:56:22 +02:00
Add plus button to prompt input for file and image attachments (#381)
* Add plus button to prompt input for file and image attachments Co-authored-by: Cursor <cursoragent@cursor.com> * Refactor chat message attachment handling and improve UI for attachments in chat input and sidebar * fixed review comments --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Arjun <6592213+arkml@users.noreply.github.com>
This commit is contained in:
parent
9aa3a3f82b
commit
cccb7a8a65
13 changed files with 782 additions and 114 deletions
|
|
@ -357,6 +357,12 @@ export async function loadAgent(id: string): Promise<z.infer<typeof Agent>> {
|
|||
return await repo.fetch(id);
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelMessage[] {
|
||||
const result: ModelMessage[] = [];
|
||||
for (const msg of messages) {
|
||||
|
|
@ -400,11 +406,37 @@ export function convertFromMessages(messages: z.infer<typeof Message>[]): ModelM
|
|||
});
|
||||
break;
|
||||
case "user":
|
||||
result.push({
|
||||
role: "user",
|
||||
content: msg.content,
|
||||
providerOptions,
|
||||
});
|
||||
if (typeof msg.content === 'string') {
|
||||
// Legacy string — pass through unchanged
|
||||
result.push({
|
||||
role: "user",
|
||||
content: msg.content,
|
||||
providerOptions,
|
||||
});
|
||||
} else {
|
||||
// New content parts array — collapse to text for LLM
|
||||
const textSegments: string[] = [];
|
||||
const attachmentLines: string[] = [];
|
||||
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "attachment") {
|
||||
const sizeStr = part.size ? `, ${formatBytes(part.size)}` : '';
|
||||
attachmentLines.push(`- ${part.filename} (${part.mimeType}${sizeStr}) at ${part.path}`);
|
||||
} else {
|
||||
textSegments.push(part.text);
|
||||
}
|
||||
}
|
||||
|
||||
if (attachmentLines.length > 0) {
|
||||
textSegments.unshift("User has attached the following files:", ...attachmentLines, "");
|
||||
}
|
||||
|
||||
result.push({
|
||||
role: "user",
|
||||
content: textSegments.join("\n"),
|
||||
providerOptions,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "tool":
|
||||
result.push({
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { IMonotonicallyIncreasingIdGenerator } from "./id-gen.js";
|
||||
import { UserMessageContent } from "@x/shared/dist/message.js";
|
||||
import z from "zod";
|
||||
|
||||
export type UserMessageContentType = z.infer<typeof UserMessageContent>;
|
||||
|
||||
type EnqueuedMessage = {
|
||||
messageId: string;
|
||||
message: string;
|
||||
message: UserMessageContentType;
|
||||
};
|
||||
|
||||
export interface IMessageQueue {
|
||||
enqueue(runId: string, message: string): Promise<string>;
|
||||
enqueue(runId: string, message: UserMessageContentType): Promise<string>;
|
||||
dequeue(runId: string): Promise<EnqueuedMessage | null>;
|
||||
}
|
||||
|
||||
|
|
@ -22,7 +26,7 @@ export class InMemoryMessageQueue implements IMessageQueue {
|
|||
this.idGenerator = idGenerator;
|
||||
}
|
||||
|
||||
async enqueue(runId: string, message: string): Promise<string> {
|
||||
async enqueue(runId: string, message: UserMessageContentType): Promise<string> {
|
||||
if (!this.store[runId]) {
|
||||
this.store[runId] = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,10 +46,18 @@ export class FSRunsRepo implements IRunsRepo {
|
|||
const messageEvent = event as z.infer<typeof MessageEvent>;
|
||||
if (messageEvent.message.role === 'user') {
|
||||
const content = messageEvent.message.content;
|
||||
if (typeof content === 'string' && content.trim()) {
|
||||
// Clean attached-files XML and @mentions, then truncate to 100 chars
|
||||
const cleaned = cleanContentForTitle(content);
|
||||
if (!cleaned) continue; // Skip if only attached files/mentions
|
||||
let textContent: string | undefined;
|
||||
if (typeof content === 'string') {
|
||||
textContent = content;
|
||||
} else {
|
||||
textContent = content
|
||||
.filter(p => p.type === 'text')
|
||||
.map(p => p.text)
|
||||
.join('');
|
||||
}
|
||||
if (textContent && textContent.trim()) {
|
||||
const cleaned = cleanContentForTitle(textContent);
|
||||
if (!cleaned) continue;
|
||||
return cleaned.length > 100 ? cleaned.substring(0, 100) : cleaned;
|
||||
}
|
||||
}
|
||||
|
|
@ -90,9 +98,17 @@ export class FSRunsRepo implements IRunsRepo {
|
|||
if (msg.role === 'user') {
|
||||
// Found first user message - use as title
|
||||
const content = msg.content;
|
||||
if (typeof content === 'string' && content.trim()) {
|
||||
// Clean attached-files XML and @mentions, then truncate
|
||||
const cleaned = cleanContentForTitle(content);
|
||||
let textContent: string | undefined;
|
||||
if (typeof content === 'string') {
|
||||
textContent = content;
|
||||
} else {
|
||||
textContent = content
|
||||
.filter(p => p.type === 'text')
|
||||
.map(p => p.text)
|
||||
.join('');
|
||||
}
|
||||
if (textContent && textContent.trim()) {
|
||||
const cleaned = cleanContentForTitle(textContent);
|
||||
if (cleaned) {
|
||||
title = cleaned.length > 100 ? cleaned.substring(0, 100) : cleaned;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import z from "zod";
|
||||
import container from "../di/container.js";
|
||||
import { IMessageQueue } from "../application/lib/message-queue.js";
|
||||
import { IMessageQueue, UserMessageContentType } from "../application/lib/message-queue.js";
|
||||
import { AskHumanResponseEvent, ToolPermissionRequestEvent, ToolPermissionResponseEvent, CreateRunOptions, Run, ListRunsResponse, ToolPermissionAuthorizePayload, AskHumanResponsePayload } from "@x/shared/dist/runs.js";
|
||||
import { IRunsRepo } from "./repo.js";
|
||||
import { IAgentRuntime } from "../agents/runtime.js";
|
||||
|
|
@ -19,7 +19,7 @@ export async function createRun(opts: z.infer<typeof CreateRunOptions>): Promise
|
|||
return run;
|
||||
}
|
||||
|
||||
export async function createMessage(runId: string, message: string): Promise<string> {
|
||||
export async function createMessage(runId: string, message: UserMessageContentType): Promise<string> {
|
||||
const queue = container.resolve<IMessageQueue>('messageQueue');
|
||||
const id = await queue.enqueue(runId, message);
|
||||
const runtime = container.resolve<IAgentRuntime>('agentRuntime');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue