Files
claude-code/src/utils/exportRenderer.tsx
claude-code-best 5b1a52b8e0 更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)

纯格式化:移除分号、React Compiler import、import 多行展开。
修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 23:24:27 +08:00

139 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef } from 'react'
import stripAnsi from 'strip-ansi'
import { Messages } from '../components/Messages.js'
import { KeybindingProvider } from '../keybindings/KeybindingContext.js'
import { loadKeybindingsSyncWithWarnings } from '../keybindings/loadUserBindings.js'
import type { KeybindingContextName } from '../keybindings/types.js'
import { AppStateProvider } from '../state/AppState.js'
import type { Tools } from '../Tool.js'
import type { Message } from '../types/message.js'
import { renderToAnsiString } from './staticRender.js'
/**
* Minimal keybinding provider for static/headless renders.
* Provides keybinding context without the ChordInterceptor (which uses useInput
* and would hang in headless renders with no stdin).
*/
function StaticKeybindingProvider({
children,
}: {
children: React.ReactNode
}): React.ReactNode {
const { bindings } = loadKeybindingsSyncWithWarnings()
const pendingChordRef = useRef(null)
const handlerRegistryRef = useRef(new Map())
const activeContexts = useRef(new Set<KeybindingContextName>()).current
return (
<KeybindingProvider
bindings={bindings}
pendingChordRef={pendingChordRef}
pendingChord={null}
setPendingChord={() => {}}
activeContexts={activeContexts}
registerActiveContext={() => {}}
unregisterActiveContext={() => {}}
handlerRegistryRef={handlerRegistryRef}
>
{children}
</KeybindingProvider>
)
}
// Upper-bound how many NormalizedMessages a Message can produce.
// normalizeMessages splits one Message with N content blocks into N
// NormalizedMessages — 1:1 with block count. String content = 1 block.
// AttachmentMessage etc. have no .message and normalize to ≤1.
function normalizedUpperBound(m: Message): number {
if (!('message' in m)) return 1
const c = m.message.content
return Array.isArray(c) ? c.length : 1
}
/**
* Streams rendered messages in chunks, ANSI codes preserved. Each chunk is a
* fresh renderToAnsiString — yoga layout tree + Ink's screen buffer are sized
* to the tallest CHUNK instead of the full session. Measured (Mar 2026,
* 538-msg session): 55% plateau RSS vs a single full render. The sink owns
* the output — write to stdout for `[` dump-to-scrollback, appendFile for `v`.
*
* Messages.renderRange slices AFTER normalize→group→collapse, so tool-call
* grouping stays correct across chunk seams; buildMessageLookups runs on
* the full normalized array so tool_use↔tool_result resolves regardless of
* which chunk each landed in.
*/
export async function streamRenderedMessages(
messages: Message[],
tools: Tools,
sink: (ansiChunk: string) => void | Promise<void>,
{
columns,
verbose = false,
chunkSize = 40,
onProgress,
}: {
columns?: number
verbose?: boolean
chunkSize?: number
onProgress?: (rendered: number) => void
} = {},
): Promise<void> {
const renderChunk = (range: readonly [number, number]) =>
renderToAnsiString(
<AppStateProvider>
<StaticKeybindingProvider>
<Messages
messages={messages}
tools={tools}
commands={[]}
verbose={verbose}
toolJSX={null}
toolUseConfirmQueue={[]}
inProgressToolUseIDs={new Set()}
isMessageSelectorVisible={false}
conversationId="export"
screen="prompt"
streamingToolUses={[]}
showAllInTranscript={true}
isLoading={false}
renderRange={range}
/>
</StaticKeybindingProvider>
</AppStateProvider>,
columns,
)
// renderRange indexes into the post-collapse array whose length we can't
// see from here — normalize splits each Message into one NormalizedMessage
// per content block (unbounded per message), collapse merges some back.
// Ceiling is the exact normalize output count + chunkSize so the loop
// always reaches the empty slice where break fires (collapse only shrinks).
let ceiling = chunkSize
for (const m of messages) ceiling += normalizedUpperBound(m)
for (let offset = 0; offset < ceiling; offset += chunkSize) {
const ansi = await renderChunk([offset, offset + chunkSize])
if (stripAnsi(ansi).trim() === '') break
await sink(ansi)
onProgress?.(offset + chunkSize)
}
}
/**
* Renders messages to a plain text string suitable for export.
* Uses the same React rendering logic as the interactive UI.
*/
export async function renderMessagesToPlainText(
messages: Message[],
tools: Tools = [],
columns?: number,
): Promise<string> {
const parts: string[] = []
await streamRenderedMessages(
messages,
tools,
chunk => void parts.push(stripAnsi(chunk)),
{ columns },
)
return parts.join('')
}