更新大量 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>
This commit is contained in:
claude-code-best
2026-04-04 23:24:27 +08:00
committed by GitHub
parent 02694918b5
commit 5b1a52b8e0
559 changed files with 103807 additions and 101817 deletions

View File

@@ -1,280 +1,140 @@
import { c as _c } from "react/compiler-runtime";
import type { StructuredPatchHunk } from 'diff';
import { resolve } from 'path';
import React, { useMemo } from 'react';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { Box, Text } from '../../ink.js';
import { getCwd } from '../../utils/cwd.js';
import { readFileSafe } from '../../utils/file.js';
import { Divider } from '../design-system/Divider.js';
import { StructuredDiff } from '../StructuredDiff.js';
import type { StructuredPatchHunk } from 'diff'
import { resolve } from 'path'
import React, { useMemo } from 'react'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Box, Text } from '../../ink.js'
import { getCwd } from '../../utils/cwd.js'
import { readFileSafe } from '../../utils/file.js'
import { Divider } from '../design-system/Divider.js'
import { StructuredDiff } from '../StructuredDiff.js'
type Props = {
filePath: string;
hunks: StructuredPatchHunk[];
isLargeFile?: boolean;
isBinary?: boolean;
isTruncated?: boolean;
isUntracked?: boolean;
};
filePath: string
hunks: StructuredPatchHunk[]
isLargeFile?: boolean
isBinary?: boolean
isTruncated?: boolean
isUntracked?: boolean
}
/**
* Displays the diff content for a single file.
* Uses StructuredDiff for word-level diffing and syntax highlighting.
* No scrolling - renders all lines (max 400 due to parsing limits).
*/
export function DiffDetailView(t0) {
const $ = _c(53);
const {
filePath,
hunks,
isLargeFile,
isBinary,
isTruncated,
isUntracked
} = t0;
const {
columns
} = useTerminalSize();
let t1;
bb0: {
export function DiffDetailView({
filePath,
hunks,
isLargeFile,
isBinary,
isTruncated,
isUntracked,
}: Props): React.ReactNode {
const { columns } = useTerminalSize()
// Read file content for syntax detection and multiline construct handling.
// Only computed when this component is rendered (detail view mode).
const { firstLine, fileContent } = useMemo(() => {
if (!filePath) {
let t2;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t2 = {
firstLine: null,
fileContent: undefined
};
$[0] = t2;
} else {
t2 = $[0];
}
t1 = t2;
break bb0;
return { firstLine: null, fileContent: undefined }
}
let content;
let t2;
if ($[1] !== filePath) {
const fullPath = resolve(getCwd(), filePath);
content = readFileSafe(fullPath);
t2 = content?.split("\n")[0] ?? null;
$[1] = filePath;
$[2] = content;
$[3] = t2;
} else {
content = $[2];
t2 = $[3];
const fullPath = resolve(getCwd(), filePath)
const content = readFileSafe(fullPath)
return {
firstLine: content?.split('\n')[0] ?? null,
fileContent: content ?? undefined,
}
const t3 = content ?? undefined;
let t4;
if ($[4] !== t2 || $[5] !== t3) {
t4 = {
firstLine: t2,
fileContent: t3
};
$[4] = t2;
$[5] = t3;
$[6] = t4;
} else {
t4 = $[6];
}
t1 = t4;
}
const {
firstLine,
fileContent
} = t1;
}, [filePath])
// Handle untracked files
if (isUntracked) {
let t2;
if ($[7] !== filePath) {
t2 = <Text bold={true}>{filePath}</Text>;
$[7] = filePath;
$[8] = t2;
} else {
t2 = $[8];
}
let t3;
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Text dimColor={true}> (untracked)</Text>;
$[9] = t3;
} else {
t3 = $[9];
}
let t4;
if ($[10] !== t2) {
t4 = <Box>{t2}{t3}</Box>;
$[10] = t2;
$[11] = t4;
} else {
t4 = $[11];
}
let t5;
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Divider padding={4} />;
$[12] = t5;
} else {
t5 = $[12];
}
let t6;
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
t6 = <Text dimColor={true} italic={true}>New file not yet staged.</Text>;
$[13] = t6;
} else {
t6 = $[13];
}
let t7;
if ($[14] !== filePath) {
t7 = <Box flexDirection="column">{t6}<Text dimColor={true} italic={true}>Run `git add {filePath}` to see line counts.</Text></Box>;
$[14] = filePath;
$[15] = t7;
} else {
t7 = $[15];
}
let t8;
if ($[16] !== t4 || $[17] !== t7) {
t8 = <Box flexDirection="column" width="100%">{t4}{t5}{t7}</Box>;
$[16] = t4;
$[17] = t7;
$[18] = t8;
} else {
t8 = $[18];
}
return t8;
return (
<Box flexDirection="column" width="100%">
<Box>
<Text bold>{filePath}</Text>
<Text dimColor> (untracked)</Text>
</Box>
<Divider padding={4} />
<Box flexDirection="column">
<Text dimColor italic>
New file not yet staged.
</Text>
<Text dimColor italic>
Run `git add {filePath}` to see line counts.
</Text>
</Box>
</Box>
)
}
// Handle binary files
if (isBinary) {
let t2;
if ($[19] !== filePath) {
t2 = <Box><Text bold={true}>{filePath}</Text></Box>;
$[19] = filePath;
$[20] = t2;
} else {
t2 = $[20];
}
let t3;
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Divider padding={4} />;
$[21] = t3;
} else {
t3 = $[21];
}
let t4;
if ($[22] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Box flexDirection="column"><Text dimColor={true} italic={true}>Binary file - cannot display diff</Text></Box>;
$[22] = t4;
} else {
t4 = $[22];
}
let t5;
if ($[23] !== t2) {
t5 = <Box flexDirection="column" width="100%">{t2}{t3}{t4}</Box>;
$[23] = t2;
$[24] = t5;
} else {
t5 = $[24];
}
return t5;
return (
<Box flexDirection="column" width="100%">
<Box>
<Text bold>{filePath}</Text>
</Box>
<Divider padding={4} />
<Box flexDirection="column">
<Text dimColor italic>
Binary file - cannot display diff
</Text>
</Box>
</Box>
)
}
// Handle large files
if (isLargeFile) {
let t2;
if ($[25] !== filePath) {
t2 = <Box><Text bold={true}>{filePath}</Text></Box>;
$[25] = filePath;
$[26] = t2;
} else {
t2 = $[26];
}
let t3;
if ($[27] === Symbol.for("react.memo_cache_sentinel")) {
t3 = <Divider padding={4} />;
$[27] = t3;
} else {
t3 = $[27];
}
let t4;
if ($[28] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Box flexDirection="column"><Text dimColor={true} italic={true}>Large file - diff exceeds 1 MB limit</Text></Box>;
$[28] = t4;
} else {
t4 = $[28];
}
let t5;
if ($[29] !== t2) {
t5 = <Box flexDirection="column" width="100%">{t2}{t3}{t4}</Box>;
$[29] = t2;
$[30] = t5;
} else {
t5 = $[30];
}
return t5;
return (
<Box flexDirection="column" width="100%">
<Box>
<Text bold>{filePath}</Text>
</Box>
<Divider padding={4} />
<Box flexDirection="column">
<Text dimColor italic>
Large file - diff exceeds 1 MB limit
</Text>
</Box>
</Box>
)
}
let t2;
if ($[31] !== filePath) {
t2 = <Text bold={true}>{filePath}</Text>;
$[31] = filePath;
$[32] = t2;
} else {
t2 = $[32];
}
let t3;
if ($[33] !== isTruncated) {
t3 = isTruncated && <Text dimColor={true}> (truncated)</Text>;
$[33] = isTruncated;
$[34] = t3;
} else {
t3 = $[34];
}
let t4;
if ($[35] !== t2 || $[36] !== t3) {
t4 = <Box>{t2}{t3}</Box>;
$[35] = t2;
$[36] = t3;
$[37] = t4;
} else {
t4 = $[37];
}
let t5;
if ($[38] === Symbol.for("react.memo_cache_sentinel")) {
t5 = <Divider padding={4} />;
$[38] = t5;
} else {
t5 = $[38];
}
let t6;
if ($[39] !== columns || $[40] !== fileContent || $[41] !== filePath || $[42] !== firstLine || $[43] !== hunks) {
t6 = hunks.length === 0 ? <Text dimColor={true}>No diff content</Text> : hunks.map((hunk, index) => <StructuredDiff key={index} patch={hunk} filePath={filePath} firstLine={firstLine} fileContent={fileContent} dim={false} width={columns - 2 - 2} />);
$[39] = columns;
$[40] = fileContent;
$[41] = filePath;
$[42] = firstLine;
$[43] = hunks;
$[44] = t6;
} else {
t6 = $[44];
}
let t7;
if ($[45] !== t6) {
t7 = <Box flexDirection="column">{t6}</Box>;
$[45] = t6;
$[46] = t7;
} else {
t7 = $[46];
}
let t8;
if ($[47] !== isTruncated) {
t8 = isTruncated && <Text dimColor={true} italic={true}> diff truncated (exceeded 400 line limit)</Text>;
$[47] = isTruncated;
$[48] = t8;
} else {
t8 = $[48];
}
let t9;
if ($[49] !== t4 || $[50] !== t7 || $[51] !== t8) {
t9 = <Box flexDirection="column" width="100%">{t4}{t5}{t7}{t8}</Box>;
$[49] = t4;
$[50] = t7;
$[51] = t8;
$[52] = t9;
} else {
t9 = $[52];
}
return t9;
const outerPaddingX = 1
const outerBorderWidth = 1
return (
<Box flexDirection="column" width="100%">
<Box>
<Text bold>{filePath}</Text>
{isTruncated && <Text dimColor> (truncated)</Text>}
</Box>
<Divider padding={4} />
<Box flexDirection="column">
{hunks.length === 0 ? (
<Text dimColor>No diff content</Text>
) : (
hunks.map((hunk, index) => (
<StructuredDiff
key={index}
patch={hunk}
filePath={filePath}
firstLine={firstLine}
fileContent={fileContent}
dim={false}
width={columns - 2 * outerPaddingX - 2 * outerBorderWidth}
/>
))
)}
</Box>
{isTruncated && (
<Text dimColor italic>
diff truncated (exceeded 400 line limit)
</Text>
)}
</Box>
)
}

View File

@@ -1,382 +1,289 @@
import { c as _c } from "react/compiler-runtime";
import type { StructuredPatchHunk } from 'diff';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import type { CommandResultDisplay } from '../../commands.js';
import { useRegisterOverlay } from '../../context/overlayContext.js';
import { type DiffData, useDiffData } from '../../hooks/useDiffData.js';
import { type TurnDiff, useTurnDiffs } from '../../hooks/useTurnDiffs.js';
import { Box, Text } from '../../ink.js';
import { useKeybindings } from '../../keybindings/useKeybinding.js';
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js';
import type { Message } from '../../types/message.js';
import { plural } from '../../utils/stringUtils.js';
import { Byline } from '../design-system/Byline.js';
import { Dialog } from '../design-system/Dialog.js';
import { DiffDetailView } from './DiffDetailView.js';
import { DiffFileList } from './DiffFileList.js';
import type { StructuredPatchHunk } from 'diff'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import type { CommandResultDisplay } from '../../commands.js'
import { useRegisterOverlay } from '../../context/overlayContext.js'
import { type DiffData, useDiffData } from '../../hooks/useDiffData.js'
import { type TurnDiff, useTurnDiffs } from '../../hooks/useTurnDiffs.js'
import { Box, Text } from '../../ink.js'
import { useKeybindings } from '../../keybindings/useKeybinding.js'
import { useShortcutDisplay } from '../../keybindings/useShortcutDisplay.js'
import type { Message } from '../../types/message.js'
import { plural } from '../../utils/stringUtils.js'
import { Byline } from '../design-system/Byline.js'
import { Dialog } from '../design-system/Dialog.js'
import { DiffDetailView } from './DiffDetailView.js'
import { DiffFileList } from './DiffFileList.js'
type Props = {
messages: Message[];
onDone: (result?: string, options?: {
display?: CommandResultDisplay;
}) => void;
};
type ViewMode = 'list' | 'detail';
type DiffSource = {
type: 'current';
} | {
type: 'turn';
turn: TurnDiff;
};
messages: Message[]
onDone: (
result?: string,
options?: { display?: CommandResultDisplay },
) => void
}
type ViewMode = 'list' | 'detail'
type DiffSource = { type: 'current' } | { type: 'turn'; turn: TurnDiff }
function turnDiffToDiffData(turn: TurnDiff): DiffData {
const files = Array.from(turn.files.values()).map(f => ({
path: f.filePath,
linesAdded: f.linesAdded,
linesRemoved: f.linesRemoved,
isBinary: false,
isLargeFile: false,
isTruncated: false,
isNewFile: f.isNewFile
})).sort((a, b) => a.path.localeCompare(b.path));
const hunks = new Map<string, StructuredPatchHunk[]>();
const files = Array.from(turn.files.values())
.map(f => ({
path: f.filePath,
linesAdded: f.linesAdded,
linesRemoved: f.linesRemoved,
isBinary: false,
isLargeFile: false,
isTruncated: false,
isNewFile: f.isNewFile,
}))
.sort((a, b) => a.path.localeCompare(b.path))
const hunks = new Map<string, StructuredPatchHunk[]>()
for (const f of turn.files.values()) {
hunks.set(f.filePath, f.hunks);
hunks.set(f.filePath, f.hunks)
}
return {
stats: {
filesCount: turn.stats.filesChanged,
linesAdded: turn.stats.linesAdded,
linesRemoved: turn.stats.linesRemoved
linesRemoved: turn.stats.linesRemoved,
},
files,
hunks,
loading: false
};
loading: false,
}
}
export function DiffDialog(t0) {
const $ = _c(73);
const {
messages,
onDone
} = t0;
const gitDiffData = useDiffData();
const turnDiffs = useTurnDiffs(messages);
const [viewMode, setViewMode] = useState("list");
const [selectedIndex, setSelectedIndex] = useState(0);
const [sourceIndex, setSourceIndex] = useState(0);
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = {
type: "current"
};
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== turnDiffs) {
t2 = [t1, ...turnDiffs.map(_temp)];
$[1] = turnDiffs;
$[2] = t2;
} else {
t2 = $[2];
}
const sources = t2;
const currentSource = sources[sourceIndex];
const currentTurn = currentSource?.type === "turn" ? currentSource.turn : null;
let t3;
if ($[3] !== currentTurn || $[4] !== gitDiffData) {
t3 = currentTurn ? turnDiffToDiffData(currentTurn) : gitDiffData;
$[3] = currentTurn;
$[4] = gitDiffData;
$[5] = t3;
} else {
t3 = $[5];
}
const diffData = t3;
const selectedFile = diffData.files[selectedIndex];
let t4;
if ($[6] !== diffData.hunks || $[7] !== selectedFile) {
t4 = selectedFile ? diffData.hunks.get(selectedFile.path) || [] : [];
$[6] = diffData.hunks;
$[7] = selectedFile;
$[8] = t4;
} else {
t4 = $[8];
}
const selectedHunks = t4;
let t5;
let t6;
if ($[9] !== sourceIndex || $[10] !== sources.length) {
t5 = () => {
if (sourceIndex >= sources.length) {
setSourceIndex(Math.max(0, sources.length - 1));
}
};
t6 = [sources.length, sourceIndex];
$[9] = sourceIndex;
$[10] = sources.length;
$[11] = t5;
$[12] = t6;
} else {
t5 = $[11];
t6 = $[12];
}
useEffect(t5, t6);
const prevSourceIndex = useRef(sourceIndex);
let t7;
let t8;
if ($[13] !== sourceIndex) {
t7 = () => {
if (prevSourceIndex.current !== sourceIndex) {
setSelectedIndex(0);
prevSourceIndex.current = sourceIndex;
}
};
t8 = [sourceIndex];
$[13] = sourceIndex;
$[14] = t7;
$[15] = t8;
} else {
t7 = $[14];
t8 = $[15];
}
useEffect(t7, t8);
useRegisterOverlay("diff-dialog", undefined);
let t10;
let t9;
if ($[16] !== sources.length || $[17] !== viewMode) {
t9 = () => {
if (viewMode === "detail") {
setViewMode("list");
} else {
if (viewMode === "list" && sources.length > 1) {
setSourceIndex(_temp2);
export function DiffDialog({ messages, onDone }: Props): React.ReactNode {
const gitDiffData = useDiffData()
const turnDiffs = useTurnDiffs(messages)
const [viewMode, setViewMode] = useState<ViewMode>('list')
const [selectedIndex, setSelectedIndex] = useState<number>(0)
const [sourceIndex, setSourceIndex] = useState<number>(0)
const sources: DiffSource[] = useMemo(
() => [
{ type: 'current' },
...turnDiffs.map((turn): DiffSource => ({ type: 'turn', turn })),
],
[turnDiffs],
)
const currentSource = sources[sourceIndex]
const currentTurn = currentSource?.type === 'turn' ? currentSource.turn : null
const diffData = useMemo((): DiffData => {
return currentTurn ? turnDiffToDiffData(currentTurn) : gitDiffData
}, [currentTurn, gitDiffData])
const selectedFile = diffData.files[selectedIndex]
const selectedHunks = useMemo(() => {
return selectedFile ? diffData.hunks.get(selectedFile.path) || [] : []
}, [selectedFile, diffData.hunks])
// Clamp sourceIndex when sources shrink (e.g., conversation rewind)
useEffect(() => {
if (sourceIndex >= sources.length) {
setSourceIndex(Math.max(0, sources.length - 1))
}
}, [sources.length, sourceIndex])
// Reset file selection when source changes
const prevSourceIndex = useRef(sourceIndex)
useEffect(() => {
if (prevSourceIndex.current !== sourceIndex) {
setSelectedIndex(0)
prevSourceIndex.current = sourceIndex
}
}, [sourceIndex])
// Register as modal overlay so Chat keybindings and CancelRequestHandler
// are disabled while DiffDialog is showing
useRegisterOverlay('diff-dialog')
// Diff dialog navigation keybindings
// View-mode dependent: left/right arrows have different behavior based on mode
// (source tab switching vs back navigation), and up/down/enter are
// context-sensitive to viewMode
//
// Note: Escape handling (diff:dismiss) is NOT registered here because Dialog's
// built-in useKeybinding('confirm:no', handleCancel) already handles it.
// Having both would be dead code since Dialog's child effect registers first
// and calls stopImmediatePropagation(). The diff:dismiss binding in
// defaultBindings.ts is kept for useShortcutDisplay to show the "esc close" hint.
useKeybindings(
{
// Left arrow: in detail mode goes back, in list mode switches source
'diff:previousSource': () => {
if (viewMode === 'detail') {
setViewMode('list')
} else if (viewMode === 'list' && sources.length > 1) {
setSourceIndex(prev => Math.max(0, prev - 1))
}
}
};
t10 = () => {
if (viewMode === "list" && sources.length > 1) {
setSourceIndex(prev_0 => Math.min(sources.length - 1, prev_0 + 1));
}
};
$[16] = sources.length;
$[17] = viewMode;
$[18] = t10;
$[19] = t9;
} else {
t10 = $[18];
t9 = $[19];
}
let t11;
if ($[20] !== viewMode) {
t11 = () => {
if (viewMode === "detail") {
setViewMode("list");
}
};
$[20] = viewMode;
$[21] = t11;
} else {
t11 = $[21];
}
let t12;
if ($[22] !== selectedFile || $[23] !== viewMode) {
t12 = () => {
if (viewMode === "list" && selectedFile) {
setViewMode("detail");
}
};
$[22] = selectedFile;
$[23] = viewMode;
$[24] = t12;
} else {
t12 = $[24];
}
let t13;
if ($[25] !== viewMode) {
t13 = () => {
if (viewMode === "list") {
setSelectedIndex(_temp3);
}
};
$[25] = viewMode;
$[26] = t13;
} else {
t13 = $[26];
}
let t14;
if ($[27] !== diffData.files.length || $[28] !== viewMode) {
t14 = () => {
if (viewMode === "list") {
setSelectedIndex(prev_2 => Math.min(diffData.files.length - 1, prev_2 + 1));
}
};
$[27] = diffData.files.length;
$[28] = viewMode;
$[29] = t14;
} else {
t14 = $[29];
}
let t15;
if ($[30] !== t10 || $[31] !== t11 || $[32] !== t12 || $[33] !== t13 || $[34] !== t14 || $[35] !== t9) {
t15 = {
"diff:previousSource": t9,
"diff:nextSource": t10,
"diff:back": t11,
"diff:viewDetails": t12,
"diff:previousFile": t13,
"diff:nextFile": t14
};
$[30] = t10;
$[31] = t11;
$[32] = t12;
$[33] = t13;
$[34] = t14;
$[35] = t9;
$[36] = t15;
} else {
t15 = $[36];
}
let t16;
if ($[37] === Symbol.for("react.memo_cache_sentinel")) {
t16 = {
context: "DiffDialog"
};
$[37] = t16;
} else {
t16 = $[37];
}
useKeybindings(t15, t16);
let t17;
if ($[38] !== diffData.stats) {
t17 = diffData.stats ? <Text dimColor={true}>{diffData.stats.filesCount} {plural(diffData.stats.filesCount, "file")}{" "}changed{diffData.stats.linesAdded > 0 && <Text color="diffAddedWord"> +{diffData.stats.linesAdded}</Text>}{diffData.stats.linesRemoved > 0 && <Text color="diffRemovedWord"> -{diffData.stats.linesRemoved}</Text>}</Text> : null;
$[38] = diffData.stats;
$[39] = t17;
} else {
t17 = $[39];
}
const subtitle = t17;
const headerTitle = currentTurn ? `Turn ${currentTurn.turnIndex}` : "Uncommitted changes";
const headerSubtitle = currentTurn ? currentTurn.userPromptPreview ? `"${currentTurn.userPromptPreview}"` : "" : "(git diff HEAD)";
let t18;
if ($[40] !== sourceIndex || $[41] !== sources) {
t18 = sources.length > 1 ? <Box>{sourceIndex > 0 && <Text dimColor={true}> </Text>}{sources.map((source, i) => {
const isSelected = i === sourceIndex;
const label = source.type === "current" ? "Current" : `T${source.turn.turnIndex}`;
return <Text key={i} dimColor={!isSelected} bold={isSelected}>{i > 0 ? " \xB7 " : ""}{label}</Text>;
})}{sourceIndex < sources.length - 1 && <Text dimColor={true}> </Text>}</Box> : null;
$[40] = sourceIndex;
$[41] = sources;
$[42] = t18;
} else {
t18 = $[42];
}
const sourceSelector = t18;
const dismissShortcut = useShortcutDisplay("diff:dismiss", "DiffDialog", "esc");
let t19;
bb0: {
},
'diff:nextSource': () => {
if (viewMode === 'list' && sources.length > 1) {
setSourceIndex(prev => Math.min(sources.length - 1, prev + 1))
}
},
'diff:back': () => {
if (viewMode === 'detail') {
setViewMode('list')
}
},
'diff:viewDetails': () => {
if (viewMode === 'list' && selectedFile) {
setViewMode('detail')
}
},
'diff:previousFile': () => {
if (viewMode === 'list') {
setSelectedIndex(prev => Math.max(0, prev - 1))
}
},
'diff:nextFile': () => {
if (viewMode === 'list') {
setSelectedIndex(prev =>
Math.min(diffData.files.length - 1, prev + 1),
)
}
},
},
{ context: 'DiffDialog' },
)
const subtitle = diffData.stats ? (
<Text dimColor>
{diffData.stats.filesCount} {plural(diffData.stats.filesCount, 'file')}{' '}
changed
{diffData.stats.linesAdded > 0 && (
<Text color="diffAddedWord"> +{diffData.stats.linesAdded}</Text>
)}
{diffData.stats.linesRemoved > 0 && (
<Text color="diffRemovedWord"> -{diffData.stats.linesRemoved}</Text>
)}
</Text>
) : null
// Build header based on current source
const headerTitle = currentTurn
? `Turn ${currentTurn.turnIndex}`
: 'Uncommitted changes'
const headerSubtitle = currentTurn
? currentTurn.userPromptPreview
? `"${currentTurn.userPromptPreview}"`
: ''
: '(git diff HEAD)'
// Source selector pills
const sourceSelector =
sources.length > 1 ? (
<Box>
{sourceIndex > 0 && <Text dimColor> </Text>}
{sources.map((source, i) => {
const isSelected = i === sourceIndex
const label =
source.type === 'current' ? 'Current' : `T${source.turn.turnIndex}`
return (
<Text key={i} dimColor={!isSelected} bold={isSelected}>
{i > 0 ? ' · ' : ''}
{label}
</Text>
)
})}
{sourceIndex < sources.length - 1 && <Text dimColor> </Text>}
</Box>
) : null
const dismissShortcut = useShortcutDisplay(
'diff:dismiss',
'DiffDialog',
'esc',
)
// Determine the appropriate message when no files are shown
const emptyMessage = (() => {
if (diffData.loading) {
t19 = "Loading diff\u2026";
break bb0;
return 'Loading diff…'
}
if (currentTurn) {
t19 = "No file changes in this turn";
break bb0;
return 'No file changes in this turn'
}
if (diffData.stats && diffData.stats.filesCount > 0 && diffData.files.length === 0) {
t19 = "Too many files to display details";
break bb0;
// Check if we have stats but no files (too many files case)
if (
diffData.stats &&
diffData.stats.filesCount > 0 &&
diffData.files.length === 0
) {
return 'Too many files to display details'
}
return 'Working tree is clean'
})()
// Build title with header subtitle inline
const title = (
<Text>
{headerTitle}
{headerSubtitle && <Text dimColor> {headerSubtitle}</Text>}
</Text>
)
// Handle cancel/dismiss - in detail mode goes back, in list mode dismisses
function handleCancel(): void {
if (viewMode === 'detail') {
setViewMode('list')
} else {
onDone('Diff dialog dismissed', { display: 'system' })
}
t19 = "Working tree is clean";
}
const emptyMessage = t19;
let t20;
if ($[43] !== headerSubtitle) {
t20 = headerSubtitle && <Text dimColor={true}> {headerSubtitle}</Text>;
$[43] = headerSubtitle;
$[44] = t20;
} else {
t20 = $[44];
}
let t21;
if ($[45] !== headerTitle || $[46] !== t20) {
t21 = <Text>{headerTitle}{t20}</Text>;
$[45] = headerTitle;
$[46] = t20;
$[47] = t21;
} else {
t21 = $[47];
}
const title = t21;
let t22;
if ($[48] !== onDone || $[49] !== viewMode) {
t22 = function handleCancel() {
if (viewMode === "detail") {
setViewMode("list");
} else {
onDone("Diff dialog dismissed", {
display: "system"
});
return (
<Dialog
title={title}
onCancel={handleCancel}
color="background"
inputGuide={exitState =>
exitState.pending ? (
<Text>Press {exitState.keyName} again to exit</Text>
) : viewMode === 'list' ? (
<Byline>
{sources.length > 1 && <Text>/ source</Text>}
<Text>/ select</Text>
<Text>Enter view</Text>
<Text>{dismissShortcut} close</Text>
</Byline>
) : (
<Byline>
<Text> back</Text>
<Text>{dismissShortcut} close</Text>
</Byline>
)
}
};
$[48] = onDone;
$[49] = viewMode;
$[50] = t22;
} else {
t22 = $[50];
}
const handleCancel = t22;
let t23;
if ($[51] !== dismissShortcut || $[52] !== sources.length || $[53] !== viewMode) {
t23 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : viewMode === "list" ? <Byline>{sources.length > 1 && <Text>/ source</Text>}<Text>/ select</Text><Text>Enter view</Text><Text>{dismissShortcut} close</Text></Byline> : <Byline><Text> back</Text><Text>{dismissShortcut} close</Text></Byline>;
$[51] = dismissShortcut;
$[52] = sources.length;
$[53] = viewMode;
$[54] = t23;
} else {
t23 = $[54];
}
let t24;
if ($[55] !== diffData.files || $[56] !== emptyMessage || $[57] !== selectedFile?.isBinary || $[58] !== selectedFile?.isLargeFile || $[59] !== selectedFile?.isTruncated || $[60] !== selectedFile?.isUntracked || $[61] !== selectedFile?.path || $[62] !== selectedHunks || $[63] !== selectedIndex || $[64] !== viewMode) {
t24 = diffData.files.length === 0 ? <Box marginTop={1}><Text dimColor={true}>{emptyMessage}</Text></Box> : viewMode === "list" ? <Box flexDirection="column" marginTop={1}><DiffFileList files={diffData.files} selectedIndex={selectedIndex} /></Box> : <Box flexDirection="column" marginTop={1}><DiffDetailView filePath={selectedFile?.path || ""} hunks={selectedHunks} isLargeFile={selectedFile?.isLargeFile} isBinary={selectedFile?.isBinary} isTruncated={selectedFile?.isTruncated} isUntracked={selectedFile?.isUntracked} /></Box>;
$[55] = diffData.files;
$[56] = emptyMessage;
$[57] = selectedFile?.isBinary;
$[58] = selectedFile?.isLargeFile;
$[59] = selectedFile?.isTruncated;
$[60] = selectedFile?.isUntracked;
$[61] = selectedFile?.path;
$[62] = selectedHunks;
$[63] = selectedIndex;
$[64] = viewMode;
$[65] = t24;
} else {
t24 = $[65];
}
let t25;
if ($[66] !== handleCancel || $[67] !== sourceSelector || $[68] !== subtitle || $[69] !== t23 || $[70] !== t24 || $[71] !== title) {
t25 = <Dialog title={title} onCancel={handleCancel} color="background" inputGuide={t23}>{sourceSelector}{subtitle}{t24}</Dialog>;
$[66] = handleCancel;
$[67] = sourceSelector;
$[68] = subtitle;
$[69] = t23;
$[70] = t24;
$[71] = title;
$[72] = t25;
} else {
t25 = $[72];
}
return t25;
}
function _temp3(prev_1) {
return Math.max(0, prev_1 - 1);
}
function _temp2(prev) {
return Math.max(0, prev - 1);
}
function _temp(turn) {
return {
type: "turn",
turn
};
>
{sourceSelector}
{subtitle}
{diffData.files.length === 0 ? (
<Box marginTop={1}>
<Text dimColor>{emptyMessage}</Text>
</Box>
) : viewMode === 'list' ? (
<Box flexDirection="column" marginTop={1}>
<DiffFileList files={diffData.files} selectedIndex={selectedIndex} />
</Box>
) : (
<Box flexDirection="column" marginTop={1}>
<DiffDetailView
filePath={selectedFile?.path || ''}
hunks={selectedHunks}
isLargeFile={selectedFile?.isLargeFile}
isBinary={selectedFile?.isBinary}
isTruncated={selectedFile?.isTruncated}
isUntracked={selectedFile?.isUntracked}
/>
</Box>
)}
</Dialog>
)
}

View File

@@ -1,291 +1,153 @@
import { c as _c } from "react/compiler-runtime";
import figures from 'figures';
import React, { useMemo } from 'react';
import type { DiffFile } from '../../hooks/useDiffData.js';
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
import { Box, Text } from '../../ink.js';
import { truncateStartToWidth } from '../../utils/format.js';
import { plural } from '../../utils/stringUtils.js';
const MAX_VISIBLE_FILES = 5;
import figures from 'figures'
import React, { useMemo } from 'react'
import type { DiffFile } from '../../hooks/useDiffData.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { Box, Text } from '../../ink.js'
import { truncateStartToWidth } from '../../utils/format.js'
import { plural } from '../../utils/stringUtils.js'
const MAX_VISIBLE_FILES = 5
type Props = {
files: DiffFile[];
selectedIndex: number;
};
export function DiffFileList(t0) {
const $ = _c(36);
const {
files,
selectedIndex
} = t0;
const {
columns
} = useTerminalSize();
let t1;
bb0: {
files: DiffFile[]
selectedIndex: number
}
export function DiffFileList({ files, selectedIndex }: Props): React.ReactNode {
const { columns } = useTerminalSize()
// Calculate scroll window - must be before early return for hooks rules
const { startIndex, endIndex } = useMemo(() => {
if (files.length === 0 || files.length <= MAX_VISIBLE_FILES) {
let t2;
if ($[0] !== files.length) {
t2 = {
startIndex: 0,
endIndex: files.length
};
$[0] = files.length;
$[1] = t2;
} else {
t2 = $[1];
}
t1 = t2;
break bb0;
return { startIndex: 0, endIndex: files.length }
}
let start = Math.max(0, selectedIndex - Math.floor(MAX_VISIBLE_FILES / 2));
let end = start + MAX_VISIBLE_FILES;
// Keep selected item roughly in the middle
let start = Math.max(0, selectedIndex - Math.floor(MAX_VISIBLE_FILES / 2))
let end = start + MAX_VISIBLE_FILES
// Adjust if we're at the end
if (end > files.length) {
end = files.length;
start = Math.max(0, end - MAX_VISIBLE_FILES);
end = files.length
start = Math.max(0, end - MAX_VISIBLE_FILES)
}
let t2;
if ($[2] !== end || $[3] !== start) {
t2 = {
startIndex: start,
endIndex: end
};
$[2] = end;
$[3] = start;
$[4] = t2;
} else {
t2 = $[4];
}
t1 = t2;
}
const {
startIndex,
endIndex
} = t1;
return { startIndex: start, endIndex: end }
}, [files.length, selectedIndex])
if (files.length === 0) {
let t2;
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
t2 = <Text dimColor={true}>No changed files</Text>;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
return <Text dimColor>No changed files</Text>
}
let T0;
let hasMoreBelow;
let needsPagination;
let t2;
let t3;
let t4;
if ($[6] !== columns || $[7] !== endIndex || $[8] !== files || $[9] !== selectedIndex || $[10] !== startIndex) {
const visibleFiles = files.slice(startIndex, endIndex);
const hasMoreAbove = startIndex > 0;
hasMoreBelow = endIndex < files.length;
needsPagination = files.length > MAX_VISIBLE_FILES;
const maxPathWidth = Math.max(20, columns - 16 - 3 - 4);
T0 = Box;
t2 = "column";
if ($[17] !== hasMoreAbove || $[18] !== needsPagination || $[19] !== startIndex) {
t3 = needsPagination && <Text dimColor={true}>{hasMoreAbove ? `${startIndex} more ${plural(startIndex, "file")}` : " "}</Text>;
$[17] = hasMoreAbove;
$[18] = needsPagination;
$[19] = startIndex;
$[20] = t3;
} else {
t3 = $[20];
}
let t5;
if ($[21] !== maxPathWidth || $[22] !== selectedIndex || $[23] !== startIndex) {
t5 = (file, index) => <FileItem key={file.path} file={file} isSelected={startIndex + index === selectedIndex} maxPathWidth={maxPathWidth} />;
$[21] = maxPathWidth;
$[22] = selectedIndex;
$[23] = startIndex;
$[24] = t5;
} else {
t5 = $[24];
}
t4 = visibleFiles.map(t5);
$[6] = columns;
$[7] = endIndex;
$[8] = files;
$[9] = selectedIndex;
$[10] = startIndex;
$[11] = T0;
$[12] = hasMoreBelow;
$[13] = needsPagination;
$[14] = t2;
$[15] = t3;
$[16] = t4;
} else {
T0 = $[11];
hasMoreBelow = $[12];
needsPagination = $[13];
t2 = $[14];
t3 = $[15];
t4 = $[16];
}
let t5;
if ($[25] !== endIndex || $[26] !== files.length || $[27] !== hasMoreBelow || $[28] !== needsPagination) {
t5 = needsPagination && <Text dimColor={true}>{hasMoreBelow ? `${files.length - endIndex} more ${plural(files.length - endIndex, "file")}` : " "}</Text>;
$[25] = endIndex;
$[26] = files.length;
$[27] = hasMoreBelow;
$[28] = needsPagination;
$[29] = t5;
} else {
t5 = $[29];
}
let t6;
if ($[30] !== T0 || $[31] !== t2 || $[32] !== t3 || $[33] !== t4 || $[34] !== t5) {
t6 = <T0 flexDirection={t2}>{t3}{t4}{t5}</T0>;
$[30] = T0;
$[31] = t2;
$[32] = t3;
$[33] = t4;
$[34] = t5;
$[35] = t6;
} else {
t6 = $[35];
}
return t6;
const visibleFiles = files.slice(startIndex, endIndex)
const hasMoreAbove = startIndex > 0
const hasMoreBelow = endIndex < files.length
const needsPagination = files.length > MAX_VISIBLE_FILES
const statsWidth = 16
const pointerWidth = 3
const maxPathWidth = Math.max(20, columns - statsWidth - pointerWidth - 4)
return (
<Box flexDirection="column">
{needsPagination && (
<Text dimColor>
{hasMoreAbove
? `${startIndex} more ${plural(startIndex, 'file')}`
: ' '}
</Text>
)}
{visibleFiles.map((file, index) => (
<FileItem
key={file.path}
file={file}
isSelected={startIndex + index === selectedIndex}
maxPathWidth={maxPathWidth}
/>
))}
{needsPagination && (
<Text dimColor>
{hasMoreBelow
? `${files.length - endIndex} more ${plural(files.length - endIndex, 'file')}`
: ' '}
</Text>
)}
</Box>
)
}
function FileItem(t0) {
const $ = _c(14);
const {
file,
isSelected,
maxPathWidth
} = t0;
let t1;
if ($[0] !== file.path || $[1] !== maxPathWidth) {
t1 = truncateStartToWidth(file.path, maxPathWidth);
$[0] = file.path;
$[1] = maxPathWidth;
$[2] = t1;
} else {
t1 = $[2];
}
const displayPath = t1;
const pointer = isSelected ? figures.pointer + " " : " ";
const line = `${pointer}${displayPath}`;
const t2 = isSelected ? "background" : undefined;
let t3;
if ($[3] !== isSelected || $[4] !== line || $[5] !== t2) {
t3 = <Text bold={isSelected} color={t2} inverse={isSelected}>{line}</Text>;
$[3] = isSelected;
$[4] = line;
$[5] = t2;
$[6] = t3;
} else {
t3 = $[6];
}
let t4;
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
t4 = <Box flexGrow={1} />;
$[7] = t4;
} else {
t4 = $[7];
}
let t5;
if ($[8] !== file || $[9] !== isSelected) {
t5 = <FileStats file={file} isSelected={isSelected} />;
$[8] = file;
$[9] = isSelected;
$[10] = t5;
} else {
t5 = $[10];
}
let t6;
if ($[11] !== t3 || $[12] !== t5) {
t6 = <Box flexDirection="row">{t3}{t4}{t5}</Box>;
$[11] = t3;
$[12] = t5;
$[13] = t6;
} else {
t6 = $[13];
}
return t6;
function FileItem({
file,
isSelected,
maxPathWidth,
}: {
file: DiffFile
isSelected: boolean
maxPathWidth: number
}): React.ReactNode {
const displayPath = truncateStartToWidth(file.path, maxPathWidth)
const pointer = isSelected ? figures.pointer + ' ' : ' '
const line = `${pointer}${displayPath}`
return (
<Box flexDirection="row">
<Text
bold={isSelected}
color={isSelected ? 'background' : undefined}
inverse={isSelected}
>
{line}
</Text>
<Box flexGrow={1} />
<FileStats file={file} isSelected={isSelected} />
</Box>
)
}
function FileStats(t0) {
const $ = _c(20);
const {
file,
isSelected
} = t0;
function FileStats({
file,
isSelected,
}: {
file: DiffFile
isSelected: boolean
}): React.ReactNode {
if (file.isUntracked) {
const t1 = !isSelected;
let t2;
if ($[0] !== t1) {
t2 = <Text dimColor={t1} italic={true}>untracked</Text>;
$[0] = t1;
$[1] = t2;
} else {
t2 = $[1];
}
return t2;
return (
<Text dimColor={!isSelected} italic>
untracked
</Text>
)
}
if (file.isBinary) {
const t1 = !isSelected;
let t2;
if ($[2] !== t1) {
t2 = <Text dimColor={t1} italic={true}>Binary file</Text>;
$[2] = t1;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
return (
<Text dimColor={!isSelected} italic>
Binary file
</Text>
)
}
if (file.isLargeFile) {
const t1 = !isSelected;
let t2;
if ($[4] !== t1) {
t2 = <Text dimColor={t1} italic={true}>Large file modified</Text>;
$[4] = t1;
$[5] = t2;
} else {
t2 = $[5];
}
return t2;
return (
<Text dimColor={!isSelected} italic>
Large file modified
</Text>
)
}
let t1;
if ($[6] !== file.linesAdded || $[7] !== isSelected) {
t1 = file.linesAdded > 0 && <Text color="diffAddedWord" bold={isSelected}>+{file.linesAdded}</Text>;
$[6] = file.linesAdded;
$[7] = isSelected;
$[8] = t1;
} else {
t1 = $[8];
}
const t2 = file.linesAdded > 0 && file.linesRemoved > 0 && " ";
let t3;
if ($[9] !== file.linesRemoved || $[10] !== isSelected) {
t3 = file.linesRemoved > 0 && <Text color="diffRemovedWord" bold={isSelected}>-{file.linesRemoved}</Text>;
$[9] = file.linesRemoved;
$[10] = isSelected;
$[11] = t3;
} else {
t3 = $[11];
}
let t4;
if ($[12] !== file.isTruncated || $[13] !== isSelected) {
t4 = file.isTruncated && <Text dimColor={!isSelected}> (truncated)</Text>;
$[12] = file.isTruncated;
$[13] = isSelected;
$[14] = t4;
} else {
t4 = $[14];
}
let t5;
if ($[15] !== t1 || $[16] !== t2 || $[17] !== t3 || $[18] !== t4) {
t5 = <Text>{t1}{t2}{t3}{t4}</Text>;
$[15] = t1;
$[16] = t2;
$[17] = t3;
$[18] = t4;
$[19] = t5;
} else {
t5 = $[19];
}
return t5;
// Normal or truncated file - show line counts
return (
<Text>
{file.linesAdded > 0 && (
<Text color="diffAddedWord" bold={isSelected}>
+{file.linesAdded}
</Text>
)}
{file.linesAdded > 0 && file.linesRemoved > 0 && ' '}
{file.linesRemoved > 0 && (
<Text color="diffRemovedWord" bold={isSelected}>
-{file.linesRemoved}
</Text>
)}
{file.isTruncated && <Text dimColor={!isSelected}> (truncated)</Text>}
</Text>
)
}