Files
claude-code/src/components/StructuredDiff.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

187 lines
6.4 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 type { StructuredPatchHunk } from 'diff'
import * as React from 'react'
import { memo } from 'react'
import { useSettings } from '../hooks/useSettings.js'
import { Box, NoSelect, RawAnsi, useTheme } from '../ink.js'
import { isFullscreenEnvEnabled } from '../utils/fullscreen.js'
import sliceAnsi from '../utils/sliceAnsi.js'
import { expectColorDiff } from './StructuredDiff/colorDiff.js'
import { StructuredDiffFallback } from './StructuredDiff/Fallback.js'
type Props = {
patch: StructuredPatchHunk
dim: boolean
filePath: string // File path for language detection
firstLine: string | null // First line of file for shebang detection
fileContent?: string // Full file content for syntax context (multiline strings, etc.)
width: number
skipHighlighting?: boolean // Skip syntax highlighting
}
// REPL.tsx renders <Messages> at two disjoint tree positions (transcript
// early-return vs prompt-mode nested in FullscreenLayout), so ctrl+o
// unmounts/remounts the entire message tree and React's memo cache is lost.
// Keep both the NAPI result AND the pre-split gutter/content columns at
// module level so the only work on remount is a WeakMap lookup plus two
// <ink-raw-ansi> leaves — not a fresh syntax highlight, nor N sliceAnsi
// calls + 6N Yoga nodes.
//
// PR #21439 (fullscreen default-on) made gutterWidth>0 the default path,
// reactivating the per-line <DiffLine> branch that PR #20378 had bypassed.
// Caching the split here restores the O(1)-leaves-per-diff invariant.
type CachedRender = {
lines: string[]
// Two RawAnsi columns replace what was N DiffLine rows. sliceAnsi work
// moves from per-remount to cold-cache-only; parseToSpans is eliminated
// entirely (RawAnsi bypasses Ansi parsing).
gutterWidth: number
gutters: string[] | null
contents: string[] | null
}
const RENDER_CACHE = new WeakMap<
StructuredPatchHunk,
Map<string, CachedRender>
>()
// Gutter width matches the Rust module's layout: marker (1) + space +
// right-aligned line number (max_digits) + space. Depends only on patch
// identity (the WeakMap key), so it's cacheable alongside the NAPI output.
function computeGutterWidth(patch: StructuredPatchHunk): number {
const maxLineNumber = Math.max(
patch.oldStart + patch.oldLines - 1,
patch.newStart + patch.newLines - 1,
1,
)
return maxLineNumber.toString().length + 3 // marker + 2 padding spaces
}
function renderColorDiff(
patch: StructuredPatchHunk,
firstLine: string | null,
filePath: string,
fileContent: string | null,
theme: string,
width: number,
dim: boolean,
splitGutter: boolean,
): CachedRender | null {
const ColorDiff = expectColorDiff()
if (!ColorDiff) return null
// Defensive: if the gutter would eat the whole render width (narrow
// terminal), skip the split. Rust already wraps to `width` so the
// single-column output stays correct; we just lose noSelect. Without
// this, sliceAnsi(line, gutterWidth) would return empty content and
// RawAnsi(width<=0) is untested.
const rawGutterWidth = splitGutter ? computeGutterWidth(patch) : 0
const gutterWidth =
rawGutterWidth > 0 && rawGutterWidth < width ? rawGutterWidth : 0
const key = `${theme}|${width}|${dim ? 1 : 0}|${gutterWidth}|${firstLine ?? ''}|${filePath}`
let perHunk = RENDER_CACHE.get(patch)
const hit = perHunk?.get(key)
if (hit) return hit
const lines = new ColorDiff(patch, firstLine, filePath, fileContent).render(
theme,
width,
dim,
)
if (lines === null) return null
// Pre-split the gutter column once (cold-cache). sliceAnsi preserves
// styles across the cut; the Rust module already pads the gutter to
// gutterWidth so the narrow RawAnsi column's width matches its cells.
let gutters: string[] | null = null
let contents: string[] | null = null
if (gutterWidth > 0) {
gutters = lines.map(l => sliceAnsi(l, 0, gutterWidth))
contents = lines.map(l => sliceAnsi(l, gutterWidth))
}
const entry: CachedRender = { lines, gutterWidth, gutters, contents }
if (!perHunk) {
perHunk = new Map()
RENDER_CACHE.set(patch, perHunk)
}
// Cap the inner map: width is part of the key, so terminal resize while a
// diff is visible accumulates a full render copy per distinct width. Four
// variants (two widths × dim on/off) covers the steady state; beyond that
// the user is actively resizing and old widths are stale.
if (perHunk.size >= 4) perHunk.clear()
perHunk.set(key, entry)
return entry
}
export const StructuredDiff = memo(function StructuredDiff({
patch,
dim,
filePath,
firstLine,
fileContent,
width,
skipHighlighting = false,
}: Props): React.ReactNode {
const [theme] = useTheme()
const settings = useSettings()
const syntaxHighlightingDisabled =
settings.syntaxHighlightingDisabled ?? false
// Ensure width is at least 1 to prevent crashes in the Rust NAPI module
// which expects u32 (can't handle negative numbers)
const safeWidth = Math.max(1, Math.floor(width))
// Only split out a noSelect gutter in fullscreen mode — terminal native
// selection is used otherwise and noSelect is meaningless. Both branches
// are now O(1) Yoga leaves per diff on remount (2 vs 1), so this gate
// only saves cold-cache sliceAnsi work when fullscreen is off.
const splitGutter = isFullscreenEnvEnabled()
const cached =
skipHighlighting || syntaxHighlightingDisabled
? null
: renderColorDiff(
patch,
firstLine,
filePath,
fileContent ?? null,
theme,
safeWidth,
dim,
splitGutter,
)
if (!cached) {
return (
<Box>
<StructuredDiffFallback patch={patch} dim={dim} width={width} />
</Box>
)
}
const { lines, gutterWidth, gutters, contents } = cached
// Two-column layout: gutter (noSelect) + content. NoSelect marks the
// Box's computed bounds non-selectable; RawAnsi's measure func sets
// rawHeight=lines.length, so one tall leaf gets the same noSelect
// coverage N per-row Boxes would — without the per-row Yoga cost.
if (gutterWidth > 0 && gutters && contents) {
return (
<Box flexDirection="row">
<NoSelect fromLeftEdge>
<RawAnsi lines={gutters} width={gutterWidth} />
</NoSelect>
<RawAnsi lines={contents} width={safeWidth - gutterWidth} />
</Box>
)
}
return (
<Box>
<RawAnsi lines={lines} width={safeWidth} />
</Box>
)
})