feat: fix img pasting in new ink plus newline after tools

This commit is contained in:
Brooklyn Nicholson 2026-04-11 13:14:32 -05:00
parent b04248f4d5
commit 3fd5cf6e3c
8 changed files with 198 additions and 75 deletions

View file

@ -436,4 +436,3 @@ export const clearYogaNodeReferences = (node: DOMElement | TextNode): void => {
node.yogaNode = undefined
}

View file

@ -12,7 +12,7 @@
import { clamp } from './layout/geometry.js'
import type { Screen, StylePool } from './screen.js'
import { CellWidth, cellAt, cellAtIndex, setCellStyleId } from './screen.js'
import { cellAt, cellAtIndex, CellWidth, setCellStyleId } from './screen.js'
type Point = { col: number; row: number }

View file

@ -20,7 +20,15 @@ import { useCompletion } from './hooks/useCompletion.js'
import { useInputHistory } from './hooks/useInputHistory.js'
import { useQueue } from './hooks/useQueue.js'
import { writeOsc52Clipboard } from './lib/osc52.js'
import { buildToolTrailLine, compactPreview, fmtK, hasInterpolation, isToolTrailResultLine, pick, sameToolTrailGroup } from './lib/text.js'
import {
buildToolTrailLine,
compactPreview,
fmtK,
hasInterpolation,
isToolTrailResultLine,
pick,
sameToolTrailGroup
} from './lib/text.js'
import { DEFAULT_THEME, fromSkin, type Theme } from './theme.js'
import type {
ActiveTool,
@ -111,7 +119,9 @@ const toTranscriptMessages = (rows: unknown): Msg[] => {
let pendingTools: string[] = []
for (const row of rows) {
if (!row || typeof row !== 'object') continue
if (!row || typeof row !== 'object') {
continue
}
const role = (row as any).role
const text = (row as any).text
@ -120,18 +130,24 @@ const toTranscriptMessages = (rows: unknown): Msg[] => {
const name = (row as any).name ?? 'tool'
const ctx = (row as any).context ?? ''
pendingTools.push(buildToolTrailLine(name, ctx))
continue
}
if (typeof text !== 'string' || !text.trim()) continue
if (typeof text !== 'string' || !text.trim()) {
continue
}
if (role === 'assistant') {
const msg: Msg = { role, text }
if (pendingTools.length) {
msg.tools = pendingTools
pendingTools = []
}
result.push(msg)
continue
}
@ -2008,6 +2024,7 @@ export function App({ gw }: { gw: GatewayClient }) {
<ToolTrail
activity={busy ? activity : []}
animateCot={busy && !streaming}
padAfter={!!streaming}
t={theme}
tools={tools}
trail={turnTrail}

View file

@ -303,9 +303,12 @@ export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, pl
// ── Input handler ────────────────────────────────────────────────
useInput(
(inp, k) => {
// Paste hotkey
if ((k.ctrl || k.meta) && inp.toLowerCase() === 'v') {
(inp, k, event) => {
// Some terminals normalize Ctrl+V to "v"; others deliver raw ^V (\x16).
const ctrlPaste = k.ctrl && (inp.toLowerCase() === 'v' || event.keypress.raw === '\x16')
const metaPaste = k.meta && inp.toLowerCase() === 'v'
if (ctrlPaste || metaPaste) {
return void emitPaste({ cursor: curRef.current, hotkey: true, text: '', value: vRef.current })
}

View file

@ -48,13 +48,15 @@ export const ToolTrail = memo(function ToolTrail({
tools = [],
trail = [],
activity = [],
animateCot = false
animateCot = false,
padAfter = false
}: {
t: Theme
tools?: ActiveTool[]
trail?: string[]
activity?: ActivityItem[]
animateCot?: boolean
padAfter?: boolean
}) {
if (!trail.length && !tools.length && !activity.length) {
return null
@ -68,6 +70,7 @@ export const ToolTrail = memo(function ToolTrail({
<>
{trail.map((line, i) => {
const lastInBlock = i === rowCount - 1
const suffix = padAfter && lastInBlock ? '\n' : ''
if (isToolTrailResultLine(line)) {
return (
@ -78,6 +81,7 @@ export const ToolTrail = memo(function ToolTrail({
>
<TreeFork last={lastInBlock} />
{line}
{suffix}
</Text>
)
}
@ -87,6 +91,7 @@ export const ToolTrail = memo(function ToolTrail({
<Text color={t.color.dim} key={`c-${i}`}>
<TreeFork last={lastInBlock} />
<Spinner color={t.color.amber} variant="think" /> {line}
{suffix}
</Text>
)
}
@ -95,29 +100,34 @@ export const ToolTrail = memo(function ToolTrail({
<Text color={t.color.dim} dimColor key={`c-${i}`}>
<TreeFork last={lastInBlock} />
{line}
{suffix}
</Text>
)
})}
{tools.map((tool, j) => {
const lastInBlock = trail.length + j === rowCount - 1
const suffix = padAfter && lastInBlock ? '\n' : ''
return (
<Text color={t.color.dim} key={tool.id}>
<TreeFork last={lastInBlock} />
<Spinner color={t.color.amber} variant="tool" /> {TOOL_VERBS[tool.name] ?? tool.name}
{tool.context ? `: ${tool.context}` : ''}
{suffix}
</Text>
)
})}
{act.map((item, k) => {
const lastInBlock = trail.length + tools.length + k === rowCount - 1
const suffix = padAfter && lastInBlock ? '\n' : ''
return (
<Text color={tone(item, t)} dimColor={item.tone === 'info'} key={`a-${item.id}`}>
<TreeFork last={lastInBlock} />
{activityGlyph(item)} {item.text}
{suffix}
</Text>
)
})}

View file

@ -39,6 +39,7 @@ export const compactPreview = (s: string, max: number) => {
export const buildToolTrailLine = (name: string, context: string, error?: boolean): string => {
const label = TOOL_VERBS[name] ?? name
const mark = error ? '✗' : '✓'
return `${label}${context ? ': ' + compactPreview(context, 72) : ''} ${mark}`
}