更新大量 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,324 +1,308 @@
import { c as _c } from "react/compiler-runtime";
import { resolve as resolvePath } from 'path';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
import { useRegisterOverlay } from '../context/overlayContext.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { Text } from '../ink.js';
import { logEvent } from '../services/analytics/index.js';
import { getCwd } from '../utils/cwd.js';
import { openFileInExternalEditor } from '../utils/editor.js';
import { truncatePathMiddle, truncateToWidth } from '../utils/format.js';
import { highlightMatch } from '../utils/highlightMatch.js';
import { relativePath } from '../utils/permissions/filesystem.js';
import { readFileInRange } from '../utils/readFileInRange.js';
import { ripGrepStream } from '../utils/ripgrep.js';
import { FuzzyPicker } from './design-system/FuzzyPicker.js';
import { LoadingState } from './design-system/LoadingState.js';
import { resolve as resolvePath } from 'path'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { useRegisterOverlay } from '../context/overlayContext.js'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { Text } from '../ink.js'
import { logEvent } from '../services/analytics/index.js'
import { getCwd } from '../utils/cwd.js'
import { openFileInExternalEditor } from '../utils/editor.js'
import { truncatePathMiddle, truncateToWidth } from '../utils/format.js'
import { highlightMatch } from '../utils/highlightMatch.js'
import { relativePath } from '../utils/permissions/filesystem.js'
import { readFileInRange } from '../utils/readFileInRange.js'
import { ripGrepStream } from '../utils/ripgrep.js'
import { FuzzyPicker } from './design-system/FuzzyPicker.js'
import { LoadingState } from './design-system/LoadingState.js'
type Props = {
onDone: () => void;
onInsert: (text: string) => void;
};
onDone: () => void
onInsert: (text: string) => void
}
type Match = {
file: string;
line: number;
text: string;
};
const VISIBLE_RESULTS = 12;
const DEBOUNCE_MS = 100;
const PREVIEW_CONTEXT_LINES = 4;
file: string
line: number
text: string
}
const VISIBLE_RESULTS = 12
const DEBOUNCE_MS = 100
const PREVIEW_CONTEXT_LINES = 4
// rg -m is per-file; we also cap the parsed array to keep memory bounded.
const MAX_MATCHES_PER_FILE = 10;
const MAX_TOTAL_MATCHES = 500;
const MAX_MATCHES_PER_FILE = 10
const MAX_TOTAL_MATCHES = 500
/**
* Global Search dialog (ctrl+shift+f / cmd+shift+f).
* Debounced ripgrep search across the workspace.
*/
export function GlobalSearchDialog(t0) {
const $ = _c(40);
const {
onDone,
onInsert
} = t0;
useRegisterOverlay("global-search", undefined);
const {
columns,
rows
} = useTerminalSize();
const previewOnRight = columns >= 140;
const visibleResults = Math.min(VISIBLE_RESULTS, Math.max(4, rows - 14));
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = [];
$[0] = t1;
} else {
t1 = $[0];
}
const [matches, setMatches] = useState(t1);
const [truncated, setTruncated] = useState(false);
const [isSearching, setIsSearching] = useState(false);
const [query, setQuery] = useState("");
const [focused, setFocused] = useState(undefined);
const [preview, setPreview] = useState(null);
const abortRef = useRef(null);
const timeoutRef = useRef(null);
let t2;
let t3;
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
t2 = () => () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
abortRef.current?.abort();
};
t3 = [];
$[1] = t2;
$[2] = t3;
} else {
t2 = $[1];
t3 = $[2];
}
useEffect(t2, t3);
let t4;
let t5;
if ($[3] !== focused) {
t4 = () => {
if (!focused) {
setPreview(null);
return;
}
const controller = new AbortController();
const absolute = resolvePath(getCwd(), focused.file);
const start = Math.max(0, focused.line - PREVIEW_CONTEXT_LINES - 1);
readFileInRange(absolute, start, PREVIEW_CONTEXT_LINES * 2 + 1, undefined, controller.signal).then(r => {
if (controller.signal.aborted) {
return;
}
export function GlobalSearchDialog({
onDone,
onInsert,
}: Props): React.ReactNode {
useRegisterOverlay('global-search')
const { columns, rows } = useTerminalSize()
const previewOnRight = columns >= 140
// Chrome (title + search + matchLabel + hints + pane border + gaps) eats
// ~14 rows. Shrink the list on short terminals so the dialog doesn't clip.
const visibleResults = Math.min(VISIBLE_RESULTS, Math.max(4, rows - 14))
const [matches, setMatches] = useState<Match[]>([])
const [truncated, setTruncated] = useState(false)
const [isSearching, setIsSearching] = useState(false)
const [query, setQuery] = useState('')
const [focused, setFocused] = useState<Match | undefined>(undefined)
const [preview, setPreview] = useState<{
file: string
line: number
content: string
} | null>(null)
const abortRef = useRef<AbortController | null>(null)
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current)
abortRef.current?.abort()
}
}, [])
// Load context lines around the focused match. AbortController prevents
// holding ↓ from piling up reads.
useEffect(() => {
if (!focused) {
setPreview(null)
return
}
const controller = new AbortController()
const absolute = resolvePath(getCwd(), focused.file)
const start = Math.max(0, focused.line - PREVIEW_CONTEXT_LINES - 1)
void readFileInRange(
absolute,
start,
PREVIEW_CONTEXT_LINES * 2 + 1,
undefined,
controller.signal,
)
.then(r => {
if (controller.signal.aborted) return
setPreview({
file: focused.file,
line: focused.line,
content: r.content
});
}).catch(() => {
if (controller.signal.aborted) {
return;
}
content: r.content,
})
})
.catch(() => {
if (controller.signal.aborted) return
setPreview({
file: focused.file,
line: focused.line,
content: "(preview unavailable)"
});
});
return () => controller.abort();
};
t5 = [focused];
$[3] = focused;
$[4] = t4;
$[5] = t5;
} else {
t4 = $[4];
t5 = $[5];
content: '(preview unavailable)',
})
})
return () => controller.abort()
}, [focused])
const handleQueryChange = (q: string) => {
setQuery(q)
if (timeoutRef.current) clearTimeout(timeoutRef.current)
abortRef.current?.abort()
if (!q.trim()) {
setMatches(m => (m.length ? [] : m))
setIsSearching(false)
setTruncated(false)
return
}
const controller = new AbortController()
abortRef.current = controller
setIsSearching(true)
setTruncated(false)
// Client-filter existing results while rg walks — keeps something on
// screen instead of flashing blank. rg results are merged in (deduped by
// file:line) rather than replaced, so the count is monotonic within a
// query: it only grows as rg streams, never dips to the first chunk's
// size. Narrowing (new query extends old): filter is exact — any line
// that matched the old -F -i literal contains the new one iff its text
// includes the new query lowered. Non-narrowing (broadening/different):
// filter is best-effort — may briefly show a subset until rg fills in
// the rest.
const queryLower = q.toLowerCase()
setMatches(m => {
const filtered = m.filter(match =>
match.text.toLowerCase().includes(queryLower),
)
return filtered.length === m.length ? m : filtered
})
timeoutRef.current = setTimeout(
(query, controller, setMatches, setTruncated, setIsSearching) => {
// ripgrep outputs absolute paths when given an absolute target, so
// relativize against cwd to preserve directory context in the truncated
// display (otherwise the cwd prefix eats the width budget).
// relativePath() returns POSIX-normalized output so truncatePathMiddle
// (which uses lastIndexOf('/')) works on Windows too.
const cwd = getCwd()
let collected = 0
void ripGrepStream(
// -e disambiguates pattern from options when the query starts with '-'
// (e.g. searching for "--verbose" or "-rf"). See GrepTool.ts for the
// same precaution.
[
'-n',
'--no-heading',
'-i',
'-m',
String(MAX_MATCHES_PER_FILE),
'-F',
'-e',
query,
],
cwd,
controller.signal,
lines => {
if (controller.signal.aborted) return
const parsed: Match[] = []
for (const line of lines) {
const m = parseRipgrepLine(line)
if (!m) continue
const rel = relativePath(cwd, m.file)
parsed.push({ ...m, file: rel.startsWith('..') ? m.file : rel })
}
if (!parsed.length) return
collected += parsed.length
setMatches(prev => {
// Append+dedupe instead of replace: prev may hold client-
// filtered results that are valid matches for this query.
// Replacing would drop the count to this chunk's size then
// grow it back — visible as a flicker.
const seen = new Set(prev.map(matchKey))
const fresh = parsed.filter(p => !seen.has(matchKey(p)))
if (!fresh.length) return prev
const next = prev.concat(fresh)
return next.length > MAX_TOTAL_MATCHES
? next.slice(0, MAX_TOTAL_MATCHES)
: next
})
if (collected >= MAX_TOTAL_MATCHES) {
controller.abort()
setTruncated(true)
setIsSearching(false)
}
},
)
.catch(() => {})
// Stream closed with zero chunks — clear stale results so
// "No matches" renders instead of the previous query's list.
.finally(() => {
if (controller.signal.aborted) return
if (collected === 0) setMatches(m => (m.length ? [] : m))
setIsSearching(false)
})
},
DEBOUNCE_MS,
q,
controller,
setMatches,
setTruncated,
setIsSearching,
)
}
useEffect(t4, t5);
let t6;
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
t6 = q => {
setQuery(q);
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
const listWidth = previewOnRight
? Math.floor((columns - 10) * 0.5)
: columns - 8
const maxPathWidth = Math.max(20, Math.floor(listWidth * 0.4))
const maxTextWidth = Math.max(20, listWidth - maxPathWidth - 4)
const previewWidth = previewOnRight
? Math.max(40, columns - listWidth - 14)
: columns - 6
const handleOpen = (m: Match) => {
const opened = openFileInExternalEditor(
resolvePath(getCwd(), m.file),
m.line,
)
logEvent('tengu_global_search_select', {
result_count: matches.length,
opened_editor: opened,
})
onDone()
}
const handleInsert = (m: Match, mention: boolean) => {
onInsert(mention ? `@${m.file}#L${m.line} ` : `${m.file}:${m.line} `)
logEvent('tengu_global_search_insert', {
result_count: matches.length,
mention,
})
onDone()
}
// Always pass a non-empty string so the line is reserved — prevents the
// searchBox from bouncing when the count appears/disappears.
const matchLabel =
matches.length > 0
? `${matches.length}${truncated ? '+' : ''} matches${isSearching ? '…' : ''}`
: ' '
return (
<FuzzyPicker
title="Global Search"
placeholder="Type to search…"
items={matches}
getKey={matchKey}
visibleCount={visibleResults}
direction="up"
previewPosition={previewOnRight ? 'right' : 'bottom'}
onQueryChange={handleQueryChange}
onFocus={setFocused}
onSelect={handleOpen}
onTab={{ action: 'mention', handler: m => handleInsert(m, true) }}
onShiftTab={{
action: 'insert path',
handler: m => handleInsert(m, false),
}}
onCancel={onDone}
emptyMessage={q =>
isSearching ? 'Searching…' : q ? 'No matches' : 'Type to search…'
}
abortRef.current?.abort();
if (!q.trim()) {
setMatches(_temp);
setIsSearching(false);
setTruncated(false);
return;
matchLabel={matchLabel}
selectAction="open in editor"
renderItem={(m, isFocused) => (
<Text color={isFocused ? 'suggestion' : undefined}>
<Text dimColor>
{truncatePathMiddle(m.file, maxPathWidth)}:{m.line}
</Text>{' '}
{highlightMatch(
truncateToWidth(m.text.trimStart(), maxTextWidth),
query,
)}
</Text>
)}
renderPreview={m =>
preview?.file === m.file && preview.line === m.line ? (
<>
<Text dimColor>
{truncatePathMiddle(m.file, previewWidth)}:{m.line}
</Text>
{preview.content.split('\n').map((line, i) => (
<Text key={i}>
{highlightMatch(truncateToWidth(line, previewWidth), query)}
</Text>
))}
</>
) : (
<LoadingState message="Loading…" dimColor />
)
}
const controller_0 = new AbortController();
abortRef.current = controller_0;
setIsSearching(true);
setTruncated(false);
const queryLower = q.toLowerCase();
setMatches(m_0 => {
const filtered = m_0.filter(match => match.text.toLowerCase().includes(queryLower));
return filtered.length === m_0.length ? m_0 : filtered;
});
timeoutRef.current = setTimeout(_temp4, DEBOUNCE_MS, q, controller_0, setMatches, setTruncated, setIsSearching);
};
$[6] = t6;
} else {
t6 = $[6];
}
const handleQueryChange = t6;
const listWidth = previewOnRight ? Math.floor((columns - 10) * 0.5) : columns - 8;
const maxPathWidth = Math.max(20, Math.floor(listWidth * 0.4));
const maxTextWidth = Math.max(20, listWidth - maxPathWidth - 4);
const previewWidth = previewOnRight ? Math.max(40, columns - listWidth - 14) : columns - 6;
let t7;
if ($[7] !== matches.length || $[8] !== onDone) {
t7 = m_3 => {
const opened = openFileInExternalEditor(resolvePath(getCwd(), m_3.file), m_3.line);
logEvent("tengu_global_search_select", {
result_count: matches.length,
opened_editor: opened
});
onDone();
};
$[7] = matches.length;
$[8] = onDone;
$[9] = t7;
} else {
t7 = $[9];
}
const handleOpen = t7;
let t8;
if ($[10] !== matches.length || $[11] !== onDone || $[12] !== onInsert) {
t8 = (m_4, mention) => {
onInsert(mention ? `@${m_4.file}#L${m_4.line} ` : `${m_4.file}:${m_4.line} `);
logEvent("tengu_global_search_insert", {
result_count: matches.length,
mention
});
onDone();
};
$[10] = matches.length;
$[11] = onDone;
$[12] = onInsert;
$[13] = t8;
} else {
t8 = $[13];
}
const handleInsert = t8;
const matchLabel = matches.length > 0 ? `${matches.length}${truncated ? "+" : ""} matches${isSearching ? "\u2026" : ""}` : " ";
const t9 = previewOnRight ? "right" : "bottom";
let t10;
if ($[14] !== handleInsert) {
t10 = {
action: "mention",
handler: m_5 => handleInsert(m_5, true)
};
$[14] = handleInsert;
$[15] = t10;
} else {
t10 = $[15];
}
let t11;
if ($[16] !== handleInsert) {
t11 = {
action: "insert path",
handler: m_6 => handleInsert(m_6, false)
};
$[16] = handleInsert;
$[17] = t11;
} else {
t11 = $[17];
}
let t12;
if ($[18] !== isSearching) {
t12 = q_0 => isSearching ? "Searching\u2026" : q_0 ? "No matches" : "Type to search\u2026";
$[18] = isSearching;
$[19] = t12;
} else {
t12 = $[19];
}
let t13;
if ($[20] !== maxPathWidth || $[21] !== maxTextWidth || $[22] !== query) {
t13 = (m_7, isFocused) => <Text color={isFocused ? "suggestion" : undefined}><Text dimColor={true}>{truncatePathMiddle(m_7.file, maxPathWidth)}:{m_7.line}</Text>{" "}{highlightMatch(truncateToWidth(m_7.text.trimStart(), maxTextWidth), query)}</Text>;
$[20] = maxPathWidth;
$[21] = maxTextWidth;
$[22] = query;
$[23] = t13;
} else {
t13 = $[23];
}
let t14;
if ($[24] !== preview || $[25] !== previewWidth || $[26] !== query) {
t14 = m_8 => preview?.file === m_8.file && preview.line === m_8.line ? <><Text dimColor={true}>{truncatePathMiddle(m_8.file, previewWidth)}:{m_8.line}</Text>{preview.content.split("\n").map((line_0, i) => <Text key={i}>{highlightMatch(truncateToWidth(line_0, previewWidth), query)}</Text>)}</> : <LoadingState message={"Loading\u2026"} dimColor={true} />;
$[24] = preview;
$[25] = previewWidth;
$[26] = query;
$[27] = t14;
} else {
t14 = $[27];
}
let t15;
if ($[28] !== handleOpen || $[29] !== matchLabel || $[30] !== matches || $[31] !== onDone || $[32] !== t10 || $[33] !== t11 || $[34] !== t12 || $[35] !== t13 || $[36] !== t14 || $[37] !== t9 || $[38] !== visibleResults) {
t15 = <FuzzyPicker title="Global Search" placeholder={"Type to search\u2026"} items={matches} getKey={matchKey} visibleCount={visibleResults} direction="up" previewPosition={t9} onQueryChange={handleQueryChange} onFocus={setFocused} onSelect={handleOpen} onTab={t10} onShiftTab={t11} onCancel={onDone} emptyMessage={t12} matchLabel={matchLabel} selectAction="open in editor" renderItem={t13} renderPreview={t14} />;
$[28] = handleOpen;
$[29] = matchLabel;
$[30] = matches;
$[31] = onDone;
$[32] = t10;
$[33] = t11;
$[34] = t12;
$[35] = t13;
$[36] = t14;
$[37] = t9;
$[38] = visibleResults;
$[39] = t15;
} else {
t15 = $[39];
}
return t15;
}
function _temp4(query_0, controller_1, setMatches_0, setTruncated_0, setIsSearching_0) {
const cwd = getCwd();
let collected = 0;
ripGrepStream(["-n", "--no-heading", "-i", "-m", String(MAX_MATCHES_PER_FILE), "-F", "-e", query_0], cwd, controller_1.signal, lines => {
if (controller_1.signal.aborted) {
return;
}
const parsed = [];
for (const line of lines) {
const m_1 = parseRipgrepLine(line);
if (!m_1) {
continue;
}
const rel = relativePath(cwd, m_1.file);
parsed.push({
...m_1,
file: rel.startsWith("..") ? m_1.file : rel
});
}
if (!parsed.length) {
return;
}
collected = collected + parsed.length;
collected;
setMatches_0(prev => {
const seen = new Set(prev.map(matchKey));
const fresh = parsed.filter(p => !seen.has(matchKey(p)));
if (!fresh.length) {
return prev;
}
const next = prev.concat(fresh);
return next.length > MAX_TOTAL_MATCHES ? next.slice(0, MAX_TOTAL_MATCHES) : next;
});
if (collected >= MAX_TOTAL_MATCHES) {
controller_1.abort();
setTruncated_0(true);
setIsSearching_0(false);
}
}).catch(_temp2).finally(() => {
if (controller_1.signal.aborted) {
return;
}
if (collected === 0) {
setMatches_0(_temp3);
}
setIsSearching_0(false);
});
}
function _temp3(m_2) {
return m_2.length ? [] : m_2;
}
function _temp2() {}
function _temp(m) {
return m.length ? [] : m;
/>
)
}
function matchKey(m: Match): string {
return `${m.file}:${m.line}`;
return `${m.file}:${m.line}`
}
/**
@@ -329,14 +313,10 @@ function matchKey(m: Match): string {
* @internal exported for testing
*/
export function parseRipgrepLine(line: string): Match | null {
const m = /^(.*?):(\d+):(.*)$/.exec(line);
if (!m) return null;
const [, file, lineStr, text] = m;
const lineNum = Number(lineStr);
if (!file || !Number.isFinite(lineNum)) return null;
return {
file,
line: lineNum,
text: text ?? ''
};
const m = /^(.*?):(\d+):(.*)$/.exec(line)
if (!m) return null
const [, file, lineStr, text] = m
const lineNum = Number(lineStr)
if (!file || !Number.isFinite(lineNum)) return null
return { file, line: lineNum, text: text ?? '' }
}