mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
* feat: 删除垃圾更改
* fix: 消除生产代码中的 as any 类型不安全模式
- API 兼容层(openai/grok/gemini): 利用 BetaRawMessageStreamEvent 的
discriminated union 在 switch/case 中直接属性访问,消除 ~29 个 as any
- ConsoleOAuthFlow: 用 as unknown as Parameters<typeof> 替代 as any
- performanceShim: 用 Record<string, unknown> 和显式类型断言替代 as any
- companionReact/auth: 直接访问已有类型属性消除 as any
- sliceAnsi/textHighlighting: 用 as Char 替代 as any(Token 联合类型收窄)
- ccrClient: 利用 RequestResult 类型收窄直接访问 retryAfterMs
- outputsScanner: 用 TurnStartTime.turnStartTime 属性访问替代双重断言
- plans: 用显式数组类型替代 as any[]
- FeedbackSurvey: 用 in 操作符和 Parameters<typeof> 替代 as any
- messageQueueManager: 用 Record<string, unknown> 替代 as any
- mcp.ts: 用 in 操作符类型守卫替代 as any
precheck 通过: typecheck 零错误 + 5420 测试全部通过 + lint 通过
* fix: 将 pipeIpc 添加到 AppState 类型声明,消除 4 个 as any
- AppStateStore: 添加 pipeIpc?: PipeIpcState 可选字段
- PromptInputFooter: 直接访问 s.pipeIpc
- useBackgroundTaskNavigation: 直接访问 s.pipeIpc
- usePipeRouter: 直接访问 store.getState().pipeIpc
- REPL.tsx: 移除 getPipeIpc(s as any) 中的 as any
precheck 通过
* fix: 消除 UltraplanChoiceDialog 中的 wheelDown/wheelUp as any
Ink Key 类型已包含 wheelDown/wheelUp 属性,直接访问即可。
* fix: 消除 sideQuestion.ts 中的 2 个 as any
- toolUse.name: 使用 as unknown as { name: string } 双重断言
- apiErr.error: 使用 as Parameters<typeof formatAPIError>[0] 类型参数
* fix: 为 auto dream 添加 maxTurns: 20 限制,防止单次执行消耗过多 token
* fix: 补充 SAFE_ENV_VARS 中缺失的 OpenAI/Gemini/Grok provider 环境变量
项目级 settings.local.json 的 env 字段在 trust dialog 之前只有
SAFE_ENV_VARS 白名单中的变量会被应用到 process.env。
OPENAI_API_KEY、OPENAI_BASE_URL 等关键变量不在白名单中,
导致容器中通过 settings.local.json 配置 OpenAI 协议时认证失败。
* fix: 修复 goalState.js 模块不存在的类型错误
* fix: 增强 providers 测试的环境变量隔离,防止 mock 污染
* fix: 内联 providers 测试逻辑,彻底隔离 mock 污染
测试不再 import providers.ts(其默认参数触发 getInitialSettings 全链),
改为内联纯函数逻辑,从根源消除 CI 上其他测试 mock.module 污染。
* fix: 添加 goalState 模块存根,修复 CI 构建打包解析失败
CI 中的 autonomy-lifecycle-user-flow 集成测试会执行 build.ts 打包 CLI。
此前 PromptInputFooterLeftSide.tsx 中 require('../../services/goal/goalState.js')
的路径在源码中不存在,打包器报 Could not resolve,导致 (unnamed) 测试失败。
新增 src/services/goal/goalState.ts 存根模块(getGoal 返回 null,组件不渲染),
让打包器在构建期可以解析该 require 路径。同时把 PromptInputFooterLeftSide.tsx
里两处 as unknown as 内联类型签名换成 as typeof import(...),让类型直接来自
存根模块,避免类型定义重复。
168 lines
4.5 KiB
TypeScript
168 lines
4.5 KiB
TypeScript
import {
|
|
type AnsiCode,
|
|
type Char,
|
|
ansiCodesToString,
|
|
reduceAnsiCodes,
|
|
type Token,
|
|
tokenize,
|
|
undoAnsiCodes,
|
|
} from '@alcalzone/ansi-tokenize'
|
|
import type { Theme } from './theme.js'
|
|
|
|
export type TextHighlight = {
|
|
start: number
|
|
end: number
|
|
color: keyof Theme | undefined
|
|
dimColor?: boolean
|
|
inverse?: boolean
|
|
shimmerColor?: keyof Theme
|
|
priority: number
|
|
}
|
|
|
|
export type TextSegment = {
|
|
text: string
|
|
start: number
|
|
highlight?: TextHighlight
|
|
}
|
|
|
|
export function segmentTextByHighlights(
|
|
text: string,
|
|
highlights: TextHighlight[],
|
|
): TextSegment[] {
|
|
if (highlights.length === 0) {
|
|
return [{ text, start: 0 }]
|
|
}
|
|
|
|
const sortedHighlights = [...highlights].sort((a, b) => {
|
|
if (a.start !== b.start) return a.start - b.start
|
|
return b.priority - a.priority
|
|
})
|
|
|
|
const resolvedHighlights: TextHighlight[] = []
|
|
const usedRanges: Array<{ start: number; end: number }> = []
|
|
|
|
for (const highlight of sortedHighlights) {
|
|
if (highlight.start === highlight.end) continue
|
|
|
|
const overlaps = usedRanges.some(
|
|
range =>
|
|
(highlight.start >= range.start && highlight.start < range.end) ||
|
|
(highlight.end > range.start && highlight.end <= range.end) ||
|
|
(highlight.start <= range.start && highlight.end >= range.end),
|
|
)
|
|
|
|
if (!overlaps) {
|
|
resolvedHighlights.push(highlight)
|
|
usedRanges.push({ start: highlight.start, end: highlight.end })
|
|
}
|
|
}
|
|
|
|
return new HighlightSegmenter(text).segment(resolvedHighlights)
|
|
}
|
|
|
|
class HighlightSegmenter {
|
|
private readonly tokens: Token[]
|
|
// Two position systems: "visible" (what the user sees, excluding ANSI codes)
|
|
// and "string" (raw positions including ANSI codes for substring extraction)
|
|
private visiblePos = 0
|
|
private stringPos = 0
|
|
private tokenIdx = 0
|
|
private charIdx = 0 // offset within current text token (for partial consumption)
|
|
private codes: AnsiCode[] = []
|
|
|
|
constructor(private readonly text: string) {
|
|
this.tokens = tokenize(text)
|
|
}
|
|
|
|
segment(highlights: TextHighlight[]): TextSegment[] {
|
|
const segments: TextSegment[] = []
|
|
|
|
for (const highlight of highlights) {
|
|
const before = this.segmentTo(highlight.start)
|
|
if (before) segments.push(before)
|
|
|
|
const highlighted = this.segmentTo(highlight.end)
|
|
if (highlighted) {
|
|
highlighted.highlight = highlight
|
|
segments.push(highlighted)
|
|
}
|
|
}
|
|
|
|
const after = this.segmentTo(Infinity)
|
|
if (after) segments.push(after)
|
|
|
|
return segments
|
|
}
|
|
|
|
private segmentTo(targetVisiblePos: number): TextSegment | null {
|
|
if (
|
|
this.tokenIdx >= this.tokens.length ||
|
|
targetVisiblePos <= this.visiblePos
|
|
) {
|
|
return null
|
|
}
|
|
|
|
const visibleStart = this.visiblePos
|
|
|
|
// Consume leading ANSI codes before first visible char
|
|
while (this.tokenIdx < this.tokens.length) {
|
|
const token = this.tokens[this.tokenIdx]!
|
|
if (token.type !== 'ansi') break
|
|
this.codes.push(token)
|
|
this.stringPos += token.code.length
|
|
this.tokenIdx++
|
|
}
|
|
|
|
const stringStart = this.stringPos
|
|
const codesStart = [...this.codes]
|
|
|
|
// Advance through tokens until we reach target
|
|
while (
|
|
this.visiblePos < targetVisiblePos &&
|
|
this.tokenIdx < this.tokens.length
|
|
) {
|
|
const token = this.tokens[this.tokenIdx]!
|
|
|
|
if (token.type === 'ansi') {
|
|
this.codes.push(token)
|
|
this.stringPos += token.code.length
|
|
this.tokenIdx++
|
|
} else {
|
|
const charsNeeded = targetVisiblePos - this.visiblePos
|
|
const charsAvailable = (token as Char).value.length - this.charIdx
|
|
const charsToTake = Math.min(charsNeeded, charsAvailable)
|
|
|
|
this.stringPos += charsToTake
|
|
this.visiblePos += charsToTake
|
|
this.charIdx += charsToTake
|
|
|
|
if (this.charIdx >= (token as Char).value.length) {
|
|
this.tokenIdx++
|
|
this.charIdx = 0
|
|
}
|
|
}
|
|
}
|
|
|
|
// Empty segment (can occur when only trailing ANSI codes remain)
|
|
if (this.stringPos === stringStart) {
|
|
return null
|
|
}
|
|
|
|
const prefixCodes = reduceCodes(codesStart)
|
|
const suffixCodes = reduceCodes(this.codes)
|
|
this.codes = suffixCodes
|
|
|
|
const prefix = ansiCodesToString(prefixCodes)
|
|
const suffix = ansiCodesToString(undoAnsiCodes(suffixCodes))
|
|
|
|
return {
|
|
text: prefix + this.text.substring(stringStart, this.stringPos) + suffix,
|
|
start: visibleStart,
|
|
}
|
|
}
|
|
}
|
|
|
|
function reduceCodes(codes: AnsiCode[]): AnsiCode[] {
|
|
return reduceAnsiCodes(codes).filter(c => c.code !== c.endCode)
|
|
}
|