From 7c95868fc61f0757dbfda48db33acea1e6acf522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihai=20-=20Alexandru=20Chindri=C8=99?= Date: Sat, 28 Feb 2026 00:56:18 +0200 Subject: [PATCH 1/8] feat(app): collapse large pasted input blocks --- packages/app/src/components/prompt-input.tsx | 37 ++++++++++++++++--- .../components/prompt-input/attachments.ts | 10 ++++- .../src/components/prompt-input/history.ts | 5 +++ packages/app/src/context/prompt.tsx | 10 ++++- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 3ba3763b8c5..a3c021c67b0 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -13,6 +13,7 @@ import { ImageAttachmentPart, AgentPart, FileAttachmentPart, + PastedPart, } from "@/context/prompt" import { useLayout } from "@/context/layout" import { useSDK } from "@/context/sdk" @@ -600,12 +601,16 @@ export const PromptInput: Component = (props) => { onSelect: handleSlashSelect, }) - const createPill = (part: FileAttachmentPart | AgentPart) => { + const createPill = (part: FileAttachmentPart | AgentPart | PastedPart) => { const pill = document.createElement("span") - pill.textContent = part.content + pill.textContent = part.type === "paste" ? part.summary : part.content pill.setAttribute("data-type", part.type) if (part.type === "file") pill.setAttribute("data-path", part.path) if (part.type === "agent") pill.setAttribute("data-name", part.name) + if (part.type === "paste") { + pill.setAttribute("data-content", part.content) + pill.setAttribute("data-summary", part.summary) + } pill.setAttribute("contenteditable", "false") pill.style.userSelect = "text" pill.style.cursor = "default" @@ -628,6 +633,7 @@ export const PromptInput: Component = (props) => { const el = node as HTMLElement if (el.dataset.type === "file") return true if (el.dataset.type === "agent") return true + if (el.dataset.type === "paste") return true return el.tagName === "BR" }) @@ -638,7 +644,7 @@ export const PromptInput: Component = (props) => { editorRef.appendChild(createTextFragment(part.content)) continue } - if (part.type === "file" || part.type === "agent") { + if (part.type === "file" || part.type === "agent" || part.type === "paste") { editorRef.appendChild(createPill(part)) } } @@ -748,6 +754,19 @@ export const PromptInput: Component = (props) => { position += content.length } + const pushPaste = (paste: HTMLElement) => { + const content = paste.dataset.content ?? "" + const summary = paste.dataset.summary ?? paste.textContent ?? "[Pasted text]" + parts.push({ + type: "paste", + content, + summary, + start: position, + end: position + content.length, + }) + position += content.length + } + const visit = (node: Node) => { if (node.nodeType === Node.TEXT_NODE) { buffer += node.textContent ?? "" @@ -766,6 +785,11 @@ export const PromptInput: Component = (props) => { pushAgent(el) return } + if (el.dataset.type === "paste") { + flushText() + pushPaste(el) + return + } if (el.tagName === "BR") { buffer += "\n" return @@ -798,7 +822,7 @@ export const PromptInput: Component = (props) => { const rawText = rawParts.length === 1 && rawParts[0]?.type === "text" ? rawParts[0].content - : rawParts.map((p) => ("content" in p ? p.content : "")).join("") + : rawParts.map((p) => (p.type === "paste" ? p.summary : "content" in p ? p.content : "")).join("") const hasNonText = rawParts.some((part) => part.type !== "text") const shouldReset = !NON_EMPTY_TEXT.test(rawText) && !hasNonText && images.length === 0 @@ -855,11 +879,11 @@ export const PromptInput: Component = (props) => { const range = selection.getRangeAt(0) if (!editorRef.contains(range.startContainer)) return false - if (part.type === "file" || part.type === "agent") { + if (part.type === "file" || part.type === "agent" || part.type === "paste") { const cursorPosition = getCursorPosition(editorRef) const rawText = prompt .current() - .map((p) => ("content" in p ? p.content : "")) + .map((p) => (p.type === "paste" ? p.summary : "content" in p ? p.content : "")) .join("") const textBeforeCursor = rawText.substring(0, cursorPosition) const atMatch = textBeforeCursor.match(/@(\S*)$/) @@ -1223,6 +1247,7 @@ export const PromptInput: Component = (props) => { "w-full pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, "[&_[data-type=file]]:text-syntax-property": true, "[&_[data-type=agent]]:text-syntax-type": true, + "[&_[data-type=paste]]:text-text-weak": true, "font-mono!": store.mode === "shell", }} /> diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index a9e4e496512..edb183a963c 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -21,6 +21,11 @@ function largePaste(text: string) { return false } +function pasteSummary(text: string) { + const lines = text.split("\n").length + return `[Pasted ~${lines} lines]` +} + type PromptAttachmentsInput = { editor: () => HTMLDivElement | undefined isFocused: () => boolean @@ -104,9 +109,10 @@ export function createPromptAttachments(input: PromptAttachmentsInput) { if (!plainText) return if (largePaste(plainText)) { - if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return + const summary = pasteSummary(plainText) + if (input.addPart({ type: "paste", content: plainText, summary, start: 0, end: 0 })) return input.focusEditor() - if (input.addPart({ type: "text", content: plainText, start: 0, end: 0 })) return + if (input.addPart({ type: "paste", content: plainText, summary, start: 0, end: 0 })) return } const inserted = typeof document.execCommand === "function" && document.execCommand("insertText", false, plainText) diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts index de62653211d..b369662dbfb 100644 --- a/packages/app/src/components/prompt-input/history.ts +++ b/packages/app/src/components/prompt-input/history.ts @@ -36,6 +36,7 @@ export function clonePromptParts(prompt: Prompt): Prompt { if (part.type === "text") return { ...part } if (part.type === "image") return { ...part } if (part.type === "agent") return { ...part } + if (part.type === "paste") return { ...part } return { ...part, selection: part.selection ? { ...part.selection } : undefined, @@ -121,6 +122,10 @@ function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistory const partB = entryB.prompt[i] if (partA.type !== partB.type) return false if (partA.type === "text" && partA.content !== (partB.type === "text" ? partB.content : "")) return false + if (partA.type === "paste") { + if (partB.type !== "paste") return false + if (partA.content !== partB.content || partA.summary !== partB.summary) return false + } if (partA.type === "file") { if (partA.path !== (partB.type === "file" ? partB.path : "")) return false const a = partA.selection diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx index fb822655911..2654d792606 100644 --- a/packages/app/src/context/prompt.tsx +++ b/packages/app/src/context/prompt.tsx @@ -27,6 +27,11 @@ export interface AgentPart extends PartBase { name: string } +export interface PastedPart extends PartBase { + type: "paste" + summary: string +} + export interface ImageAttachmentPart { type: "image" id: string @@ -35,7 +40,7 @@ export interface ImageAttachmentPart { dataUrl: string } -export type ContentPart = TextPart | FileAttachmentPart | AgentPart | ImageAttachmentPart +export type ContentPart = TextPart | FileAttachmentPart | AgentPart | PastedPart | ImageAttachmentPart export type Prompt = ContentPart[] export type FileContextItem = { @@ -68,6 +73,8 @@ function isPartEqual(partA: ContentPart, partB: ContentPart) { return partB.type === "file" && partA.path === partB.path && isSelectionEqual(partA.selection, partB.selection) case "agent": return partB.type === "agent" && partA.name === partB.name + case "paste": + return partB.type === "paste" && partA.content === partB.content && partA.summary === partB.summary case "image": return partB.type === "image" && partA.id === partB.id } @@ -90,6 +97,7 @@ function clonePart(part: ContentPart): ContentPart { if (part.type === "text") return { ...part } if (part.type === "image") return { ...part } if (part.type === "agent") return { ...part } + if (part.type === "paste") return { ...part } return { ...part, selection: cloneSelection(part.selection), From 93547d33b6ef956fee9b39668c2e7adfe78e877a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihai=20-=20Alexandru=20Chindri=C8=99?= Date: Sat, 28 Feb 2026 01:20:17 +0200 Subject: [PATCH 2/8] test(e2e): make session seed waits more resilient --- packages/app/e2e/actions.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/app/e2e/actions.ts b/packages/app/e2e/actions.ts index a7ccba61752..55693f898ff 100644 --- a/packages/app/e2e/actions.ts +++ b/packages/app/e2e/actions.ts @@ -339,8 +339,11 @@ const seedSystem = [ "Do not call any extra tools.", ].join(" ") +const SEED_TIMEOUT = 60_000 +const SEED_ATTEMPTS = 3 + const wait = async (input: { probe: () => Promise; timeout?: number }) => { - const timeout = input.timeout ?? 30_000 + const timeout = input.timeout ?? SEED_TIMEOUT const end = Date.now() + timeout while (Date.now() < end) { const value = await input.probe() @@ -357,7 +360,7 @@ const seed = async (input: { timeout?: number attempts?: number }) => { - for (let i = 0; i < (input.attempts ?? 2); i++) { + for (let i = 0; i < (input.attempts ?? SEED_ATTEMPTS); i++) { await input.sdk.session.promptAsync({ sessionID: input.sessionID, agent: "build", @@ -396,7 +399,7 @@ export async function seedSessionQuestion( sdk, sessionID: input.sessionID, prompt: text, - timeout: 30_000, + timeout: SEED_TIMEOUT, probe: async () => { const list = await sdk.question.list().then((x) => x.data ?? []) return list.find((item) => item.sessionID === input.sessionID && item.questions[0]?.header === first.header) @@ -430,7 +433,7 @@ export async function seedSessionPermission( sdk, sessionID: input.sessionID, prompt: text, - timeout: 30_000, + timeout: SEED_TIMEOUT, probe: async () => { const list = await sdk.permission.list().then((x) => x.data ?? []) return list.find((item) => item.sessionID === input.sessionID) @@ -459,7 +462,7 @@ export async function seedSessionTodos( sdk, sessionID: input.sessionID, prompt: text, - timeout: 30_000, + timeout: SEED_TIMEOUT, probe: async () => { const todos = await sdk.session.todo({ sessionID: input.sessionID }).then((x) => x.data ?? []) if (JSON.stringify(todos) !== target) return From 758496c26a73037cca33d1349dfd1ce2284aa4e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihai=20-=20Alexandru=20Chindri=C8=99?= Date: Sat, 28 Feb 2026 02:04:17 +0200 Subject: [PATCH 3/8] feat(app): lower paste summary threshold for desktop UX --- packages/app/src/components/prompt-input/attachments.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index edb183a963c..15af9696c7c 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -7,8 +7,8 @@ import { getCursorPosition } from "./editor-dom" export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"] -const LARGE_PASTE_CHARS = 8000 -const LARGE_PASTE_BREAKS = 120 +const LARGE_PASTE_CHARS = 200 +const LARGE_PASTE_BREAKS = 5 function largePaste(text: string) { if (text.length >= LARGE_PASTE_CHARS) return true From 5f549b538570bde18df24b9c1fb33409df5219cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihai=20-=20Alexandru=20Chindri=C8=99?= Date: Sat, 28 Feb 2026 02:06:26 +0200 Subject: [PATCH 4/8] chore(app): restore upstream paste summary thresholds --- packages/app/src/components/prompt-input/attachments.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/prompt-input/attachments.ts b/packages/app/src/components/prompt-input/attachments.ts index 15af9696c7c..edb183a963c 100644 --- a/packages/app/src/components/prompt-input/attachments.ts +++ b/packages/app/src/components/prompt-input/attachments.ts @@ -7,8 +7,8 @@ import { getCursorPosition } from "./editor-dom" export const ACCEPTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"] export const ACCEPTED_FILE_TYPES = [...ACCEPTED_IMAGE_TYPES, "application/pdf"] -const LARGE_PASTE_CHARS = 200 -const LARGE_PASTE_BREAKS = 5 +const LARGE_PASTE_CHARS = 8000 +const LARGE_PASTE_BREAKS = 120 function largePaste(text: string) { if (text.length >= LARGE_PASTE_CHARS) return true From fb1b80162f23ccf9f8f40d2f2ce6144cb0d276b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihai=20-=20Alexandru=20Chindri=C8=99?= Date: Sat, 28 Feb 2026 02:34:16 +0200 Subject: [PATCH 5/8] style(app): match TUI yellow highlight for paste summary pills --- packages/app/src/components/prompt-input.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index a3c021c67b0..512c0d0139b 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -610,6 +610,8 @@ export const PromptInput: Component = (props) => { if (part.type === "paste") { pill.setAttribute("data-content", part.content) pill.setAttribute("data-summary", part.summary) + pill.style.padding = "1px 6px" + pill.style.borderRadius = "4px" } pill.setAttribute("contenteditable", "false") pill.style.userSelect = "text" @@ -1247,7 +1249,9 @@ export const PromptInput: Component = (props) => { "w-full pl-3 pr-2 pt-2 pb-11 text-14-regular text-text-strong focus:outline-none whitespace-pre-wrap": true, "[&_[data-type=file]]:text-syntax-property": true, "[&_[data-type=agent]]:text-syntax-type": true, - "[&_[data-type=paste]]:text-text-weak": true, + "[&_[data-type=paste]]:bg-surface-warning-strong": true, + "[&_[data-type=paste]]:text-text-on-warning-base": true, + "[&_[data-type=paste]]:font-bold": true, "font-mono!": store.mode === "shell", }} /> From 58ca2090929eef1ca4b018dda5a89e760b83b083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihai=20-=20Alexandru=20Chindri=C8=99?= Date: Sat, 28 Feb 2026 04:24:34 +0200 Subject: [PATCH 6/8] fix(app): use dark text for paste summary pills --- packages/app/src/components/prompt-input.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 512c0d0139b..ad3b289736e 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -612,6 +612,7 @@ export const PromptInput: Component = (props) => { pill.setAttribute("data-summary", part.summary) pill.style.padding = "1px 6px" pill.style.borderRadius = "4px" + pill.style.color = "var(--text-on-warning-strong)" } pill.setAttribute("contenteditable", "false") pill.style.userSelect = "text" From b14279672d0895143519c60585338c6b84f6ffb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihai=20-=20Alexandru=20Chindri=C8=99?= Date: Sat, 28 Feb 2026 04:33:15 +0200 Subject: [PATCH 7/8] fix(app): enforce dark text for paste summary pills --- packages/app/src/components/prompt-input.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index ad3b289736e..f8a562b03e2 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -612,7 +612,8 @@ export const PromptInput: Component = (props) => { pill.setAttribute("data-summary", part.summary) pill.style.padding = "1px 6px" pill.style.borderRadius = "4px" - pill.style.color = "var(--text-on-warning-strong)" + pill.style.setProperty("color", "var(--text-on-warning-strong)", "important") + pill.style.setProperty("-webkit-text-fill-color", "var(--text-on-warning-strong)", "important") } pill.setAttribute("contenteditable", "false") pill.style.userSelect = "text" From fab167d8b210ba9b89023a5fbf46f5a771036c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mihai=20-=20Alexandru=20Chindri=C8=99?= Date: Sat, 28 Feb 2026 04:35:37 +0200 Subject: [PATCH 8/8] fix(app): use dark primitive color for paste pill text --- packages/app/src/components/prompt-input.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index f8a562b03e2..b01fcaf3a4f 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -612,8 +612,8 @@ export const PromptInput: Component = (props) => { pill.setAttribute("data-summary", part.summary) pill.style.padding = "1px 6px" pill.style.borderRadius = "4px" - pill.style.setProperty("color", "var(--text-on-warning-strong)", "important") - pill.style.setProperty("-webkit-text-fill-color", "var(--text-on-warning-strong)", "important") + pill.style.setProperty("color", "var(--smoke-light-12)", "important") + pill.style.setProperty("-webkit-text-fill-color", "var(--smoke-light-12)", "important") } pill.setAttribute("contenteditable", "false") pill.style.userSelect = "text"