mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
fix(desktop): wrap pending tool JSON input (#712)
Tested: - cd desktop && bun run test src/components/chat/CodeViewer.test.tsx - cd desktop && bun run test src/components/chat/chatBlocks.test.tsx - cd desktop && bun run lint - bun run check:desktop Confidence: high Scope-risk: narrow
This commit is contained in:
parent
b5161a4709
commit
794a063553
@ -22,4 +22,15 @@ describe('CodeViewer', () => {
|
||||
expect(container.querySelector('[data-line-number="1"]')).toBeTruthy()
|
||||
expect(container.querySelector('[data-line-number="2"]')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('can wrap long highlighted code content when requested', () => {
|
||||
const { container } = render(
|
||||
<CodeViewer code={'{"command":"cat << EOF > /tmp/index.html"}'} language="json" wrapLongLines />,
|
||||
)
|
||||
|
||||
const contentWrapper = container.querySelector('[data-code-viewer-content]') as HTMLElement | null
|
||||
expect(contentWrapper).toBeTruthy()
|
||||
expect(contentWrapper?.style.whiteSpace).toBe('pre-wrap')
|
||||
expect(contentWrapper?.style.wordBreak).toBe('break-word')
|
||||
})
|
||||
})
|
||||
|
||||
@ -7,6 +7,7 @@ type Props = {
|
||||
language?: string
|
||||
maxLines?: number
|
||||
showLineNumbers?: boolean
|
||||
wrapLongLines?: boolean
|
||||
}
|
||||
|
||||
const warmPrismTheme: PrismTheme = {
|
||||
@ -124,7 +125,17 @@ function loadShikiRuntime(): Promise<ShikiRuntime | null> {
|
||||
return shikiRuntimePromise
|
||||
}
|
||||
|
||||
function PrismCodeContent({ code, language, showLineNumbers }: { code: string; language?: string; showLineNumbers: boolean }) {
|
||||
function PrismCodeContent({
|
||||
code,
|
||||
language,
|
||||
showLineNumbers,
|
||||
wrapLongLines,
|
||||
}: {
|
||||
code: string
|
||||
language?: string
|
||||
showLineNumbers: boolean
|
||||
wrapLongLines: boolean
|
||||
}) {
|
||||
return (
|
||||
<Highlight
|
||||
theme={warmPrismTheme}
|
||||
@ -141,8 +152,8 @@ function PrismCodeContent({ code, language, showLineNumbers }: { code: string; l
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '12px',
|
||||
lineHeight: String(CODE_LINE_HEIGHT),
|
||||
whiteSpace: 'pre',
|
||||
wordBreak: 'normal',
|
||||
whiteSpace: wrapLongLines ? 'pre-wrap' : 'pre',
|
||||
wordBreak: wrapLongLines ? 'break-word' : 'normal',
|
||||
color: 'var(--color-code-fg)',
|
||||
}}
|
||||
>
|
||||
@ -168,7 +179,17 @@ function PrismCodeContent({ code, language, showLineNumbers }: { code: string; l
|
||||
)
|
||||
}
|
||||
|
||||
function CodeArea({ code, language, showLineNumbers }: { code: string; language?: string; showLineNumbers: boolean }) {
|
||||
function CodeArea({
|
||||
code,
|
||||
language,
|
||||
showLineNumbers,
|
||||
wrapLongLines,
|
||||
}: {
|
||||
code: string
|
||||
language?: string
|
||||
showLineNumbers: boolean
|
||||
wrapLongLines: boolean
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [runtime, setRuntime] = useState<ShikiRuntime | null>(null)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
@ -217,6 +238,7 @@ function CodeArea({ code, language, showLineNumbers }: { code: string; language?
|
||||
code={code}
|
||||
language={language}
|
||||
showLineNumbers={showLineNumbers}
|
||||
wrapLongLines={wrapLongLines}
|
||||
/>
|
||||
)}
|
||||
{ShikiHighlighter && (
|
||||
@ -247,7 +269,8 @@ function CodeArea({ code, language, showLineNumbers }: { code: string; language?
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: '12px',
|
||||
lineHeight: String(CODE_LINE_HEIGHT),
|
||||
whiteSpace: 'pre',
|
||||
whiteSpace: wrapLongLines ? 'pre-wrap' : 'pre',
|
||||
wordBreak: wrapLongLines ? 'break-word' : 'normal',
|
||||
}}
|
||||
>
|
||||
{code}
|
||||
@ -258,7 +281,7 @@ function CodeArea({ code, language, showLineNumbers }: { code: string; language?
|
||||
)
|
||||
}
|
||||
|
||||
export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = false }: Props) {
|
||||
export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = false, wrapLongLines = false }: Props) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
|
||||
const allLines = code.split('\n')
|
||||
@ -289,6 +312,7 @@ export function CodeViewer({ code, language, maxLines = 20, showLineNumbers = fa
|
||||
code={visibleCode}
|
||||
language={language}
|
||||
showLineNumbers={effectiveShowLineNumbers}
|
||||
wrapLongLines={wrapLongLines}
|
||||
/>
|
||||
|
||||
{/* Expand/collapse toggle */}
|
||||
|
||||
@ -577,16 +577,96 @@ function renderPartialInput(
|
||||
partialInput: string,
|
||||
t?: (key: TranslationKey, params?: Record<string, string | number>) => string,
|
||||
) {
|
||||
const formattedInput = formatPartialJsonInput(partialInput)
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)]">
|
||||
<div className="border-b border-[var(--color-border)] px-3 py-2 text-[10px] uppercase tracking-[0.18em] text-[var(--color-outline)]">
|
||||
{t?.('tool.partialInput') ?? 'Partial input'}
|
||||
</div>
|
||||
<CodeViewer code={partialInput} language="json" maxLines={8} />
|
||||
<CodeViewer code={formattedInput} language="json" maxLines={8} wrapLongLines />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function formatPartialJsonInput(source: string): string {
|
||||
const trimmed = source.trim()
|
||||
if (!trimmed) return source
|
||||
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(trimmed), null, 2)
|
||||
} catch {
|
||||
return formatJsonLikeInput(trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
function formatJsonLikeInput(source: string): string {
|
||||
let output = ''
|
||||
let indent = 0
|
||||
let inString = false
|
||||
let escaping = false
|
||||
let skipWhitespace = false
|
||||
|
||||
const newline = () => {
|
||||
output = output.trimEnd()
|
||||
output += `\n${' '.repeat(indent)}`
|
||||
skipWhitespace = true
|
||||
}
|
||||
|
||||
for (const char of source) {
|
||||
if (inString) {
|
||||
output += char
|
||||
if (escaping) {
|
||||
escaping = false
|
||||
} else if (char === '\\') {
|
||||
escaping = true
|
||||
} else if (char === '"') {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (skipWhitespace && /\s/.test(char)) continue
|
||||
skipWhitespace = false
|
||||
|
||||
if (char === '"') {
|
||||
inString = true
|
||||
output += char
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '{' || char === '[') {
|
||||
output += char
|
||||
indent += 1
|
||||
newline()
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === '}' || char === ']') {
|
||||
indent = Math.max(0, indent - 1)
|
||||
if (!output.endsWith('\n')) newline()
|
||||
output += char
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === ',') {
|
||||
output += char
|
||||
newline()
|
||||
continue
|
||||
}
|
||||
|
||||
if (char === ':') {
|
||||
output += ': '
|
||||
skipWhitespace = true
|
||||
continue
|
||||
}
|
||||
|
||||
output += char
|
||||
}
|
||||
|
||||
return output.trimEnd()
|
||||
}
|
||||
|
||||
function getPendingSummary(
|
||||
toolName: string,
|
||||
t?: (key: TranslationKey, params?: Record<string, string | number>) => string,
|
||||
|
||||
@ -143,6 +143,32 @@ describe('chat blocks', () => {
|
||||
expect(container.textContent).not.toContain('"content"')
|
||||
})
|
||||
|
||||
it('formats and wraps pending Bash partial JSON input when expanded', () => {
|
||||
const partialInput = [
|
||||
'{"command":"cat << \'HTMLEOF\' > /tmp/index.html\\n<!DOCTYPE html>\\n<html lang=\\"zh-CN\\">",',
|
||||
'"description":"Create HTML shell command"}',
|
||||
].join('')
|
||||
const { container } = render(
|
||||
<ToolCallBlock
|
||||
toolName="Bash"
|
||||
input={{ command: 'cat << \'HTMLEOF\' > /tmp/index.html', description: 'Create HTML shell command' }}
|
||||
isPending
|
||||
partialInput={partialInput}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
expect(container.textContent).toContain('Partial input')
|
||||
expect(container.textContent).toContain('json')
|
||||
expect(container.textContent).toContain('4 lines')
|
||||
expect(container.textContent).not.toContain('1 line')
|
||||
|
||||
const contentWrapper = container.querySelector('[data-code-viewer-content]') as HTMLElement | null
|
||||
expect(contentWrapper?.style.whiteSpace).toBe('pre-wrap')
|
||||
expect(contentWrapper?.style.wordBreak).toBe('break-word')
|
||||
})
|
||||
|
||||
it('shows non-windowed Writer preview stats before the 120-line limit', () => {
|
||||
const { container } = render(
|
||||
<ToolCallBlock
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user