Skip to content
Merged
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [6.7.4] - 2026-08-04

### Added

- **Paste chips for large pasted text.** Pasting 500+ characters into the
prompt input collapses the text into a removable chip above the prompt,
named after the first few words of the pasted text. On submit the full text
is merged back into the message at the exact cursor position where the paste
happened, separated by two blank lines. Backspace with an empty prompt
removes the most recent chip.

## [6.7.3] - 2026-08-04

### Changed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@matterailab/orbcode",
"version": "6.7.3",
"version": "6.7.4",
"description": "OrbCode CLI — agentic coding in your terminal, powered by Axon models by MatterAI",
"type": "module",
"bin": {
Expand Down
99 changes: 93 additions & 6 deletions src/ui/components/InputBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,45 @@ interface InputBoxProps {
const MAX_FILE_MATCHES = 8
const POPUP_PADDING_X = 2

// Pastes at least this long are collapsed into a paste chip shown above the
// prompt. The full text is merged back into the message at the recorded cursor
// position on submit, as if the text had been pasted there directly.
const PASTE_CHIP_THRESHOLD = 500

// Two newlines separate a merged chip's text from the surrounding prompt text.
const PASTE_CHIP_SEPARATOR = "\n\n"

interface PasteChip {
id: number
text: string
insertPosition: number
name: string
}

// Name shown on a paste chip: first few words of the pasted text, normalized
// and truncated so it fits on one line.
function formatPasteChipName(text: string): string {
const normalized = text.replace(/\s+/g, " ").trim()
const words = normalized.split(" ").slice(0, 4).join(" ")
return words.length > 32 ? `${words.slice(0, 29)}…` : words || "Pasted text"
}

// Merge paste chips back into the message text at their recorded cursor
// positions. Chips are inserted from the end backwards so earlier positions
// stay valid.
function mergePasteChips(text: string, chips: PasteChip[]): string {
const sorted = [...chips].sort((a, b) => b.insertPosition - a.insertPosition)
let result = text
for (const chip of sorted) {
const position = Math.max(0, Math.min(chip.insertPosition, result.length))
const content = chip.text.trim()
const prefix = position > 0 ? PASTE_CHIP_SEPARATOR : ""
const suffix = position < result.length ? PASTE_CHIP_SEPARATOR : ""
result = result.slice(0, position) + prefix + content + suffix + result.slice(position)
}
return result
}

function fitText(text: string, maxWidth: number): string {
if (text.length <= maxWidth) return text
if (maxWidth <= 1) return text.slice(0, Math.max(0, maxWidth))
Expand Down Expand Up @@ -104,6 +143,11 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
const cursorRef = useRef(0)
const [attachments, setAttachments] = useState<Attachment[]>([])
const attachmentsRef = useRef<Attachment[]>([])
// Paste chips: large text pastes shown as chips above the prompt and merged
// back into the message at their recorded cursor position on submit.
const [pasteChips, setPasteChips] = useState<PasteChip[]>([])
const pasteChipsRef = useRef<PasteChip[]>([])
const nextPasteChipIdRef = useRef(0)
const parseQueueRef = useRef<Promise<void>>(Promise.resolve())
const pendingParsesRef = useRef(0)
const composerGenerationRef = useRef(0)
Expand Down Expand Up @@ -159,7 +203,10 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
}, [value])

const submit = (text: string) => {
const trimmed = text.trim()
// Paste chips are merged back into the message at their recorded cursor
// positions first, then cleared (the text is now part of the prompt).
const merged = mergePasteChips(text, pasteChipsRef.current)
const trimmed = merged.trim()
if (trimmed === "/attach") {
setEditor("", 0)
setHistoryIndex(-1)
Expand All @@ -173,6 +220,8 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
}
setHistoryIndex(-1)
setEditor("", 0)
pasteChipsRef.current = []
setPasteChips([])
const submittedAttachments = attachmentsRef.current
composerGenerationRef.current++
attachmentsRef.current = []
Expand All @@ -186,6 +235,8 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
setEditor("", 0)
attachmentsRef.current = []
setAttachments([])
pasteChipsRef.current = []
setPasteChips([])
setAttachmentMessage(null)
setHistoryIndex(-1)
setFileIndex(0)
Expand Down Expand Up @@ -271,6 +322,31 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
setEditor(next, currentCursor + clean.length)
}

const addPasteChip = (text: string, insertPosition: number) => {
const next = [
...pasteChipsRef.current,
{ id: nextPasteChipIdRef.current++, text, insertPosition, name: formatPasteChipName(text) },
]
pasteChipsRef.current = next
setPasteChips(next)
setHistoryIndex(-1)
}

const removePasteChip = (id: number) => {
const next = pasteChipsRef.current.filter((chip) => chip.id !== id)
pasteChipsRef.current = next
setPasteChips(next)
}

// Large text pastes become a chip; smaller ones are inserted inline.
const insertPaste = (input: string) => {
if (input.length >= PASTE_CHIP_THRESHOLD) {
addPasteChip(input.replace(/\r\n?/g, "\n"), cursorRef.current)
return
}
insertPastedText(input)
}

const handlePaste = (input: string, kind?: "text" | "binary" | "unknown") => {
const droppedPaths = droppedAttachmentPaths(input, process.cwd())
if (droppedPaths.length > 0) {
Expand All @@ -287,10 +363,10 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
.then((filePaths) => {
if (generation !== composerGenerationRef.current) return
if (filePaths.length > 0) addDroppedAttachments(filePaths)
else insertPastedText(input)
else insertPaste(input)
})
.catch(() => {
if (generation === composerGenerationRef.current) insertPastedText(input)
if (generation === composerGenerationRef.current) insertPaste(input)
})
return true
}
Expand Down Expand Up @@ -394,6 +470,11 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
attachmentsRef.current = next
setAttachments(next)
setAttachmentMessage(null)
} else if (currentValue.length === 0 && pasteChipsRef.current.length > 0) {
// Backspace with an empty prompt removes the most recent paste chip.
const next = pasteChipsRef.current.slice(0, -1)
pasteChipsRef.current = next
setPasteChips(next)
}
return
}
Expand Down Expand Up @@ -427,8 +508,9 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
}
// Multi-char chunks (paste) are inserted as-is. Newlines inside
// the chunk are literal newlines, not a submit signal — press
// Enter when you're ready to send.
insertPastedText(input)
// Enter when you're ready to send. Large chunks collapse into a
// paste chip instead (see insertPaste).
insertPaste(input)
}
},
{
Expand All @@ -449,7 +531,7 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
)
const slashPopupHeight = slashMatches.length > 0 ? slashMatches.length + 3 : 0
const filePopupHeight = fileMatches.length > 0 ? fileMatches.length + 3 : 0
const attachmentRows = attachments.length + (attachmentMessage ? 1 : 0)
const attachmentRows = attachments.length + pasteChips.length + (attachmentMessage ? 1 : 0)
const renderedHeight = 2 + promptHeight + attachmentRows + slashPopupHeight + filePopupHeight

// Parent viewport calculations must use the real bottom-stack height. A
Expand Down Expand Up @@ -531,6 +613,11 @@ export function InputBox({ active, width, slashCommands, onSubmit, supportsImage
</Text>
)
})}
{pasteChips.map((chip) => (
<Text key={chip.id} color={COLORS.primary}>
{fitText(`📋 ${chip.name}`, Math.max(1, width - 4))}
</Text>
))}
{attachmentMessage && (
<Text color={attachmentMessage.isError ? COLORS.error : COLORS.dim}>
{fitText(attachmentMessage.text, Math.max(1, width - 4))}
Expand Down
Loading