mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 21:05:51 +00:00
* 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>
187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
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>
|
||
)
|
||
})
|